From b6125d41735647741ceac1ecc98e2101c8b39864 Mon Sep 17 00:00:00 2001 From: Mehdi ABAAKOUK Date: Tue, 3 Nov 2026 19:05:46 +0100 Subject: [PATCH] ci: release setup-cli when its default mergify-cli version changes Renovate bumps the default mergify-cli version in action.yml and the dependencies queue merges it, but nobody tagged a release afterwards: main moved to 2026.10.8 on 2026-10-27 while the newest release, v1.3.0, still installs 2026.7.21.1, and gha-mergify-ci, which pins a setup-cli release, kept the old CLI. Every push to main now compares the default version in action.yml with the one in the latest vX.Y.Z release. When they differ, it releases the next minor (v1.4.0 after v1.3.0, the bump v1.2.0 and v1.3.0 used for their CLI updates) at the pushed commit. The notes open with the CLI version and continue with GitHub's generated ones. Comparing with the release rather than with the push's own diff lets the next push catch up a run that failed. It refuses, and fails the run, when a commit since the latest release is marked breaking (`type!:` or `BREAKING CHANGE:`), since that needs a new major cut by hand, and when the tag already exists without a published release, since the API would publish on that tag's commit. A release created with GITHUB_TOKEN triggers no workflow, so the major tag move now runs in the same job, on every run: push, release and dispatch. It already picks the newest release of the major, and now skips when the tag is already there, so a move that failed or was skipped is repaired by the next push. Merging this is itself a push to main: it releases v1.4.0 with mergify-cli 2026.10.8 at its merge commit, unless v1.4.0 was cut by hand first, and moves v1 from v1.3.0 to it. workflow_dispatch without a tag moves the newest release's major tag and dry-runs the release check on the dispatched ref: it prints the full notes a push there would publish, and creates nothing. Fixes MRGFY-11553 Co-Authored-By: Claude Opus 5.5 Change-Id: I13deb11322db2b37c9fe7fe945b780967377005a --- .github/workflows/release.yaml | 111 ++++++++++++++++++++++++++++++--- README.md | 3 +- 2 files changed, 105 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4dc1ab0..f409f5d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -2,14 +2,20 @@ name: Release permissions: {} on: + push: + branches: + - main release: types: - published workflow_dispatch: inputs: tag: - description: A vX.Y.Z release tag; its major tag (vX) is moved - required: true + description: >- + A vX.Y.Z release tag whose major tag (vX) is moved. Leave empty to + move the newest release's major and dry-run the release a push to + this branch would cut. + required: false type: string concurrency: @@ -17,10 +23,8 @@ concurrency: cancel-in-progress: false jobs: - move-major-tag: - # `uses: Mergifyio/setup-cli@v1` resolves the v1 tag, so it has to follow - # every v1.x.y release or users silently stay on an old one. - if: ${{ github.event_name == 'workflow_dispatch' || !github.event.release.prerelease }} + release: + if: ${{ github.event_name != 'release' || !github.event.release.prerelease }} timeout-minutes: 5 runs-on: ubuntu-26.04 permissions: @@ -28,9 +32,95 @@ jobs: env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} - TAG: ${{ inputs.tag || github.event.release.tag_name }} steps: + - name: Release main if its default mergify-cli version is not released + id: release + if: ${{ github.event_name != 'release' && !inputs.tag }} + env: + DRY_RUN: ${{ github.event_name == 'workflow_dispatch' }} + run: | + set -euo pipefail + default_cli() { + local version + version=$( + gh api "repos/$REPO/contents/action.yml?ref=$1" \ + -H "Accept: application/vnd.github.raw" \ + | yq '.inputs.mergify_cli_version.default' + ) + if [ -z "$version" ] || [ "$version" = null ]; then + echo "::error::No inputs.mergify_cli_version.default in action.yml at $1" >&2 + exit 1 + fi + echo "$version" + } + + current=$(default_cli "$GITHUB_SHA") + + # Compare against the latest release rather than this push's diff: + # a run that failed, or a push carrying several commits, is caught + # up by the next push to main. + latest=$( + gh api --paginate "repos/$REPO/releases" \ + --jq '.[] | select((.draft or .prerelease) | not) | .tag_name' \ + | { grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || true; } \ + | sort -V | tail -n 1 + ) + if [[ ! "$latest" =~ ^v([0-9]+)\.([0-9]+)\.[0-9]+$ ]]; then + echo "::error::No published vX.Y.Z release found" + exit 1 + fi + next="v${BASH_REMATCH[1]}.$((BASH_REMATCH[2] + 1)).0" + echo "tag=$latest" >> "$GITHUB_OUTPUT" + + released=$(default_cli "$latest") + if [ "$current" = "$released" ]; then + echo "$latest already installs mergify-cli $current by default" | tee -a "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + # A human used to be the guard against shipping a breaking change + # as a minor and moving the major tag onto it. + messages=$(gh api "repos/$REPO/compare/$latest...$GITHUB_SHA" --jq '.commits[].commit.message') + if grep -qE '^[a-z]+(\([^)]*\))?!:|^BREAKING[ -]CHANGE:' <<< "$messages"; then + echo "::error::A breaking change landed since $latest: release the next major by hand" + exit 1 + fi + # On an existing tag the API ignores --target and publishes the + # release on whatever commit that tag points at. + if gh api "repos/$REPO/git/ref/tags/$next" > /dev/null 2>&1; then + echo "::error::Tag $next exists without a published release: publish or delete it" + exit 1 + fi + + generated=$( + gh api -X POST "repos/$REPO/releases/generate-notes" \ + -f tag_name="$next" -f target_commitish="$GITHUB_SHA" \ + -f previous_tag_name="$latest" --jq .body + ) + notes="Installs [mergify-cli $current](https://github.com/Mergifyio/mergify-cli/releases/tag/$current) by default (was $released). + + $generated" + if [ "$DRY_RUN" = true ]; then + { + echo "Dry run: would release $next at $GITHUB_SHA with these notes:" + echo + echo "$notes" + } | tee -a "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + gh release create "$next" --repo "$REPO" --target "$GITHUB_SHA" \ + --title "$next" --notes "$notes" + echo "tag=$next" >> "$GITHUB_OUTPUT" + echo "Released $next at $GITHUB_SHA" | tee -a "$GITHUB_STEP_SUMMARY" + + # `uses: Mergifyio/setup-cli@v1` resolves the v1 tag, so it has to follow + # every v1.x.y release or users silently stay on an old one. It also runs + # on every push: a release created with GITHUB_TOKEN triggers no + # `release` workflow, and a move that failed is repaired by the next run. - name: Point the major tag at the latest release of that major + env: + TAG: ${{ steps.release.outputs.tag || inputs.tag || github.event.release.tag_name }} run: | set -euo pipefail if [[ ! "$TAG" =~ ^(v[0-9]+)\.[0-9]+\.[0-9]+$ ]]; then @@ -53,7 +143,12 @@ jobs: fi sha=$(gh api "repos/$REPO/commits/$latest" --jq .sha) - if gh api "repos/$REPO/git/ref/tags/$major" > /dev/null 2>&1; then + # gh prints the error body on stdout, so a missing ref must reset it. + current=$(gh api "repos/$REPO/git/ref/tags/$major" --jq .object.sha 2> /dev/null) || current="" + if [ "$current" = "$sha" ]; then + echo "$major already points at $latest ($sha)" | tee -a "$GITHUB_STEP_SUMMARY" + exit 0 + elif [ -n "$current" ]; then gh api -X PATCH "repos/$REPO/git/refs/tags/$major" \ -f sha="$sha" -F force=true > /dev/null else diff --git a/README.md b/README.md index 4f5eefb..ebdeb53 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ More information on https://mergify.com Pin the action to a released major (see the [releases](https://github.com/Mergifyio/setup-cli/releases)). The `v1` tag moves to each new `v1.x.y` release; pin a full version such as -`v1.3.0` to stay on one release: +`v1.3.0` to stay on one release. A change of the default `mergify-cli` version +releases itself as the next `v1.x.0` once it lands on `main`: ```yaml - uses: Mergifyio/setup-cli@v1