Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions .github/workflows/release-proposal-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ on:
required: false
type: boolean
default: false
level_overrides:
description: >
Force the bump level of specific crates instead of computing it, as
comma-separated CRATE=LEVEL (e.g. "libdd-capabilities=minor, libdd-ipc=major").
Every crate named must be part of the release.
required: false
type: string
default: ''

concurrency:
group: release-proposal-dispatch-group
Expand Down Expand Up @@ -231,18 +239,15 @@ jobs:
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c #stable
with:
toolchain: ${{ env.RUSTUP_TOOLCHAIN }}
- uses: taiki-e/cache-cargo-install-action@7447f04c51f2ba27ca35e7f1e28fab848c5b3ba7 # 2.3.1
with:
tool: cargo-public-api@0.52.0
- uses: taiki-e/cache-cargo-install-action@7447f04c51f2ba27ca35e7f1e28fab848c5b3ba7 # 2.3.1
with:
tool: cargo-release
- uses: taiki-e/cache-cargo-install-action@7447f04c51f2ba27ca35e7f1e28fab848c5b3ba7 # 2.3.1
with:
tool: git-cliff
- uses: taiki-e/cache-cargo-install-action@7447f04c51f2ba27ca35e7f1e28fab848c5b3ba7 # 2.3.1

- name: Install dependencies
run: |
sudo apt update && sudo apt install -y libssl-dev # cargo-public-api dependency

- name: Install cargo tools
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # 2.49.27
with:
tool: cargo-semver-checks@0.48.0
tool: cargo-public-api@0.52.0, cargo-release@1.1.5, git-cliff@2.14.1, cargo-semver-checks@0.48.0

- uses: DataDog/dd-octo-sts-action@acaa02eee7e3bb0839e4272dacb37b8f3b58ba80 # v1.0.3
id: octo-sts
Expand Down Expand Up @@ -490,6 +495,7 @@ jobs:
EPHEMERAL_BRANCH: ${{ steps.ephemeral-branch.outputs.ephemeral_branch }}
IS_HOTFIX: ${{ steps.ephemeral-branch.outputs.is_hotfix }}
BYPASS_STANDARD_CHECKS: ${{ inputs.bypass_standard_checks }}
LEVEL_OVERRIDES: ${{ inputs.level_overrides }}
run: |
set -euo pipefail

Expand All @@ -503,6 +509,7 @@ jobs:
ARGS=()
if [ "$IS_HOTFIX" = "true" ]; then ARGS+=(--hotfix); fi
if [ "$BYPASS_STANDARD_CHECKS" = "true" ]; then ARGS+=(--bypass-standard-checks); fi
if [ -n "$LEVEL_OVERRIDES" ]; then ARGS+=(--level-overrides "$LEVEL_OVERRIDES"); fi

