Skip to content

release: 0.3.3

release: 0.3.3 #10

name: Release PR Version
on:
# `edited` is not one of the default pull_request types, but it is the point of this
# workflow: retitling the release PR is how a maintainer picks an exact version. The
# other types keep the consistency check attached to the PR as it evolves.
pull_request:
types: [opened, reopened, edited, synchronize]
# Read-only by default; only the job that pushes the Release-As commit widens this.
permissions:
contents: read
jobs:
version-consistency:
# Also the check name the release PR's footer tells maintainers to wait for.
name: Release PR version
# A release-shaped title against the release branch is the whole test here, with no clause
# on who opened the pull request or what branch it is rendered onto. This job only reads,
# and a pull request claiming to be a release is worth checking against the committed
# version whoever opened it. Every other pull request skips this job, which GitHub reports
# as neutral.
#
# Loose on purpose: the prefix, not the full semver pattern the script matches. A version
# typo'd into the title still reaches the script and fails there, rather than falling out
# of the guard and leaving no check at all — a missing check is not a failing one, and this
# is the check the release PR's footer tells maintainers to wait for before merging.
#
# It is also what keeps the workflow from failing silently if the platform ever opens
# release PRs from a different account: the bridge below stops firing, but this check still
# runs, the retitled version and the committed one disagree, and it turns red. The merge is
# blocked loudly instead of releasing the old version.
if: >-
${{ github.event.pull_request.base.ref == 'main'
&& startsWith(github.event.pull_request.title, 'release: ') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
# The version committed on the PR head is what merging would actually release.
ref: ${{ github.event.pull_request.head.sha }}
# Fails while the title version and the committed version disagree — the window between
# a maintainer's retitle and the platform re-rendering the PR from it. Without this a
# merge in that window would tag a release whose own files self-report the old version,
# and a title release-please cannot parse would silently cut no release at all.
- name: Compare the title version with the committed version
env:
# The title is human input, so it is bound through the environment (never
# interpolated into the script) and matched against a strict semver pattern
# before it is read.
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
pattern='^release: ((0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?)$'
if [[ ! "$PR_TITLE" =~ $pattern ]]; then
echo "::error::Release PR title must be \"release: X.Y.Z\" with a full semver version, got: $PR_TITLE"
exit 1
fi
title_version="${BASH_REMATCH[1]}"
manifest_version="$(jq -r '.["."]' .release-please-manifest.json)"
if [[ "$title_version" != "$manifest_version" ]]; then
echo "::error::Release PR title says $title_version but the pull request is versioned $manifest_version. Wait for the release PR to be re-rendered at $title_version before merging."
exit 1
fi
echo "Release PR title and committed version agree on $title_version."
apply-title-version:
name: Apply edited release version
# A human retitle of the open release PR is bridged into the canonical explicit-version
# mechanism (a Release-As commit on scalar-next, which the platform re-renders the
# release PR from). `changes.title` is only set when the title itself changed, and bot
# senders are ignored so the platform's own retitles cannot bounce back into another
# commit.
#
# Both signals are required here, unlike the read-only check above, and each covers what the
# other cannot. The author, because this job pushes with `contents: write` and a title is
# free text — anyone able to open a pull request against main could otherwise
# name a version and have it committed to scalar-next. `user.login` is set by GitHub when
# the pull request is opened and cannot be forged by whoever edits the title afterwards.
# The title, because those accounts open pull requests other than release PRs, and retitling
# one of those must not push a release.
#
# One login per platform deployment, since a repo generated by staging carries staging's
# app. Parenthesised because the group is ANDed with the title clause below: without the
# parens that `&&` would bind to the last login alone, letting the other accounts push a
# release off any title. Listed rather than matched on the shared `scalar-docs` stem — a
# prefix test would also admit any future `scalar-docs-*[bot]`, including someone else's.
if: >-
${{ github.event.action == 'edited'
&& github.event.changes.title != null
&& github.event.sender.type != 'Bot'
&& github.event.pull_request.state == 'open'
&& github.event.pull_request.base.ref == 'main'
&& (github.event.pull_request.user.login == 'scalar-docs[bot]'
|| github.event.pull_request.user.login == 'scalar-docs-staging[bot]'
|| github.event.pull_request.user.login == 'scalar-docs-development[bot]')
&& startsWith(github.event.pull_request.title, 'release: ') }}
# One bridge run at a time per pull request, newest retitle wins. Two retitles in quick
# succession (a version typo corrected seconds later) would otherwise start two runs that
# both read the version committed on the PR head, both get past the no-op guard, and both
# push — and since each fetches scalar-next immediately before committing, the second
# push fast-forwards instead of failing. release-please honours the newest Release-As
# footer, so the release would land on whichever run happened to push last, not on the
# title the maintainer actually left behind.
concurrency:
group: release-title-edit-${{ github.event.pull_request.number }}
cancel-in-progress: true
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
# The PR head, so the version the pull request currently carries can be read before
# deciding whether anything needs to change.
ref: ${{ github.event.pull_request.head.sha }}
- name: Push a Release-As commit for the edited version
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
set -euo pipefail
pattern='^release: ((0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?)$'
if [[ ! "$PR_TITLE" =~ $pattern ]]; then
echo "::error::Release PR title must be \"release: X.Y.Z\" with a full semver version, got: $PR_TITLE"
exit 1
fi
# Only ever a validated semver from here on, so it is safe in a commit message.
version="${BASH_REMATCH[1]}"
manifest_version="$(jq -r '.["."]' .release-please-manifest.json)"
# The re-rendered PR is retitled to the version it now carries; stopping here keeps
# that from producing an endless chain of Release-As commits.
if [[ "$version" == "$manifest_version" ]]; then
echo "This release PR already carries $version; nothing to do."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin scalar-next
git checkout -B scalar-next origin/scalar-next
git commit --allow-empty -m "chore: release $version" -m "Release-As: $version"
# Plain (non-force) push: losing a race against a regeneration push fails loudly
# rather than discarding it, and the retitle can simply be repeated.
git push origin scalar-next
echo "Pushed Release-As: $version to scalar-next. The release PR will be re-rendered at that version."