name: release
permissions: {}
on:
push:
branches: [RK_TRUNK_BRANCH, 'RK_LINE_PREFIX**']
jobs:
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@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 id: app-token
with:
client-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 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 }}
RELEASE_STYLE: RK_STYLE
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
# RK_GATE_ARM_BEGIN
# The trunk style's standing arm, re-applied on every refresh; the
# app token is what keeps the eventual merge able to start this
# workflow's release half, and the lines style leaves the merge a
# human's.
if [ "$RELEASE_STYLE" != "trunk" ]; then
echo "the lines style merges each release by hand; not arming."
exit 0
fi
if [ "$REF" != "RK_TRUNK_BRANCH" ]; then
echo "a line's request is never armed; its candidate is what a human validated."
exit 0
fi
gh pr merge "$branch" --auto RK_MERGE_FLAG --delete-branch
echo "armed the release request for v$next."
# RK_GATE_ARM_END
tag-and-attach:
if: github.repository_owner == 'OWNER' runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
attestations: write
artifact-metadata: write
steps:
- uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 id: app-token
with:
client-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }}
permission-contents: write
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with:
fetch-depth: 0
persist-credentials: false
- name: install git-cliff
run: pipx install git-cliff==2.13.1
- name: install the build packages
run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends RK_BUILD_PACKAGES
- name: build
id: build
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
# The commit that introduces VERSION is the landing, not a release.
if [ -z "$previous" ]; then
echo "this commit introduces VERSION; nothing to release."
exit 0
fi
# The baseline is the unreleased state and never a release.
if [ "$version" = "0.0.0" ]; then
echo "0.0.0 is the unreleased baseline; nothing to release."
exit 0
fi
# A version the forge already published at another commit is an
# un-bump back to it, not a release. The forge decides, not a
# version order: a draft, or a release at this very commit,
# continues so attach can finish it.
if released="$(gh release view "v$version" --json isDraft -q .isDraft 2>/dev/null)" && [ "$released" = "false" ]; then
at="$(gh api "repos/$GH_REPO/commits/v$version" -q .sha 2>/dev/null || true)"
if [ -n "$at" ] && [ "$at" != "$SHA" ]; then
echo "v$version is already released at $at; nothing to release."
exit 0
fi
fi
RK_BUILD_COMMAND
# The version is named, because no tag exists yet for git-cliff to read.
git cliff --unreleased --tag "v$version" -o notes.md
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "built=true" >> "$GITHUB_OUTPUT"
- uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 if: steps.build.outputs.built == 'true'
with:
subject-path: dist/*.tar.gz
- name: attach
if: steps.build.outputs.built == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GH_REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
VERSION: ${{ steps.build.outputs.version }}
run: |
set -eu
# Three states, never two: a tag that exists, a tag the forge
# answers 404 for, and a read that failed, which refuses, because
# a tag nobody could read may name another commit.
err="$(mktemp)"
tagged=""
if gh api "repos/$GH_REPO/git/ref/tags/v$VERSION" >/dev/null 2>"$err"; then
if ! tagged="$(gh api "repos/$GH_REPO/commits/v$VERSION" -q .sha)" || [ -z "$tagged" ]; then
echo "the commit v$VERSION tags could not be read; refusing to publish." >&2
exit 1
fi
elif ! grep -q "HTTP 404" "$err"; then
echo "the tag v$VERSION could not be read: $(cat "$err"); refusing to publish." >&2
exit 1
fi
if [ -n "$tagged" ] && [ "$tagged" != "$SHA" ]; then
echo "v$VERSION already tags $tagged, not this run's $SHA; recover through the section The tag exists and the release does not (rk method recovery)." >&2
exit 1
fi
if draft="$(gh release view "v$VERSION" --json isDraft -q .isDraft 2>/dev/null)"; then
if [ "$draft" = "true" ]; then
gh release upload "v$VERSION" dist/*.tar.gz dist/*.sha256 --clobber
# A draft keeps whatever target it was made with, and its
# publication creates the tag there: point it at this run's
# commit first, which the guard above proved the tag names
# where one exists.
gh release edit "v$VERSION" --target "$SHA" --draft=false
echo "finished the draft release v$VERSION."
else
echo "the release page already exists; leaving it as it is."
fi
exit 0
fi
gh release create "v$VERSION" dist/*.tar.gz dist/*.sha256 \
--target "$SHA" --title "v$VERSION" --notes-file notes.md
echo "tagged and released v$VERSION at $SHA."