From 34977d24575c636b3d028b821f0128c707256f5b Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Tue, 28 Jul 2026 17:01:44 -0400 Subject: [PATCH] feat(release): cut a tag and release when a reusable workflow changes Callers pin `uses:` to a commit SHA of this repo. Dependabot's github-actions updater finds a newer SHA for a pinned ref by reading the target repo's tags and releases; this repo has 0 tags and 0 releases, so there is nothing for it to resolve and it reports no update. The fleet's pins have not moved as a result. Adds a caller for the release reusable, triggered on push to main and filtered to paths under .github/workflows/ excluding the three files no caller consumes (ci.yaml, labeler.yaml, and this file). Version is a patch bump from the highest existing vMAJOR.MINOR.PATCH tag, 1.0.0 when none exist. workflow_dispatch takes an explicit version for minor and major bumps. Runs are serialised with cancel-in-progress false: two merges landing together would otherwise read the same highest tag, compute the same next version, and the second would hit the reusable's existing-tag guard and no-op, leaving that change unreleased. --- .github/workflows/release-on-merge.yaml | 109 ++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 .github/workflows/release-on-merge.yaml diff --git a/.github/workflows/release-on-merge.yaml b/.github/workflows/release-on-merge.yaml new file mode 100644 index 0000000..7460afe --- /dev/null +++ b/.github/workflows/release-on-merge.yaml @@ -0,0 +1,109 @@ +name: Release on reusable change + +# Cuts a tag and GitHub Release for THIS repo when a merge to main changes a +# reusable workflow. +# +# Why this exists: every caller in the org pins its `uses:` reference to a +# commit SHA of this repo. Dependabot's github-actions updater resolves a newer +# SHA for a pinned reference by looking at that repo's tags and releases. This +# repo has had neither, so a pinned reference has nothing to be bumped to and +# Dependabot correctly reports no update. Tagging each change to a reusable +# gives it something to diff against. +# +# The trigger is deliberately narrow. Only workflows carrying `on: +# workflow_call` are consumed by other repos, so only those warrant a release. +# Changes to this repo's own CI, its labeler caller, this file, the starter +# templates under workflow-templates/ and the docs change nothing for a caller +# pinned to a SHA, so they do not cut one. The exclusions below are the +# non-reusable workflows; add to this list when adding another workflow that is +# not `workflow_call`, or it will cut a release it does not need to. +# +# Version scheme: patch bump from the highest existing vMAJOR.MINOR.PATCH tag, +# starting at 1.0.0 when there are none. Use the manual dispatch with an +# explicit version for a minor or major bump, which is the case whenever a +# reusable gains a required input, renames one, or otherwise breaks a caller. + +on: + push: + branches: [main] + paths: + - ".github/workflows/**" + - "!.github/workflows/ci.yaml" + - "!.github/workflows/labeler.yaml" + - "!.github/workflows/release-on-merge.yaml" + workflow_dispatch: + inputs: + version: + description: 'Explicit version, e.g. "1.1.0". Leave empty to patch-bump the highest existing tag.' + type: string + required: false + default: "" + +# Runs are serialised, never cancelled. Two merges landing close together would +# otherwise both read the same highest tag, compute the same next version, and +# the second would find the tag already created and no-op — leaving that change +# unreleased. Queuing makes the second run read the tag the first one pushed. +concurrency: + group: release-on-merge + cancel-in-progress: false + +permissions: + contents: read + +jobs: + version: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + version: ${{ steps.next.outputs.version }} + steps: + - uses: actions/checkout@v7 + with: + # Tags are the input to the version calculation, so they must be + # fetched; a shallow checkout without them would restart at 1.0.0. + fetch-depth: 0 + fetch-tags: true + + - name: Compute the next version + id: next + env: + OVERRIDE: ${{ inputs.version }} + run: | + set -euo pipefail + + if [ -n "${OVERRIDE}" ]; then + echo "version=${OVERRIDE#v}" >> "${GITHUB_OUTPUT}" + echo "Using the explicitly supplied version ${OVERRIDE}." + exit 0 + fi + + # Only plain vMAJOR.MINOR.PATCH tags take part. -v:refname sorts by + # version rather than lexically, so v1.0.10 ranks above v1.0.9. + latest="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n 1)" + + if [ -z "${latest}" ]; then + next="1.0.0" + echo "No existing version tag. Starting at ${next}." + else + current="${latest#v}" + major="${current%%.*}" + rest="${current#*.}" + minor="${rest%%.*}" + patch="${rest#*.}" + + next="${major}.${minor}.$((patch + 1))" + echo "Highest existing tag is ${latest}. Next version is ${next}." + fi + + echo "version=${next}" >> "${GITHUB_OUTPUT}" + + release: + needs: version + # Referenced by local path, not by SHA. A repo calling its own reusable + # cannot pin to a commit of itself without the pin lagging the file it + # points at by one merge every time either changes. + uses: ./.github/workflows/release.yaml + permissions: + contents: write + with: + version: ${{ needs.version.outputs.version }}