Files
Myasnikov Daniil 2dfbeea776 ci(release): dispatch changelog generation at rc time
Extract the promote-time changelog generation into a reusable workflow
(changelog-generate.yaml, workflow_call) and add a dispatchable wrapper
(changelog-rc.yaml) so a maintainer can generate the release changelog
the moment an rc goes green, while there is still time to review it.

The reusable core keeps promote-rc's observable behaviour: detached
rc-tag checkout, read-only app token for the AI step, the
validate-changelog.sh gate, non-blocking AI failure, and the same
changelog-vX.Y.Z artifact. .release-tooling is overlaid from
github.ref_name, which under workflow_call is the caller's dispatch ref
(and coincides with the reusable workflow's own ref for a local uses:),
so "current tooling against an old rc tag" is preserved. Secrets are
declared explicitly rather than inherited, so a caller that forgets one
fails at parse time.

The rc tag is parsed by a shared hack/parse-rc-tag.sh that REJECTS
whitespace outright (a stripped 'v1. 6.0-rc.2' would silently become a
different tag). promote-rc parses it ONCE in a cheap `parse` job that
both `promote` and `changelog` depend on — so they still run in parallel
with each other while consuming one validated tag, and a whitespace tag
fails the whole promotion rather than letting promote succeed while the
changelog job rejects the same input and the PR opens empty. The
reusable core mirrors the reject-whitespace policy inline because its
parse must run before any checkout.

changelog-rc.yaml verifies the rc staging branch release-X.Y.Z-rc.N
exists UP FRONT (in parse, which generate depends on), so a missing
branch fails in seconds rather than after a 30-minute AI run, then
commits the result there as docs/changelogs/vX.Y.Z.md idempotently
(compare-before-push).

promote-rc's changelog job is a thin uses: call. Source selection —
validate-then-fall-through across the rc tag tree, the rc staging branch
(the rc-time pickup), then AI generation, discarding any invalid source
with a loud warning so it never suppresses a valid lower-priority one —
lives in hack/select-changelog-source.sh, executed after the tooling
overlay. open-pr carries the picked-up file onto release-X.Y.Z and
finalize publishes it.

Contract and behaviour bats are repointed and extended: the shared
parser, the source-selection script, and promote's parse policy are all
exercised by execution on adversarial inputs; the remaining grep pins
match executable lines with exact counts so a comment or dead block
cannot satisfy them. docs/release.md lists the new workflow and the
rc-time entry point. actionlint and zizmor (default and pedantic) are
clean.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-24 18:16:16 +05:00

49 lines
1.8 KiB
Bash
Executable File

#!/bin/sh
# Usage: parse-rc-tag.sh <rc_tag>
#
# Strictly parse a release-candidate tag and print the derived release
# identifiers as `key=value` lines, ready to append to "$GITHUB_OUTPUT":
#
# rc_tag=v1.6.0-rc.2
# stable_version=1.6.0
# rc_branch=release-1.6.0-rc.2
#
# Whitespace is REJECTED, never stripped. Stripping (`tr -d '[:space:]'`) would
# silently "correct" a typo like 'v1. 6.0-rc.2' into a different, valid-looking
# tag. Worse, a caller that keys its concurrency lane on the raw dispatch input
# would then run two spellings of the same tag ('v1.6.0-rc.2' and
# 'v1.6.0-rc.2 ') in different lanes, racing to push the same staging branch.
# Rejecting outright makes the raw input a caller saw byte-identical to the tag
# acted on here: one canonical spelling per tag, or a hard error.
#
# Exit 0 = valid; the three key=value lines on stdout, nothing on stderr.
# Exit 1 = invalid; the reason on stderr, nothing on stdout.
set -eu
RC_TAG="${1?usage: parse-rc-tag.sh <rc_tag>}"
fail() {
echo "$1" >&2
exit 1
}
# Reject any whitespace (leading, trailing, or internal) and the empty string
# outright, before the pattern match — a stripped-then-matched value would let a
# whitespace-bearing spelling through as if it were the canonical tag.
case "$RC_TAG" in
'') fail "rc_tag is empty" ;;
*[[:space:]]*) fail "rc_tag '$RC_TAG' must not contain whitespace" ;;
esac
printf '%s' "$RC_TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$' \
|| fail "rc_tag '$RC_TAG' must match 'vX.Y.Z-rc.N' (e.g. v1.6.0-rc.1)"
rest="${RC_TAG#v}" # 1.6.0-rc.2
version="${rest%-rc.*}" # 1.6.0
n="${rest##*-rc.}" # 2
printf 'rc_tag=v%s-rc.%s\n' "$version" "$n"
printf 'stable_version=%s\n' "$version"
printf 'rc_branch=release-%s-rc.%s\n' "$version" "$n"