release-kit 0.2.2

A canonical release workflow: a technology-agnostic method, per-technology bindings, and the rk CLI that lands and serves them.
Documentation
# The whole release pipeline: there is no registry, so publishing is tagging
# and the tarball attached to the release page is the distribution.
#
# Both jobs run on every push to the trunk, and to a release/* line when a
# project keeps older lines. The release-request job drives git-cliff to
# maintain the one release pull request — VERSION plus the changelog — against
# the pushed branch, and merging that request is the release: the bump push is
# what makes tag-and-attach tag the commit, build the tarball with make dist,
# and attach it. On a release/* line the same pair backports a patch and tags
# it there.
name: release

permissions: {}

on:
  push:
    branches: [master, 'release/**']

jobs:
  # The release request. git-cliff maintains no pull request on its own, so
  # this job computes the bump, rewrites VERSION and the changelog on a
  # request branch, and opens or refreshes the pull request. Force-pushing the
  # branch is the refresh: the request always reflects the pushed branch's tip.
  release-request:
    if: github.repository_owner == 'OWNER'
    runs-on: ubuntu-latest
    permissions:
      contents: read
    concurrency:
      group: release-request-${{ github.ref }}
      cancel-in-progress: false
    steps:
      - uses: actions/create-github-app-token@v3
        id: app-token
        with:
          app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
          private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }}
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ steps.app-token.outputs.token }}
      - name: install git-cliff
        run: pipx install git-cliff==2.13.1
      - name: maintain the release request
        env:
          GH_TOKEN: ${{ steps.app-token.outputs.token }}
          APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
          REF: ${{ github.ref_name }}
        run: |
          set -eu
          current="$(cat VERSION)"
          next="$(git cliff --bumped-version | sed 's/^v//')"
          if [ "$next" = "$current" ]; then
            echo "no release-worthy commits since v$current; no request to maintain."
            exit 0
          fi
          if [ -n "$(git ls-remote --tags origin "refs/tags/v$next")" ]; then
            echo "v$next is already tagged; no request to maintain."
            exit 0
          fi
          branch="chore/release-v$next"
          git switch -c "$branch"
          printf '%s\n' "$next" > VERSION
          git cliff --bump -o CHANGELOG.md
          uid="$(gh api "/users/${APP_SLUG}[bot]" -q .id)"
          git config user.name "${APP_SLUG}[bot]"
          git config user.email "${uid}+${APP_SLUG}[bot]@users.noreply.github.com"
          git add VERSION CHANGELOG.md
          git commit -m "chore(release): v$next"
          git push -f origin "$branch"
          open="$(gh pr list --base "$REF" --head "$branch" --state open --json number -q 'length')"
          if [ "$open" -eq 0 ]; then
            gh pr create --base "$REF" --head "$branch" \
              --title "chore(release): v$next" \
              --body "Bumps VERSION to $next and rewrites the changelog. Merging this request is the release: the bump push tags v$next and attaches the tarball."
          fi

  # The release itself. Only the push that lands the bot's own bump releases —
  # the VERSION comparison against the parent commit is the guard — so an
  # ordinary work merge tags nothing. Tag the bump commit, build the tarball
  # as a pure function of the tree, and attach it with its checksum.
  tag-and-attach:
    if: github.repository_owner == 'OWNER'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      # Minting the build-provenance attestation: the OIDC token identifies
      # this run to Sigstore, and the other two write the attestation back.
      id-token: write
      attestations: write
      artifact-metadata: write
    steps:
      - uses: actions/create-github-app-token@v3
        id: app-token
        with:
          app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
          private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }}
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: install git-cliff
        run: pipx install git-cliff==2.13.1
      - name: tag and attach
        id: release
        env:
          GH_TOKEN: ${{ steps.app-token.outputs.token }}
          GH_REPO: ${{ github.repository }}
          SHA: ${{ github.sha }}
        run: |
          set -eu
          version="$(cat VERSION)"
          previous="$(git show "$SHA^:VERSION" 2>/dev/null || echo "")"
          if [ "$previous" = "$version" ]; then
            echo "this commit does not bump the version; nothing to release."
            exit 0
          fi
          if [ -z "$(git ls-remote --tags origin "refs/tags/v$version")" ]; then
            gh api -X POST "repos/$GH_REPO/git/refs" \
              -f "ref=refs/tags/v$version" -f "sha=$SHA" >/dev/null
            echo "tagged v$version at $SHA."
          fi
          git fetch origin "refs/tags/v$version:refs/tags/v$version"
          make dist
          echo "built=true" >> "$GITHUB_OUTPUT"
          git cliff --latest -o notes.md
          if gh release view "v$version" >/dev/null 2>&1; then
            echo "the release page already exists; leaving it as it is."
            exit 0
          fi
          gh release create "v$version" dist/*.tar.gz dist/*.sha256 \
            --title "v$version" --notes-file notes.md

      # Provenance. The run that built the tarball signs it, so a consumer can
      # check which workflow and commit produced it instead of trusting the
      # release page the checksum also sits on. Attesting by hand is not
      # possible: only the building run holds the identity that signs.
      - uses: actions/attest@v4
        if: steps.release.outputs.built == 'true'
        with:
          subject-path: dist/*.tar.gz