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 }}