Skip to content
Open
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
4 changes: 3 additions & 1 deletion .github/workflows/doc-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path> --flag` whose flag was
# never registered on the command it names (the --quick regression
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/homebrew-version-lockstep.yml
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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

Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/proxy-swap-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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') }}
Expand Down
11 changes: 9 additions & 2 deletions internal/templates/clone/entitlement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}
Expand Down
3 changes: 2 additions & 1 deletion scripts/ci/wiki-link-audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand Down
37 changes: 37 additions & 0 deletions scripts/ci/wiki-link-audit_test.py
Original file line number Diff line number Diff line change
@@ -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()
35 changes: 35 additions & 0 deletions scripts/e2e/proxy-swap-kill_test.py
Original file line number Diff line number Diff line change
@@ -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()
30 changes: 30 additions & 0 deletions scripts/e2e/proxy-swap-workflow_test.py
Original file line number Diff line number Diff line change
@@ -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()
8 changes: 4 additions & 4 deletions scripts/e2e/proxy-swap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 8 additions & 1 deletion scripts/release/fixtures/bin/gh
Original file line number Diff line number Diff line change
Expand Up @@ -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' \
Expand Down Expand Up @@ -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 ;;
Expand Down
1 change: 1 addition & 0 deletions scripts/release/fixtures/cases2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 11 additions & 0 deletions scripts/release/lib/steps2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions scripts/release/lockstep_test.sh
Original file line number Diff line number Diff line change
@@ -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'
2 changes: 2 additions & 0 deletions scripts/release/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)); }
Expand Down Expand Up @@ -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
Expand Down
Loading