"${WORKFLOW_SCRIPTS_ROOT}/release-version-bumps.sh" \
--commits-by-crate /tmp/commits-by-crate.json \
Expand Down Expand Up @@ -652,6 +659,8 @@ jobs:
PROPOSAL_BRANCH_PREFIX: ${{ env.PROPOSAL_BRANCH_PREFIX }}
RELEASE_BRANCH_PREFIX: ${{ env.RELEASE_BRANCH_PREFIX }}
CRATES_COUNT: ${{ needs.validate-inputs.outputs.count }}
CRATES_DISPLAY: ${{ needs.validate-inputs.outputs.crates_display }}
LEVEL_OVERRIDES: ${{ inputs.level_overrides }}
run: |
BRANCH_NAME="${{ needs.cargo-release.outputs.branch_name }}"
EPHEMERAL_BRANCH="${{ needs.cargo-release.outputs.ephemeral_branch }}"
Expand Down Expand Up @@ -688,6 +697,9 @@ jobs:
"",
(if .version then "**Next version:** `\(.version)`" else null end),
"**Semver bump:** `\(.level)`",
(if (.level_override // "") != "" then
"**Level set by hand:** `\(.level_override)` was requested via the level_overrides input instead of being computed"
else null end),
(if .tag then "**Tag:** `\(.tag)`\n" else null end),
(if (.major_bumps // [] | length) > 0 then
"### :warning: major bump forced due to:\n\n"
Expand All @@ -713,7 +725,25 @@ jobs:
POSSESSIVE="its"
fi

PR_BODY="# Release proposal for ${{ needs.validate-inputs.outputs.crates_display }} and $POSSESSIVE dependencies
# A machine-readable record of the inputs this proposal was generated from.
# Built with jq so operator-supplied values are JSON-escaped rather than pasted
# into markdown. A `-->` inside any value would close the comment early and
# truncate what a reader parses back; `>` only occurs inside JSON strings here
# and \u003e decodes to the same character, so escaping it keeps the payload
# intact.
BYPASS_JSON=false
if [ "$BYPASS_STANDARD_CHECKS" = "true" ]; then BYPASS_JSON=true; fi
PROPOSAL_INPUTS=$(jq -nc \
--arg crates "$CRATES_DISPLAY" \
--arg main_start_ref "$MAIN_START_REF" \
--arg level_overrides "$LEVEL_OVERRIDES" \
--argjson bypass_standard_checks "$BYPASS_JSON" \
'{crates: $crates, main_start_ref: $main_start_ref,
level_overrides: $level_overrides,
bypass_standard_checks: $bypass_standard_checks}')
INPUTS_NOTE="<!-- release-proposal-inputs: ${PROPOSAL_INPUTS//>/\\u003e} -->"$'\n\n'

PR_BODY="${INPUTS_NOTE}# Release proposal for ${{ needs.validate-inputs.outputs.crates_display }} and $POSSESSIVE dependencies

This PR contains version bumps based on public API changes and commits since last release.

Expand Down
92 changes: 80 additions & 12 deletions scripts/release-version-bumps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#
# Usage: ./release-version-bumps.sh --commits-by-crate FILE --out FILE --branch BRANCH
# [--hotfix] [--bypass-standard-checks]
# [--level-overrides CRATE=LEVEL,...]
#
# For every crate in the input, one of four things happens:
#
Expand All @@ -17,9 +18,17 @@
# major-bump check can pull it back into the release, or drop it.
# skipped its tag is not the latest for that crate, so a newer release already
# exists elsewhere. Overridden by --hotfix and --bypass-standard-checks.
# released semver-level.sh picks the level, cargo-release applies it.
# released semver-level.sh picks the level unless --level-overrides names the crate,
# in which case that level is used and semver-level.sh is not run.
# cargo-release applies whichever level won.
# initial no tag at all: released at 0.1.0, or the run fails.
#
# --level-overrides exists so a reviewer who spots a wrong level can correct it by
# re-dispatching the workflow, rather than checking the proposal branch out and running
# cargo-release by hand. Overriding the input rather than patching the branch keeps the
# cascade, the changelogs and the publication order consistent with the new level -- a
# level corrected to major can pull further crates into the release.
#
# Diagnostics go to stdout (they are the caller's job log); the JSON result is written
# to --out. semver-level.sh is resolved next to this script, so it always comes from
# the same checkout as the caller.
Expand All @@ -33,6 +42,7 @@ OUT_FILE=""
BRANCH_NAME=""
IS_HOTFIX=false
BYPASS_STANDARD_CHECKS=false
LEVEL_OVERRIDES=""

usage() {
echo "Usage: $0 --commits-by-crate FILE --out FILE --branch BRANCH [--hotfix] [--bypass-standard-checks]"
Expand All @@ -43,6 +53,8 @@ usage() {
echo " --branch BRANCH Branch cargo-release is allowed to operate on (required)"
echo " --hotfix Release even when the crate's tag is not the latest"
echo " --bypass-standard-checks Same, for testing runs"
echo " --level-overrides SPEC Comma-separated CRATE=LEVEL forcing a crate's bump"
echo " level (major|minor|patch) instead of computing it"
echo " --help, -h Show this message"
}

Expand All @@ -53,6 +65,7 @@ while [[ $# -gt 0 ]]; do
--branch) BRANCH_NAME="${2:?--branch needs a value}"; shift 2 ;;
--hotfix) IS_HOTFIX=true; shift ;;
--bypass-standard-checks) BYPASS_STANDARD_CHECKS=true; shift ;;
--level-overrides) LEVEL_OVERRIDES="${2:?--level-overrides needs a value}"; shift 2 ;;
--help|-h) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
esac
Expand All @@ -65,6 +78,27 @@ done
jq -e 'type == "array"' "$COMMITS_BY_CRATE" >/dev/null \
|| { echo "ERROR: $COMMITS_BY_CRATE is not a JSON array" >&2; exit 1; }

# Parse --level-overrides into a lookup. Whitespace is stripped per entry, matching how
# the workflow normalizes its `crates` input, so "a=minor, b=major" is accepted.
declare -A OVERRIDE_LEVEL=()
if [ -n "$LEVEL_OVERRIDES" ]; then
while IFS= read -r OV_ENTRY; do
[ -n "$OV_ENTRY" ] || continue
case "$OV_ENTRY" in
*=*) ;;
*) echo "ERROR: --level-overrides entry '$OV_ENTRY' is not CRATE=LEVEL" >&2; exit 1 ;;
esac
OV_CRATE="${OV_ENTRY%%=*}"
OV_LEVEL="${OV_ENTRY#*=}"
[ -n "$OV_CRATE" ] || { echo "ERROR: --level-overrides entry '$OV_ENTRY' has no crate name" >&2; exit 1; }
case "$OV_LEVEL" in
major|minor|patch) ;;
*) echo "ERROR: --level-overrides level for '$OV_CRATE' must be major, minor or patch, got '$OV_LEVEL'" >&2; exit 1 ;;
esac
OVERRIDE_LEVEL["$OV_CRATE"]="$OV_LEVEL"
done <<< "$(printf '%s\n' "$LEVEL_OVERRIDES" | tr ',' '\n' | sed 's/[[:space:]]//g')"
fi

