Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ on:
# immutable-package release track keyed to module.package.codefly.yaml.
tags: ["v*", "module-package/v*"]
pull_request:
branches: [main]
# release/** carries a patch line cut from a deploy-counter tag: a pull
# request into one gets the same checks as one into main, or it would merge
# with none.
branches: [main, "release/**"]
# The merge queue builds each entry as `main + the PR` and reports the
# required checks from THAT ref, which is the guarantee "require branches to
# be up to date" was buying by making every author rebase by hand. Every
Expand Down Expand Up @@ -236,13 +239,18 @@ jobs:
# Renders to "false" when the label is absent; the gate requires an
# affirmative token, so only the label opens the escape hatch.
AUTHZ_ALLOW_BROADENING: ${{ contains(github.event.pull_request.labels.*.name, 'authz-broadening-approved') }}
# The branch the pull request merges into: main, or a release/** patch
# line. Diffing a patch line's pull request against main would report
# everything main gained since the line was cut, or hide a broadening
# main already carries. Outside a pull request it is main, as before.
BASE_REF: ${{ github.event.pull_request.base.ref || 'main' }}
run: |
git fetch --no-tags origin refs/heads/main:refs/remotes/origin/main
git fetch --no-tags origin "refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"
base="$(mktemp)"
if git show origin/main:module/services/accounts/generated/authz-methods.json > "${base}" 2>/dev/null; then
if git show "origin/${BASE_REF}:module/services/accounts/generated/authz-methods.json" > "${base}" 2>/dev/null; then
node module/tools/authz-coverage-gate.mjs no-broadening --base "${base}"
else
echo "no authorization catalog on main yet; nothing to diff"
echo "no authorization catalog on ${BASE_REF} yet; nothing to diff"
fi
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
Expand Down Expand Up @@ -517,12 +525,26 @@ jobs:
# contract once a concurrent change lands is caught where it has to
# be: the merge queue builds main + the PR afresh, so there HEAD^1 IS
# current main.
#
# A tag whose commit is not on main was cut from a release/** patch
# line. Its baseline is the line's base, where it forked from main
# (the release it patches), not main as it stands: main may have
# gained messages since, which the line lacks and would report as
# deletions. Every tag on main, and every other event, keeps main.
if git rev-parse --verify -q HEAD^2 >/dev/null; then
baseline_rev="HEAD^1"
else
git fetch --no-tags origin \
refs/heads/main:refs/remotes/origin/main
baseline_rev="origin/main"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
if git merge-base --is-ancestor HEAD origin/main; then on_main=0; else on_main=$?; fi
case "${on_main}" in
0) ;;
1) baseline_rev="$(git merge-base HEAD origin/main)" ;;
*) echo "::error::cannot tell whether ${GITHUB_REF_NAME} is on main (git exit ${on_main})"; exit 1 ;;
esac
fi
fi
echo "proto baseline: ${baseline_rev} ($(git rev-parse --short "${baseline_rev}"))"
git archive "${baseline_rev}" module/services/accounts/proto | tar -x -C "${baseline}"
Expand Down Expand Up @@ -1398,10 +1420,40 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
# A tag push names a commit, not a branch, so the branch a tag was cut
# from is not in the event. What decides "current" is whether main
# contains the tagged commit: a tag cut from a release/** patch line is
# not on main, and announcing it would make an interface current that
# lacks whatever main released since. Such a tag still passes every gate
# above; only the announcement waits for main's next tag.
- name: Check the tagged commit is on main
id: on-main
run: |
git fetch --no-tags origin main
# 1 is "not an ancestor"; anything else is git failing to answer,
# which must fail the step rather than read as "not on main".
if git merge-base --is-ancestor "${GITHUB_SHA}" FETCH_HEAD; then on_main=0; else on_main=$?; fi
case "${on_main}" in
0)
echo "announce=true" >> "${GITHUB_OUTPUT}"
;;
1)
echo "::notice::${GITHUB_REF_NAME} is not on main; the release is not announced."
echo "announce=false" >> "${GITHUB_OUTPUT}"
;;
*)
echo "::error::cannot tell whether ${GITHUB_REF_NAME} is on main (git exit ${on_main})"
exit 1
;;
esac
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
if: steps.on-main.outputs.announce == 'true'
with:
node-version: 24
- name: Dispatch surface-bump
if: steps.on-main.outputs.announce == 'true'
env:
HANDBOOK_REPOSITORY: ${{ vars.HANDBOOK_REPOSITORY }}
GH_TOKEN: ${{ secrets.HANDBOOK_DISPATCH_TOKEN }}
Expand Down
21 changes: 21 additions & 0 deletions RELEASE_GATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ With both present, a dispatch the handbook rejects still fails the job.
including that an unconfigured run dispatches nothing at all — and runs in
`release-contract`.

Only a tag on `main` is announced. A tag push names a commit, not a branch, so
`handbook-surface-bump` first asks whether `main` contains the tagged commit
(`git merge-base --is-ancestor`). A tag cut from a `release/**` patch line is
not on `main`: it passes every gate and publishes every artifact, but it is not
announced as current, because it lacks whatever `main` released after the line
was cut. `main`'s next tag carries the announcement. Exit 1 of that check is
"not on main"; any other failure fails the job rather than skipping the
announcement silently.

### The contract test

`release-contract` runs `scripts/ci/release-gates.mjs check`, which parses every
Expand Down Expand Up @@ -795,6 +804,18 @@ To cut a deploy-counter tag:
2. Commit it as `release: v0.0.N`.
3. Tag that commit with an annotated `v0.0.N` tag and push the tag.

**Patch lines.** A fix for a deployment pinned to an older deploy-counter tag
goes on a `release/**` branch cut from that tag, through a pull request into
it; pull requests into `release/**` get the same checks as pull requests into
`main`. A tag on the line passes every release gate and publishes its artifacts
like a tag on `main`, with two differences. It is not announced as current
(only a tag on `main` is; see `handbook-surface-bump` above). And the gates that
compare against a baseline compare against the line's base, not against `main`
as it stands: the authorization no-broadening check diffs a pull request
against the branch it merges into, and the protobuf breaking-change check on a
tag that is not on `main` compares against its merge base with `main`, the
release the line patches.

To cut an immutable module-package release:

1. Bump `version:` in `module/module.package.codefly.yaml`.
Expand Down
Loading
Loading