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
87 changes: 50 additions & 37 deletions .github/workflows/promote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ jobs:
# Defensive guard (the trigger already implies it): only a real, published release.
if: github.event.release.prerelease == false && github.event.release.draft == false
permissions:
contents: write # commit the regenerated feed back to main (best-effort)
contents: write # push the regenerated feed to its own branch (never to the line itself)
pull-requests: write # open the feed PR and label it (6j6v.exga)
id-token: write # OIDC: assume nxs-prod-release (copy + manifest write)
actions: write # dispatch publish-content.yml after the feed commit (85y.19; re-homed by 6j6v.0a6p)
env:
# Tag/release data flows ONLY through env, never `${{ }}`-interpolated into a shell
# (security hygiene, spec §2). version-check.yml enforces tag == v<workspace-version>.
Expand Down Expand Up @@ -195,50 +195,63 @@ jobs:
done
echo "All 4 tarballs + .sha256 + .minisig present in s3://.../download/stable/${VERSION}/"

# Best-effort: commit the regenerated feed back to main. `continue-on-error` is scoped to
# THIS step only — a branch-protection reject of the feed push must NOT fail an already-
# done promotion. The push outcome drives `dispatch`: only a landed feed commit should
# trigger the site refresh.
- name: Commit + push regenerated feed (best-effort)
id: feed
continue-on-error: true
# The regenerated feed reaches its line through a PULL REQUEST, never a push (6j6v.exga). main
# carries ruleset 23939298 ("every change through a pull request", no bypass — owner rule), so
# the old direct push was rejected, and it degraded to a ::warning:: while the promotion
# itself stayed green: main's feed silently lacked the stable entry and the public changelog
# never refreshed. Now the feed commit goes to its own branch and a PR carries it.
#
# WHY THE OWNER RE-OPENS THE PR. A PR opened with GITHUB_TOKEN starts no workflows (GitHub's
# loop prevention), so its six required checks would never report and it could never merge.
# Closing and re-opening it as a human fires `pull_request: reopened`, and every required
# check runs. The owner chose that over a GitHub App token (6j6v.exga): a stable promotion is
# already a deliberate human gesture, and it costs no new secret. The job summary spells it
# out.
#
# NOT best-effort any more: a feed that cannot reach a PR fails the job. The promotion above
# has already landed by then, so red here means "stable is live, main's feed is not", which
# is exactly what somebody must see.
#
# publish-content is NOT dispatched from here any more. It must run on the MERGED feed, and
# the owner's merge is a human push to the line that touches release-notes.json, which fires
# publish-content.yml's own push trigger (main only; a backport's entry reaches main through
# its forward-port PR, as before).
- name: Open the feed PR
env:
VERSION: ${{ steps.v.outputs.version }}
LINE_REF: ${{ steps.line.outputs.ref }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if git diff --quiet -- release-notes.json; then
echo "feed unchanged — nothing to commit"
echo "dispatch=false" >> "$GITHUB_OUTPUT"; exit 0
echo "feed unchanged — nothing to commit"; exit 0
fi
branch="release-notes/promote-v${VERSION}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git switch -c "$branch"
git add release-notes.json
git commit -m "chore(release-notes): promote ${VERSION} to stable"
# Push back to the LINE it came from (main for a normal release; release/x.Y for a
# backport). A backport's stable entry reaches main + the public site via the
# forward-port step in the backport runbook (docs/specs/maintenance-and-backports.md).
if git push origin "HEAD:${LINE_REF}"; then
echo "dispatch=true" >> "$GITHUB_OUTPUT"
else
echo "::warning::feed commit rejected (branch protection?) — promotion already succeeded, skipping content refresh"
echo "dispatch=false" >> "$GITHUB_OUTPUT"
fi
# The branch is this job's own: a re-run regenerates the same feed and may overwrite it.
git push --force origin "HEAD:refs/heads/${branch}"

# Refresh the public changelog from the just-pushed feed (spec §8, 85y.19; re-homed to the
# content provider by 6j6v.0a6p). The dispatch is EXPLICIT because a GITHUB_TOKEN push never
# triggers `on: push` workflows (Actions loop prevention) — publish-content.yml's push trigger
# only catches human commits. NOT best-effort: unlike the feed push, a failed dispatch means
# the public changelog silently never refreshes, so let it fail the job and surface (the
# promotion itself already landed).
#
# ONLY for the main line. The public landing builds from main's feed; a backport commits its
# feed entry to release/x.Y, which is NOT yet on main, so dispatching here would refresh the
# content to the SAME (backport-less) state — a misleading, wasted run. A backport's entry
# reaches main + the landing via the forward-port PR (backport runbook §4.6), whose merge fires
# publish-content.yml on main naturally.
- name: Publish content (refresh public changelog)
if: steps.feed.outputs.dispatch == 'true' && steps.line.outputs.ref == 'main'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run publish-content.yml --ref main -f env=prod
pr="$(gh pr list --head "$branch" --base "$LINE_REF" --state open --json url --jq '.[0].url')"
if [ -z "$pr" ]; then
pr="$(gh pr create --base "$LINE_REF" --head "$branch" --label skip-changelog \
--title "chore(release-notes): promote ${VERSION} to stable" \
--body "The stable entry for ${VERSION}, regenerated by promote.yml after the promotion itself succeeded. The checks do not start on their own for a PR opened by a workflow: close and re-open this PR, wait for the required checks, then merge. The merge refreshes the public changelog through publish-content.yml.")"
fi
number="${pr##*/}"
echo "feed PR: $pr"
{
echo "## Stable ${VERSION} is live. Its feed entry waits in a PR"
echo
echo "Merge $pr to put the stable entry into \`${LINE_REF}\` and refresh the public changelog."
echo "Its checks do not start by themselves (the PR was opened by a workflow). Start them by closing and re-opening it:"
echo
echo '```'
echo "gh pr close ${number} -R ${GITHUB_REPOSITORY} && gh pr reopen ${number} -R ${GITHUB_REPOSITORY}"
echo "gh pr checks ${number} -R ${GITHUB_REPOSITORY} --watch"
echo "gh pr merge ${number} -R ${GITHUB_REPOSITORY} --squash --delete-branch"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
5 changes: 4 additions & 1 deletion NEXUS_MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -1972,7 +1972,10 @@ for the commit, red release, never a fallback test run. Merging to main produces

PROMOTION TO STABLE IS A MANUAL OWNER STEP: open the GitHub release, deselect "Set as pre-release",
set "Set as latest". That fires `promote.yml` (`release: types: [released]`), which SERVER-SIDE
COPIES the same bytes and signatures to stable — nothing is rebuilt or re-signed.
COPIES the same bytes and signatures to stable — nothing is rebuilt or re-signed. Since 6j6v.exga
it then opens a FEED PR (branch release-notes/promote-v<v>): main's ruleset allows no direct push.
A workflow-opened PR starts no checks, so CLOSE AND RE-OPEN it, wait for the checks, MERGE it. Only
that merge puts the stable entry on main and refreshes the public changelog (publish-content.yml).

AND IF PROMOTE EVER COMPLAINS "no beta entry for <v> to promote": do NOT flip the GitHub release
first. Add the entry, re-run `version set`, then fire the promotion. That failure is now structurally
Expand Down
5 changes: 3 additions & 2 deletions docs/specs/maintenance-and-backports.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ For each active line `x.Y` (current first, then the previous ones):
aggregates within the line (`prev-stable` returns the line predecessor `vx.Y.z`, not a
higher-line stable — proven by the `backport_promotion_is_line_scoped_across_parallel_lines`
test), and copies the same bytes beta→stable (build-once-promote, per line).
6. **Forward-port the line's feed entry to `main`.** `promote.yml` commits the regenerated feed
back to the **line** branch. To surface the backport in the canonical feed + the public
6. **Forward-port the line's feed entry to `main`.** `promote.yml` opens a PR carrying the
regenerated feed against the **line** branch (release-management.md §5.5 step 5: close and
re-open it so its checks run, then merge). To surface the backport in the canonical feed + the public
changelog (which build from `main`'s `release-notes.json`), open a small PR cherry-picking that
`release-notes.json` change onto `main`. Merging it triggers `site.yml` and refreshes the
public site. (The canonical feed on `main` is the **union** of all lines' entries; backport
Expand Down
18 changes: 13 additions & 5 deletions docs/specs/release-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,9 +526,16 @@ Flow (identical to the reference, only n platforms instead of 2-into-1):
condenses multiple beta refinements of one feature into a single stable line, and phrases for a
stable audience before publishing.
4. Rewrite `manifests/stable.json`, promote the feed + publish to S3.
5. Commit the regenerated feed **best-effort** back to `main` (no `[skip ci]` — the
push triggers exactly the site deploy that updates the changelog; loop-free, because the
release runs only on tags). A branch-protection reject must not fail the promotion.
5. Carry the regenerated feed to its line **through a pull request** (6j6v.exga): the commit goes
to its own branch `release-notes/promote-v<v>`, and `promote.yml` opens a PR against the line
(`main`, or `release/x.Y` for a backport), labelled `skip-changelog`. `main` carries a ruleset
with no bypass ("every change through a pull request"), so the old direct push was rejected and,
being best-effort, left main's feed silently without the stable entry. Failing to open the PR now
fails the job; the promotion itself has landed by then. **Owner step:** a PR opened by a workflow
starts no workflows, so its required checks run only after the owner closes and re-opens it; then
the owner merges. That merge is a human push touching `release-notes.json`, and it fires
`publish-content.yml`, which refreshes the public changelog. The job summary prints the three
commands.

### 5.6 Container image for `nxf-relay` — **specified, deliberately not built yet**

Expand Down Expand Up @@ -720,7 +727,7 @@ prerendered, EN + `/de`, no router — the reasons documented there hold unchang
| `changelog-check.yml` | PR (+`labeled`/`unlabeled`) | fragment requirement, waived by itself when nothing the PR touches ships, opt-out `skip-changelog`; plus the explicit `facade:` verdict when the diff touches a consumed surface (§4.2, `6j6v.gmjd`) |
| `facade-semver.yml` | PR, main, `release/<major>.<minor>`, tags `v*` | `cargo xtask facade semver-check` — public-API SemVer gate over all three consumed surfaces, `nexus-flow-facade`/`nexus-chat`/`nexus-memory` (patch never breaks); fail-closed on a patch bump, report-only on a feature PR / minor; runs per line (§4.3) |
| `release.yml` | tag `v*` (prod) / dispatch (staging) | **`ci-green` gate** → matrix build, sign, GitHub pre-release, S3 publish, beta manifest, GHCR image. The gate DEMANDS a green `ci.yml` run for the tagged commit (`push`/`workflow_dispatch` only — a `pull_request` run skips the release profile) and never tests the tree itself; no green run ⇒ red release. It replaced a `test` job that re-ran `cargo test --all` and `cargo test --all --release` on a commit CI had just tested — 21m28 of a 31m52 release on v0.51.0 (6j6v.31rs). Script: `.github/scripts/require-green-ci.sh`, hermetically tested by `tests/require-green-ci.test.sh`. |
| `promote.yml` | `release: released` | beta→stable: S3 copy, stable manifest, notes aggregate, feed commit, image retag; **line-aware** — promotes from `main` or, for a backport, the release's `release/x.Y` line |
| `promote.yml` | `release: released` | beta→stable: S3 copy, stable manifest, notes aggregate, feed PR (owner re-opens + merges), image retag; **line-aware** — promotes from `main` or, for a backport, the release's `release/x.Y` line |

**SIX ROWS USED TO STAND HERE AND THE FILES ARE GONE.** `staging-gate.yml`, `infra-staging.yml`,
`infra-staging-release.yml`, `infra-prod.yml` and `infra-ci.yml` were deleted with the infra
Expand Down Expand Up @@ -762,7 +769,8 @@ This table was never exhaustive and is less so now; `.github/workflows/` is the
3. **Dogfood on beta**: `nxs self-update --channel beta` (we use nexus-flow ourselves —
the engine eats its own tail, vision §Phases).
4. **Promote**: open the GitHub release, deselect "Set as pre-release", set "Set as latest"
→ `promote.yml` copies the same bytes to stable.
→ `promote.yml` copies the same bytes to stable and opens the feed PR. **Then** close and re-open
that PR (its checks do not start otherwise), wait for them, and merge it (§5.5 step 5).
5. **Broken?** `enabled:false` into the affected channel manifest (kill switch, immediate), fix
forward as `<v+1>`; stable simply skips the broken version.

Expand Down
Loading