echo "Release version bumps..."

# Initialize results array. It holds one row per candidate crate: those released here,
Expand All @@ -84,6 +118,21 @@ append_row() {
# script would exit 0 having released nothing.
CRATE_ROWS=$(jq -c '.[]' "$COMMITS_BY_CRATE")

# Every override must name a candidate. A name that is not one is a typo, or a crate the
# dependency closure never pulled in; ignoring it silently would ship the computed level
# while the operator believed they had corrected it.
if [ ${#OVERRIDE_LEVEL[@]} -gt 0 ]; then
CANDIDATE_NAMES=$(jq -r '.[].name' "$COMMITS_BY_CRATE")
for OV_CRATE in "${!OVERRIDE_LEVEL[@]}"; do
if ! grep -qxF -- "$OV_CRATE" <<< "$CANDIDATE_NAMES"; then
echo "ERROR: --level-overrides names '$OV_CRATE', which is not in this release." >&2
echo "Candidates are:" >&2
sed 's/^/ /' <<< "$CANDIDATE_NAMES" >&2
exit 1
fi
done
fi

# iterate over the commits and execute cargo release for each crate
while read -r crate; do
# $ROWS is empty when there are no candidates; <<< still feeds one blank line.
Expand All @@ -98,12 +147,20 @@ while read -r crate; do
TAG_COMMIT=""
RANGE=""
LEVEL=""
# The level --level-overrides asked for, recorded so the PR body can say the level was
# set by hand. Kept as the requested level rather than a flag: the libdd-* major-bump
# cascade may raise it afterwards, and then the row shows both what was asked and what
# the release landed on.
LEVEL_OVERRIDE_REQUESTED=""

# if there are no commits and there is an existing tag, do not release the crate here.
# but record it as a pending candidate
if [ "$COMMITS" = "[]" ] && [ "$TAG_EXISTS" = "true" ]; then
VERSION=$(echo "$crate" | jq -r '.version')
echo "No commits since last release for $NAME; deferring to the libdd-* major-bump check"
if [ -n "${OVERRIDE_LEVEL[$NAME]:-}" ]; then
echo "WARNING: --level-overrides asked for ${OVERRIDE_LEVEL[$NAME]} on $NAME, but it has no commits and is deferred; the override does not apply" >&2
fi
append_row --arg name "$NAME" \
--arg tag "$TAG" \
--arg version "$VERSION" \
Expand Down Expand Up @@ -143,17 +200,23 @@ while read -r crate; do
fi
fi

echo "Executing semver-level.sh for $NAME since $RANGE (tag: $TAG)..."
# stderr is folded in so the reason travels with a failure; without this the
# capture swallows it and the run aborts with nothing to go on.
if ! SEMVER_LEVEL=$("${SCRIPT_DIR}/semver-level.sh" "$NAME" "refs/tags/$TAG" 2>&1); then
echo "ERROR: semver-level.sh failed for $NAME:" >&2
echo "$SEMVER_LEVEL" >&2
exit 1
fi
echo "Semver level: $SEMVER_LEVEL"
if [ -n "${OVERRIDE_LEVEL[$NAME]:-}" ]; then
LEVEL="${OVERRIDE_LEVEL[$NAME]}"
LEVEL_OVERRIDE_REQUESTED="$LEVEL"
echo "Semver level for $NAME overridden to $LEVEL; not running semver-level.sh"
else
echo "Executing semver-level.sh for $NAME since $RANGE (tag: $TAG)..."
# stderr is folded in so the reason travels with a failure; without this the
# capture swallows it and the run aborts with nothing to go on.
if ! SEMVER_LEVEL=$("${SCRIPT_DIR}/semver-level.sh" "$NAME" "refs/tags/$TAG" 2>&1); then
echo "ERROR: semver-level.sh failed for $NAME:" >&2
echo "$SEMVER_LEVEL" >&2
exit 1
fi
echo "Semver level: $SEMVER_LEVEL"

LEVEL=$(echo "$SEMVER_LEVEL" | jq -r '.level')
LEVEL=$(echo "$SEMVER_LEVEL" | jq -r '.level')
fi

echo "Executing cargo release for $NAME since $TAG with level $LEVEL..."
cargo release version -p "$NAME" --prev-tag-name "$TAG" --allow-branch "$BRANCH_NAME" -x "$LEVEL" --no-confirm
Expand All @@ -167,6 +230,10 @@ while read -r crate; do
TAG=""
RANGE=""

if [ -n "${OVERRIDE_LEVEL[$NAME]:-}" ]; then
echo "WARNING: --level-overrides asked for ${OVERRIDE_LEVEL[$NAME]} on $NAME, but it has no previous tag; an initial release is 0.1.0 regardless" >&2
fi

# fail when the version is not an initial release
if [ "$VERSION" != "0.1.0" ]; then
echo "Error: $NAME is not a 0.1.0 release" >&2
Expand Down Expand Up @@ -195,7 +262,8 @@ while read -r crate; do
--argjson commits "$COMMITS" \
--arg path "$CRATE_PATH" \
--arg initial_release "$INITIAL_RELEASE" \
'. += [{"name": $name, "level": $level, "tag": $tag, "prev_tag": $prev_tag, "version": $version, "range": $range, "commits": $commits, "path": $path, "initial_release": $initial_release}]'
--arg level_override "$LEVEL_OVERRIDE_REQUESTED" \
'. += [{"name": $name, "level": $level, "tag": $tag, "prev_tag": $prev_tag, "version": $version, "range": $range, "commits": $commits, "path": $path, "initial_release": $initial_release, "level_override": $level_override}]'
done <<< "$CRATE_ROWS"

# Output the results
Expand Down
Loading