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/policy.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 }}