From 4e1c5f4fc79a3e69d733be295d64b199fcdbfbda Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 09:07:20 -0400 Subject: [PATCH 1/5] fix(e2e): require an interrupted proxy removal build (D-1164) --- scripts/e2e/proxy-swap-kill_test.py | 35 +++++++++++++++++++++++++++++ scripts/e2e/proxy-swap.sh | 8 +++---- 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 scripts/e2e/proxy-swap-kill_test.py diff --git a/scripts/e2e/proxy-swap-kill_test.py b/scripts/e2e/proxy-swap-kill_test.py new file mode 100644 index 000000000..c987cdd75 --- /dev/null +++ b/scripts/e2e/proxy-swap-kill_test.py @@ -0,0 +1,35 @@ +"""Exercise the kill verdict without Docker or a live build.""" +import pathlib +import subprocess +import unittest + + +class KillVerdictTest(unittest.TestCase): + def test_verdict(self): + source = pathlib.Path(__file__).with_name("proxy-swap.sh").read_text() + block = source.split(" alive=no\n", 1)[1].split(" # Recovery re-runs", 1)[0] + for alive, killed, status, gone, succeeds in [ + (0, 0, 137, 1, True), + (1, 1, 0, 1, False), + (0, 1, 0, 1, False), + (0, 0, 0, 1, False), + (0, 0, 137, 0, False), + ]: + with self.subTest(alive=alive, killed=killed, status=status, gone=gone): + prelude = f""" +set -euo pipefail +W=$(mktemp -d) +trap 'rm -rf "$W"' EXIT +mkdir "$W/evidence" +build_pid=123 +gone={gone} +kill() {{ if [[ $1 == -0 ]]; then return {alive}; else return {killed}; fi; }} +wait() {{ return {status}; }} +alive=no +""" + result = subprocess.run(["bash", "-c", prelude + block], capture_output=True) + self.assertEqual(result.returncode == 0, succeeds, result.stderr.decode()) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/e2e/proxy-swap.sh b/scripts/e2e/proxy-swap.sh index 6fb9dbbd7..bb6d9bfb2 100755 --- a/scripts/e2e/proxy-swap.sh +++ b/scripts/e2e/proxy-swap.sh @@ -385,12 +385,12 @@ if [[ $MODE != add && $MODE != renew ]]; then sleep .02 done alive=no - if kill -0 "$build_pid" 2>/dev/null; then alive=yes; fi - kill -9 "$build_pid" 2>/dev/null || true - wait "$build_pid" 2>/dev/null || true + if kill -0 "$build_pid" 2>/dev/null && kill -9 "$build_pid" 2>/dev/null; then alive=yes; fi + build_status=0 + wait "$build_pid" 2>/dev/null || build_status=$? build_pid='' printf 'traefik gone: %s\nbuild still running at kill: %s\n' "$gone" "$alive" | tee "$W/evidence/kill-mid-remove.txt" - [[ $alive == yes ]] || echo 'WARN: the build had already exited when killed; this run proves the recovery path only, not a mid-build kill' >&2 + [[ $alive == yes && $build_status == 137 ]] || { echo 'FAIL: the build was not interrupted by SIGKILL; mid-build recovery was not exercised' >&2; exit 1; } [[ $gone == 1 ]] || { echo 'FAIL: the traefik container never stopped' >&2; exit 1; } # Recovery re-runs the interrupted step: build is idempotent and removes the orphans the kill left # (start never removes orphans, DEPL EPIC 2026-10-11 ruling 2), then start brings nginx back. From 898e35dd241bc550102a4211dea4740e1bd81b0c Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 09:07:44 -0400 Subject: [PATCH 2/5] fix(ci): exercise proxy recovery legs on pull requests (D-1165) --- .github/workflows/proxy-swap-e2e.yml | 8 +++++-- scripts/e2e/proxy-swap-workflow_test.py | 30 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 scripts/e2e/proxy-swap-workflow_test.py diff --git a/.github/workflows/proxy-swap-e2e.yml b/.github/workflows/proxy-swap-e2e.yml index 4d5e7983f..dddca903f 100644 --- a/.github/workflows/proxy-swap-e2e.yml +++ b/.github/workflows/proxy-swap-e2e.yml @@ -56,14 +56,18 @@ jobs: uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod + - name: Harness regression tests + run: | + python3 scripts/e2e/proxy-swap-kill_test.py + python3 scripts/e2e/proxy-swap-workflow_test.py - name: Add leg if: ${{ github.event_name == 'pull_request' || inputs.leg == 'add' }} run: bash scripts/e2e/proxy-swap.sh --add - name: Round-trip leg - if: ${{ github.event_name == 'workflow_dispatch' && inputs.leg == 'round-trip' }} + if: ${{ github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.leg == 'round-trip') }} run: bash scripts/e2e/proxy-swap.sh --round-trip --assert-identical - name: Kill-mid-remove leg - if: ${{ github.event_name == 'workflow_dispatch' && inputs.leg == 'round-trip' }} + if: ${{ github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.leg == 'round-trip') }} run: bash scripts/e2e/proxy-swap.sh --kill-mid-remove - name: Renew leg if: ${{ github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.leg == 'renew') }} diff --git a/scripts/e2e/proxy-swap-workflow_test.py b/scripts/e2e/proxy-swap-workflow_test.py new file mode 100644 index 000000000..d330fc21e --- /dev/null +++ b/scripts/e2e/proxy-swap-workflow_test.py @@ -0,0 +1,30 @@ +"""Check the proxy legs selected for PRs and manual dispatches.""" +import pathlib +import re +import unittest + + +class WorkflowTest(unittest.TestCase): + def test_legs(self): + root = pathlib.Path(__file__).resolve().parents[2] + workflow = (root / ".github/workflows/proxy-swap-e2e.yml").read_text() + conditions = dict(re.findall(r"- name: (.+ leg)\n\s+if: \$\{\{ (.+) \}\}", workflow)) + for event, leg, expected in [ + ("pull_request", "", set(conditions)), + ("workflow_dispatch", "add", {"Add leg"}), + ("workflow_dispatch", "round-trip", {"Round-trip leg", "Kill-mid-remove leg"}), + ("workflow_dispatch", "renew", {"Renew leg"}), + ]: + with self.subTest(event=event, leg=leg): + selected = set() + for name, condition in conditions.items(): + expression = condition.replace("github.event_name", repr(event)) + expression = expression.replace("inputs.leg", repr(leg)) + expression = expression.replace("&&", "and").replace("||", "or") + if eval(expression, {"__builtins__": {}}): + selected.add(name) + self.assertEqual(selected, expected) + + +if __name__ == "__main__": + unittest.main() From e42390d2e946cdf1bc0f8d6de75d92a7554d6232 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 09:08:43 -0400 Subject: [PATCH 3/5] fix(ci): use byte collation for wiki link audit (D-1288) --- .github/workflows/doc-sync.yml | 4 +++- scripts/ci/wiki-link-audit.sh | 3 ++- scripts/ci/wiki-link-audit_test.py | 37 ++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 scripts/ci/wiki-link-audit_test.py diff --git a/.github/workflows/doc-sync.yml b/.github/workflows/doc-sync.yml index 4869fc223..9cb927b8a 100644 --- a/.github/workflows/doc-sync.yml +++ b/.github/workflows/doc-sync.yml @@ -37,7 +37,9 @@ jobs: run: bin/wikigen -check - name: Wiki links resolve - run: bash scripts/ci/wiki-link-audit.sh + run: | + python3 scripts/ci/wiki-link-audit_test.py + bash scripts/ci/wiki-link-audit.sh # Catches a documented/scripted `nself --flag` whose flag was # never registered on the command it names (the --quick regression diff --git a/scripts/ci/wiki-link-audit.sh b/scripts/ci/wiki-link-audit.sh index 91a166680..bccf0c715 100755 --- a/scripts/ci/wiki-link-audit.sh +++ b/scripts/ci/wiki-link-audit.sh @@ -12,6 +12,7 @@ # wiki's own link-audit notes show [[link]] syntax as an example, and # GitHub Wiki does not resolve a backtick-enclosed token as a link either. set -euo pipefail +export LC_ALL=C cd "$(dirname "$0")/../.." WIKI=".github/wiki" @@ -64,7 +65,7 @@ for root, _dirs, files in os.walk(wiki): continue found.add(target) -for target in sorted(found): +for target in sorted(found, key=lambda value: value.encode("utf-8")): print(target) PYEOF diff --git a/scripts/ci/wiki-link-audit_test.py b/scripts/ci/wiki-link-audit_test.py new file mode 100644 index 000000000..b35b0eb22 --- /dev/null +++ b/scripts/ci/wiki-link-audit_test.py @@ -0,0 +1,37 @@ +"""Audit the same Unicode page names under different caller locales.""" +import os +import pathlib +import shutil +import subprocess +import tempfile +import unittest + + +class WikiLocaleTest(unittest.TestCase): + def test_locales(self): + script = pathlib.Path(__file__).with_name("wiki-link-audit.sh") + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + target = root / "scripts/ci/wiki-link-audit.sh" + target.parent.mkdir(parents=True) + shutil.copyfile(script, target) + wiki = root / ".github/wiki" + wiki.mkdir(parents=True) + pages = ["Alpha", "Zebra", "apple", "éclair"] + for page in pages: + (wiki / f"{page}.md").write_text("\n".join(f"[[{p}]]" for p in pages)) + for locale in ["C", "en_US.UTF-8"]: + for missing in [False, True]: + with self.subTest(locale=locale, missing=missing): + (wiki / "Home.md").write_text("[[Absent]]" if missing else "") + result = subprocess.run( + ["bash", str(target)], env={**os.environ, "LC_ALL": locale}, + capture_output=True, text=True, + ) + self.assertEqual(result.returncode, int(missing), result.stderr) + if missing: + self.assertIn("[[Absent]]", result.stderr) + + +if __name__ == "__main__": + unittest.main() From 01604f9723776efab11fa9cdf1e6354129a80033 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 09:11:24 -0400 Subject: [PATCH 4/5] test(templates): observe the entitlement check working directory (D-1171) --- internal/templates/clone/entitlement_test.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/templates/clone/entitlement_test.go b/internal/templates/clone/entitlement_test.go index 4a95d221b..f346cd3d4 100644 --- a/internal/templates/clone/entitlement_test.go +++ b/internal/templates/clone/entitlement_test.go @@ -55,6 +55,10 @@ func required(t *testing.T, template string) []string { } func TestTemplateEntitlementE110NoWrite(t *testing.T) { + // Check runs in the scaffold target, so an accidental relative write is + // visible to the assertion below. + dir := t.TempDir() + t.Chdir(dir) s := &spy{err: errors.New("no license key configured")} rep := Check(context.Background(), "discord-clone", required(t, "discord-clone"), fixtureCatalog(), s.lic) if len(rep.MissingLicence) != 4 || len(rep.Unknown) != 0 || len(rep.Unchecked) != 0 { @@ -78,8 +82,11 @@ func TestTemplateEntitlementE110NoWrite(t *testing.T) { t.Errorf("bundles asked = %s, want family, clawde and claw once each", got) } // Check does no I/O: the scaffold target stays untouched. - dir := t.TempDir() - if entries, _ := os.ReadDir(dir); len(entries) != 0 { + entries, readErr := os.ReadDir(dir) + if readErr != nil { + t.Fatal(readErr) + } + if len(entries) != 0 { t.Errorf("target not empty: %v", entries) } } From c1391fc77895605914a87279cb7c54c0d59b12b4 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 09:13:46 -0400 Subject: [PATCH 5/5] fix(release): rerun standalone Homebrew gate after formula merge (D-1209) --- .../workflows/homebrew-version-lockstep.yml | 14 +++++----- scripts/release/fixtures/bin/gh | 9 ++++++- scripts/release/fixtures/cases2.sh | 1 + scripts/release/lib/steps2.sh | 11 ++++++++ scripts/release/lockstep_test.sh | 27 +++++++++++++++++++ scripts/release/test.sh | 2 ++ 6 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 scripts/release/lockstep_test.sh diff --git a/.github/workflows/homebrew-version-lockstep.yml b/.github/workflows/homebrew-version-lockstep.yml index c37d3d535..a969e194a 100644 --- a/.github/workflows/homebrew-version-lockstep.yml +++ b/.github/workflows/homebrew-version-lockstep.yml @@ -1,12 +1,12 @@ name: Homebrew Version Lockstep # Standalone gate: fires on every v*.*.* tag push to nself-org/cli. -# Asserts that the Homebrew formula version already matches the tag being cut. +# Asserts that the Homebrew formula version matches the tag being cut. # The release.yml workflow also has this check embedded; this standalone workflow -# fires earlier and provides a clear red status without requiring the full build. +# fires earlier and initially fails while S6 prepares the formula PR. # -# If this gate fails: update Formula/nself.rb in nself-org/homebrew-nself first, -# then re-push (or re-tag) on cli. +# S6 re-runs this standalone gate after the formula merges. Resume S6 if the +# formula or this check still needs repair; release tags are never moved. # # Authority: S30-T02, doc-sync-ritual.md#release-gates @@ -75,15 +75,15 @@ jobs: echo " CLI tag ${TAG} does not match Homebrew formula version ${FORMULA_VER}." echo "" echo " CLI tag v${TAG_VER} does not match Homebrew formula version ${FORMULA_VER}." - echo " Run S14 (formula bump) before cutting this tag." + echo " S6 merges the formula after the release artifacts are built." echo "" echo " Steps to fix:" echo " 1. Update Formula/nself.rb in nself-org/homebrew-nself:" echo " - Set: version \"${TAG_VER}\"" echo " - Update sha256 for the new release archive" echo " 2. Commit and push to homebrew-nself main" - echo " 3. Delete this tag: git tag -d ${TAG} && git push origin :${TAG}" - echo " 4. Re-tag cli at the same commit: git tag ${TAG} && git push origin ${TAG}" + echo " 3. Resume scripts/release/release.sh --version ${TAG_VER} --from S6" + echo " S6 re-runs this check after the formula merges." exit 1 fi diff --git a/scripts/release/fixtures/bin/gh b/scripts/release/fixtures/bin/gh index 2b52f698a..902ee620b 100755 --- a/scripts/release/fixtures/bin/gh +++ b/scripts/release/fixtures/bin/gh @@ -95,6 +95,12 @@ case "$cmd $sub" in prev=$a done case "$wf" in + homebrew-version-lockstep.yml) + if [ -f "$ST/rerun-standalone" ]; then + echo '[{"databaseId":5552,"status":"completed","conclusion":"success"}]' | emit + else + echo '[{"databaseId":5552,"status":"completed","conclusion":"failure"}]' | emit + fi ;; release.yml) if has_tag "$br"; then printf '[{"databaseId":5551,"status":"completed","conclusion":"success","event":"push","headSha":"%s"}]\n' \ @@ -141,7 +147,8 @@ case "$cmd $sub" in [ -f "$ST/artifact_expired" ] && { echo "no valid artifacts found" >&2; exit 1; } dir=""; prev=""; for a in "$@"; do [ "$prev" = -D ] && dir=$a; prev=$a; done mkdir -p "$dir"; cp "$FIXTURES/checksums.txt" "$dir/checksums.txt" ;; - "run rerun") touch "$ST/rerun" ;; + "run rerun") + if [ "$3" = 5552 ]; then touch "$ST/rerun-standalone"; else touch "$ST/rerun"; fi ;; "release view") published || { echo "release not found" >&2; exit 1; } printf '{"publishedAt":"2026-01-01T00:00:00Z","assets":[{"name":"checksums.txt"},{"name":"nself-1.5.0-darwin-arm64.tar.gz"}]}\n' | emit ;; diff --git a/scripts/release/fixtures/cases2.sh b/scripts/release/fixtures/cases2.sh index aff43bfe9..d1448e4d7 100644 --- a/scripts/release/fixtures/cases2.sh +++ b/scripts/release/fixtures/cases2.sh @@ -83,6 +83,7 @@ run_pty --version "$V" --from S6 --yes check "S6 done path exits 0" test "$RC" -eq 0 check "S6 done path says done" grep -q "S6 homebrew-formula.* done" "$T/out" check "S6 done path re-ran the lockstep job" grep -q 'run rerun 5551 -R nself-org/cli --failed' "$GH_LOG" +check "S6 done path re-ran the standalone gate" grep -q 'run rerun 5552 -R nself-org/cli --failed' "$GH_LOG" check "S6 done path merged nothing" test ! -s "$ADMIN_LOG" rm -rf "$T" mk_env "$HAVE_GO" diff --git a/scripts/release/lib/steps2.sh b/scripts/release/lib/steps2.sh index 2b25961f3..cee553cdb 100644 --- a/scripts/release/lib/steps2.sh +++ b/scripts/release/lib/steps2.sh @@ -56,6 +56,17 @@ step_build_wait() { # run still in progress is left alone. rerun_lockstep() { local c + # The standalone tag-push gate runs before S6 can merge the formula. + # Repair that expected failure as well as release.yml's embedded gate. + gh_need "the standalone Homebrew gate for $TAG" run list -R nself-org/cli \ + --workflow homebrew-version-lockstep.yml --branch "$TAG" --event push \ + --json databaseId,status,conclusion \ + --jq '[.[] | select(.status == "completed" and .conclusion == "failure")][0].databaseId // empty' + if [ -n "$GH_OUT" ]; then + gh run rerun "$GH_OUT" -R nself-org/cli --failed \ + || die_step S6 homebrew-formula "could not re-run standalone Homebrew gate $GH_OUT" + say "S6 homebrew-formula: re-ran standalone Homebrew gate $GH_OUT" + fi find_run if [ -z "$RUN_ID" ]; then return 0; fi if [ "$1" = 1 ]; then poll "run $RUN_ID to settle" 60 release_finished || return 0; fi diff --git a/scripts/release/lockstep_test.sh b/scripts/release/lockstep_test.sh new file mode 100644 index 000000000..927564271 --- /dev/null +++ b/scripts/release/lockstep_test.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Exercise S6's standalone gate repair without GitHub or release mutations. +set -euo pipefail +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +for state in failure success absent; do + ( + . "$ROOT/scripts/release/lib/steps2.sh" + TAG=v9.9.9 + GH_OUT='' + calls='' + gh_need() { + [[ $* == *homebrew-version-lockstep.yml* && $* == *"--branch $TAG"* ]] + if [[ $state == failure ]]; then GH_OUT=5552; else GH_OUT=''; fi + } + gh() { calls="$*"; } + say() { :; } + die_step() { exit 1; } + find_run() { RUN_ID=''; } + rerun_lockstep 0 + if [[ $state == failure ]]; then + [[ $calls == 'run rerun 5552 -R nself-org/cli --failed' ]] + else + [[ -z $calls ]] + fi + ) +done +echo 'PASS: S6 repairs failed standalone lockstep runs only' diff --git a/scripts/release/test.sh b/scripts/release/test.sh index f95a74093..4764077a7 100755 --- a/scripts/release/test.sh +++ b/scripts/release/test.sh @@ -16,6 +16,7 @@ BASE=$(mktemp -d "${TMPDIR:-/tmp}/release-test.XXXXXX") trap 'rm -rf "$BASE"' EXIT # shellcheck disable=SC1091 . "$FIXTURES/setup.sh" +bash "$RELDIR/lockstep_test.sh" || exit 1 PASS=0; FAILN=0 ok() { echo "PASS $1"; PASS=$((PASS + 1)); } @@ -108,6 +109,7 @@ check "formula on_intel hash" test "$(formula_hash intel)" = "$INTEL" check "formula hashes differ" test "$(formula_hash arm)" != "$(formula_hash intel)" check "formula step needed no release download" test -z "$(grep 'releases/download' "$CURL_LOG")" check "lockstep job re-run" grep -q 'run rerun 5551 -R nself-org/cli --failed' "$GH_LOG" +check "standalone lockstep job re-run" grep -q 'run rerun 5552 -R nself-org/cli --failed' "$GH_LOG" check "cli image dispatched" grep -q "workflow run docker-publish.yml -R nself-org/cli -f version=$V" "$GH_LOG" check "admin tagged on its lockstep merge commit" test "$(git --git-dir="$FIX/admin.git" rev-parse "refs/tags/v$V^{commit}")" = "$(git --git-dir="$FIX/admin.git" rev-parse main)" check "admin tag is annotated" test "$(git --git-dir="$FIX/admin.git" cat-file -t "refs/tags/v$V")" = tag