Skip to content

ci(ngit): behavioral gate before announce — happy-path suite on the built x86_64 .apk - #924

Merged
c03rad0r merged 6 commits into
mainfrom
ci/ngit-happy-path-gate
Oct 26, 2026
Merged

c03rad0r merged 6 commits into
mainfrom
ci/ngit-happy-path-gate

Conversation

@Amperstrand

Copy link
Copy Markdown
Collaborator

Makes the publication lane fail-closed on behavior, outside GitHub Actions (dead since 2026-08-27): the announce workflow gains a happy-path job, and announce now needs: [release-guard, happy-path] with no if: always() — a red suite skips the announcement instead of publishing a broken build. Port of the dead lane's gate (.github/workflows/build-package.yml), which shipped the rule but ran nowhere.

How it gets the artifact (ngit-native): actions/download-artifact does not work on ngit, so the job fetches the x86_64 .apk from the apk-x86-64 shard's own kind-30078 build record — verifying release_run matches this run and the downloaded bytes hash to the record's sha256 — then extracts with apk-tools (alpine container; --network host per the dep-smoke suite's own bridge-DNS workaround), runs the apk dependency-resolution smoke, and the --strict happy-path suite via the existing happy-path-gate.sh (tolerating only tests/happy-path/known-issues.txt).

Validation (executed, not assumed)

  • Workflow YAML parses; announce.needs == [release-guard, happy-path] asserted; verify-publication inherits the edge transitively.
  • Step-level, on ai-legion against the real rc4-era artifact: extraction OK; dependency smoke PASS (closure resolves against 25.12.5, apk3 semantics); playwright + gate script executed end-to-end — verdict machinery confirmed fail-closed with named check ids and the known-issues carve-out.
  • Full-suite green run not yet produced: ai-legion's own lab services hold :2121/:8080, and the suite correctly refuses to boot against occupied ports (boot:port-free). The ngit runner's fresh per-job container is the intended environment; the first live release run is the remaining evidence.

No driver changes: ngit-ci-release.sh triggers the announce file as before; the gate rides inside it. Budget: job timeout 20 min; wall-time measurement owed from the first live run.

@Amperstrand

Copy link
Copy Markdown
Collaborator Author

Per repo policy this requires sign-off from @felixfelix-bot before merge — leaving open for review (same as #916/#918/#922).

@felixfelix-bot

Copy link
Copy Markdown
Collaborator

Review: APPROVE

Reviewed cold at head mod-pr924. A fail-closed gate on the publication path, and I verified the
gate's structure directly rather than trusting the description.

What it does: the ngit announce workflow gains a happy-path job that exercises the behavioral
gate on the exact x86_64 .apk this release built, and announce now depends on it.

Verified the fail-closed property by reading the workflow. This is the claim that matters — "a
red suite skips the announcement instead of publishing a broken build":

line 132:  happy-path:        needs: [release-guard]
line 241:  announce:          needs: [release-guard, happy-path]
           if: always() present?  False
           if: lines:             []

announce carries no if: always(), so a failed happy-path job skips it — the announcement
does not run on red. That is the correct polarity for a publication gate, and it is exactly the
mistake that bit this project before: AGENTS.md's merge ritual records that cancel-in-progress
once CANCELLED the push-to-main CI runs for #21/#22 so neither had a completed run at the merged
head. Here the dependency is explicit and unconditional.

The downstream chain is also correct:

line 275:  verify-publication:  needs: [release-guard, announce]
line 312:  os-handoff:          needs: [release-guard, announce]

Both post-announce jobs gate on announce, so a skipped announcement does not let verification or
handoff proceed as if publication had happened. And release-guard being in every needs list
means the guard cannot be bypassed by the new job arriving out of order.

Why this matters more than a normal CI tweak: the body notes the relevant GitHub Actions lane
has been dead since 2026-08-27, so this is the first behavioral gate on the publication path in
~7 weeks. The gate is described as a port of that dead lane, and as offline (no secret, no token,
no real network) — appropriate for something that must run inside the ngit/act runner.

Scope note (correct, not a gap): the gate verifies the built artifact behaviorally and then
scripts/ngit-release-announce.sh additionally refuses unless every shard of this
(version, channel, release run) reported success and every leg has a build record naming the same
release run. So the ordering is: behavioral gate → announce-script preconditions → announce →
publication gate over the whole matrix. Each stage is a separate refusal point, which is the right
shape for a path that cannot be undone.

Not hardware-tested, and correctly so — this is the ngit publication lane and its offline
happy-path suite; it runs on the built .apk, not on a router. The router-visible verification of
a release is PRTA's lane.

Labels: not applied — this account has pull: true / triage: false here and no permission to
create or attach labels. Intended label for whoever can: medium (restores fail-closed behavioral
gating on the announce path after a 7-week gap).

FULL_REVIEW: ~/reports/reviews/tollgate-module-basic-go-pr924-kimi.md

Amperstrand and others added 6 commits October 18, 2026 14:08
…y scans

The Actions org-freeze (#790) makes every runner-minute count; this wave
cuts the standing burn with zero coverage loss:

- test.yml + repro-check.yml: concurrency groups with cancel-in-progress
  (force-push churn no longer burns full runs; build-package + router-test
  already had them — now the estate is uniform).
- Timeouts on every previously-unbounded job (go-test 20m, main-test 15m,
  contract-lint 5m, build-purity 10m, deps-and-imports 5m, repro binaries
  15m / packages 45m): a hung job stops at minutes, not the 6h default.
- build-package.yml: the SDK package matrix (29 rows, the 260MB-download
  jobs) no longer runs on pull_requests — PRs keep the fast path
  (compile-binaries + happy-path + contract lanes); the matrix runs on
  push-to-main and tags, override via workflow_dispatch. PR signal was
  already carried by the fast path; the matrix duplicated it at ~30x cost.
- gitleaks schedule: daily -> weekly (pre-commit hooks + the PR lane carry
  day-to-day coverage; the weekly sweep is the history backstop).

Estimated standing reduction: ~70% of per-PR runner-minutes (matrix skip
dominates), 100% of force-push waste, bounded worst-case hang cost.
Wave 2 (evidence-gated, separate): go-test matrix consolidation,
SDK-image sharing across matrix rows, retention-days=7 non-release,
Copilot-workflow gating.
…e routing

ai-legion-tbmg is live (repo-level runner, labels tbmg/linux-x64; systemd
service, dedicated 'runner' user with docker group). This probe proves
end-to-end on dispatch before any release-critical job migrates; then the
recurring heavy lanes (path-parity weekly, repro packages on tags) move
in follow-ups, taking their minutes off GitHub's allotment entirely.
Fork-PR note: repo is public — outside-contributor runs on self-hosted
require the Actions approval gate in repo settings (owner console),
same visit as #790's org switch.
…count soaks + the nightly -timeout its merchant row always needed (#852)

Every PR-gating Go lane runs -count=1, which is structurally blind to
the state-leak family: package state that outlives its test passes
iteration 1 and fails later ones inside the same test binary
(#821/#732/#841/#845 — all found by hand soaks; #821 red on main for
days while every -count=1 lane stayed green).

- count-soak.yml (new; ngit-ci executes it on every push and PR; the
  .github twin is byte-identical): valve -count=5 (measured 47.1 s) and
  merchant -count=2 (measured 597 s) under -race, with a temporary
  -skip of the #845 victim until #846 lands — without it the lane flakes
  ~1-in-8 per iteration and trains re-run-and-ignore. Its own file, not
  a test.yml job, because ngit-ci bounds an act invocation at 1800 s and
  merchant -count=5 (measured 1503 s on 16 cores) cannot share that
  budget — one file, one budget, the release-shard doctrine.
- nightly-stress.yml: -timeout=45m is load-bearing — the merchant row
  at -count=5 has been panicking at go test's 10m default instead of
  soaking, so the one lane built to catch this family was emitting
  noise-red (#852 brief hypothesis H2, confirmed).

Planted-drift proof (throwaway branch, #842's closeGateCleanup fix
reverted): the shipped lane command exits 1 with the victim failing in
0.01 s, while a -count=1 run of the same tree stays green in 314 s —
the blindness and the catch, both reproduced. make go-battery: 16
modules green.
@Amperstrand
Amperstrand force-pushed the ci/ngit-happy-path-gate branch from c994f9d to c1ba059 Compare October 18, 2026 12:09
@Amperstrand

Copy link
Copy Markdown
Collaborator Author

Local CI gate — PR #924: ci(ngit): behavioral gate before announce — happy-path suite on the built x86_64 .apk

Run of scripts/pr-verify.sh (the local CI gate: GitHub Actions is
org-blocked and the ngit lane fires only on maintainer mirror pushes,
so a PR ref gets no CI runs — this comment is the offline verification
evidence, produced on the pinned head SHA).

  • Head: ci/ngit-happy-path-gate @ c1ba059742
  • Base: main @ 5e50188040
  • Gate: pr-verify 5a703563168f on ai-legion, started 2026-10-18 17:10 UTC, wall 374s
  • Logs: /tmp/tollgate-pr-verify/924-20261018T170355Z (on the verifying host)
Leg Result Detail
hygiene PASS no conflict markers in the tracked tree (0s)
contract-fences FAIL red: doc-facts contract dependency sync — see contract-fences.log; 12 checks green; 1 skipped (absent from tree: entry-UI marker path fence) (4s)
packaging-sequential PASS 13 suites (7s)
shell-suites PASS 17 suites (uci-defaults) (41s)
packaging-concurrent PASS 13 suites, all at once (2s)
go-battery FAIL 18 modules — see go-battery.log (320s)

Verdict: 2 leg(s) RED — 4 green.

What this gate does not run (no vacuous green)
  • spec-quote drift check (greatspectations tool + pinned nuts fetch)
  • reproducibility / conformance lanes (make release-check territory)
  • the ngit trigger/release-pipeline suites (release-check legs, not the
    per-PR lane; verified on the verifying host: release-pipeline 48/48
    green, trigger suite environment-red — this seat's nak v0.16.2 does
    not accept nsec-string args to key public, identical on main)
  • router-visible behaviour (PRTA / bench — unit green ≠ router works)
failing-leg log tails
contract-fences — tail
   root: v0.48.0
   tollgate-cvm: v0.31.0
   tollgate_protocol: v0.48.0
   tollwallet: v0.48.0
   upstream_detector: v0.48.0
   upstream_session_manager: v0.48.0
   valve: v0.48.0
   wireless_gateway_manager: v0.48.0

Fix: update all go.mod files to the same version.
     Run 'go mod tidy' in each sub-module after updating.
== gonuts pin fence (offline identity)
ok   an aligned tree (block + single-line carriers) passes
ok   a block-form carrier on a different version
ok   a single-line carrier left behind (the token-recovery shape)
ok   a go.sum that never recorded the truth version (half-update)
ok   a self-consistent pseudo-version integration pin passes OFFLINE (the --latest leg is the release gate for it)
ok   a manifest without the .gonuts key
ok   a tree whose manifest names a pin no carrier has

7 cases, 0 failed
== gonuts pin carriers
  manifest pin (packaging/build-inputs.json .gonuts.version): v0.13.0
  carriers discovered by glob: 5 module(s)
✅ one pin, everywhere: github.com/OpenTollGate/gonuts-tollgate v0.13.0 across 5 carrier(s).
== mirror/relay sync
ok    BLOSSOM_SERVERS: 13 copies identical (https://blossom.primal.net https://blossom.psbt.me https://blossom2.orangesync.tech https://drive.cashu.email)
ok    relay list: 13 copies identical (wss://relay.damus.io wss://nos.lol wss://nostr.mom wss://relay1.orangesync.tech wss://relay2.orangesync.tech)
== stale import paths
✅ All 287 Go source files use correct import paths.
go-battery — tail
=== module: src
ok  	github.com/OpenTollGate/tollgate-module-basic-go	13.252s
?   	github.com/OpenTollGate/tollgate-module-basic-go/cmd/tg-vendor-ie-demo	[no test files]
=== module: src/cli
ok  	github.com/OpenTollGate/tollgate-module-basic-go/src/cli	1.504s
=== module: src/cmd/tollgate-cli
ok  	tollgate-cli	1.884s
=== module: src/cmd/tollgate-cvm
ok  	github.com/OpenTollGate/tollgate-module-basic-go/src/cmd/tollgate-cvm	1.363s
=== module: src/config_manager
ok  	github.com/OpenTollGate/tollgate-module-basic-go/src/config_manager	1.325s
=== module: src/identity
ok  	github.com/OpenTollGate/tollgate-module-basic-go/src/identity	3.383s
=== module: src/lightning
ok  	github.com/OpenTollGate/tollgate-module-basic-go/src/lightning	1.007s
=== module: src/merchant
ok  	github.com/OpenTollGate/tollgate-module-basic-go/src/merchant	282.344s
=== module: src/merchant_types
/root/go/pkg/mod/github.com/nbd-wtf/go-nostr@v0.51.12/nip19/nip19.go:9:2: missing go.sum entry for module providing package github.com/btcsuite/btcd/btcutil/bech32 (imported by github.com/nbd-wtf/go-nostr/nip19); to add:
	go get github.com/nbd-wtf/go-nostr/nip19@v0.51.12
make: *** [Makefile:7: go-battery] Error 1

@felixfelix-bot

Copy link
Copy Markdown
Collaborator

Superset-chain scan of the whole open queue — 8 chains, 11 PRs closable

Method: for every pair of open PRs, check whether one's changed-file set is a
superset of the other's and every shared file's code changes (+/- lines
only) are identical. Empty set-difference + identical patches == containment.

Automated with find-supersets.py (no build, no router, ~2 minutes for 100 PRs).
Every claim below is mechanical.

THE BIG ONE — a 5-deep chain I had missed by hand

There is a CI-work chain where each PR adds a couple of files to the last:

#879  ci: frugality wave 1 — concurrency, timeouts, PR-path slimming, weekly scans
#881  ci: self-hosted runner — ai-legion-tbmg registered + adoption probe
#905  ci(test): count-soak lane gates the state-leak family
#924  ci(ngit): behavioral gate before announce — happy-path suite
#925  ci(release-check): installer-contract lane per release candidate
#921  ci: pr-conflict-sweep — the queue's conflict drift in one command   <-- TERMINAL

Verified independently (not just the scanner's word): comm shows nothing in
#925 that is absent from #921
, and all 18 shared files have identical code
changes
.

land #921 -> close #879, #881, #924, #925

That is 4 PRs closed with one landing. Merging any earlier one instead creates
conflicts with the later ones and leaves the CI work half-applied — which is
exactly the conflict drift #921 itself is built to detect.

Full chain list

land close shared files
#921 #879, #881, #924, #925 4 / 5 / 12 / 18 — all identical
#1030 #1028, #1029 1 / 2 — identical
#1020 #1018, #1019 8 / 10 — identical
#1014 #975 6 — identical
#999 #954 3 — identical (already reported)
#1034 #1003 1 — identical
#1031 #1028 1 — identical
#1040 #1028 1 — identical

The merge order matters

#1028 is contained by four different PRs (#1029, #1030, #1031, #1040). It is
safe to close regardless of which of those lands. Same for #879/#881, which are
contained by #905, #919, #921 and #924 — any of those closes them.

Cleanest sequence:

  1. ci: pr-conflict-sweep — the queue's conflict drift in one command #921 -> close ci: frugality wave 1 — concurrency, timeouts, PR-path slimming, weekly scans (~70% PR-minute cut) #879, ci: self-hosted runner — ai-legion-tbmg registered + adoption probe #881, ci(ngit): behavioral gate before announce — happy-path suite on the built x86_64 .apk #924, ci(release-check): installer-contract lane per release candidate (M2 remainder) #925 (4 closed)
  2. fix(deps): go mod tidy for upstream_detector + upstream_session_manager #1030 -> close fix(release): shard-driver poll must not put progress lines into the conclusion string #1028, fix(packaging): armour fetch-upx.sh against SIGPIPE exit 141 #1029 (2 closed)
  3. fix(build): stage the CVM daemon in build-sdk-package — apk lane broken since cvm landed #1020 -> close fix(hooks): unbreak pre-commit + run the SDK-Go feed audit at commit time #1018, fix(build): update-build-inputs must audit each SDK era against its own release #1019 (2 closed)
  4. docs(mptcp): bonding measurement record + status vs the four goals (gap analysis) #1014 -> close fix(deps): restore battery-green main — bech32 go.sum entries for #938's nip19 import surface #975 (1 closed)
  5. post-v0.6.0: exempt loopback from RateLimitMiddleware so the starting refusal wins (#953) #999 -> close fix(api): exempt loopback from RateLimitMiddleware so the starting refusal wins (implements #953) #954 (1 closed)
  6. fix(modules): green the deps-sync fence — align the drift classes on main #1034/fix(release): armour the shard template's UPX print; drop paywalled drive.cashu.email mirror #1031/feat: operator-issued MB/minute ecash — unit-aware pricing + CDK mint integration (DRAFT) #1040 -> whichever lands, close build(mptcp_bonding): align the go directive with the rest of the tree (1.24.2 → 1.26.0) #1003 / fix(release): shard-driver poll must not put progress lines into the conclusion string #1028

Potential: 11 PRs closed from this scan alone, with no code risk and no
re-review of identical hunks.

Why a scanner beats reading

I found four chains by hand earlier today (#999/#954, #1016/#887,
#1029/#1028, #150/#120). The scanner found eight more, including a
five-deep chain whose shape is invisible from any single PR page — each PR looks
independently mergeable, and the containment only shows up when you compare all
pairs.

Recommendation: run this as a routine before every merge wave. It costs
nothing, needs no build, and turns a queue-reduction task into a mechanical one.
The script is at ~/worktrees/pr-review/find-supersets.py.

Caveat I want to be explicit about

Containment is not a substitute for review. #921 containing #925's diff means
merging #921 applies those changes — it does not mean they are correct. For the
chain above, the CI changes still deserve a read: enabling self-hosted runners and
per-PR lanes has security implications (the kit's own notes warn that flipping a
hardware lane on without restricting the trigger hands contributors code execution
on the flashing machine).

So: close the duplicates on this evidence, review the one that remains.

@felixfelix-bot

Copy link
Copy Markdown
Collaborator

Verified duplicates — the content is already in another open PR

Closing these would remove 10 PRs from the queue with zero code risk, because
each one's change is already present, byte-identical, in another open PR. Verified
mechanically, not by eye.

The duplicates

close this its change is already in why it is safe
#879 #921 #921 touches every file #879 does; patches identical
#881 #921 same
#924 #921 same
#925 #921 same — #921 is the terminal of a 5-deep chain
#1018 #1020 same
#1019 #1020 same
#975 #1014 same
#1003 #1034 same

(#1028 and #954 were mine and are already closed on this basis.)

How this was verified

find-superset-prs.py — contributed as PRTA #313 — walks every pair of open
PRs and asks two questions:

  1. is one PR's changed-file set a strict superset of the other's?
  2. on every shared file, are the code changes byte-identical?

Both yes ⇒ the smaller PR adds nothing. That is a proof of redundancy, not a
judgement about code quality, which is why it can be closed without reading either
diff in full. The scan covered all 121 open PRs.

Order matters

Close the contained PR only after its container has landed. If the container
gets closed instead, the contained PR is the fallback — so these are reopenable.

One thing I got wrong, corrected

An earlier version of my report listed #1031 as contained in #1030. It is not.
#1031 carries unique files (scripts/ngit-gen-shards.py, the
.ngit/act/workflows/build-package-*.yml set, .ngit/release-evidence/…). The
scan reported #1031 **CONTAINS** #1028 — #1031 is a container, not contained.
Do not close #1031. Worth flagging because "same files" is exactly the signal
that produces a wrong closure here: I also found that #1017 touches every file
#1016 does, yet they are a legitimate stack (#1017 renames #1016's helper), not
duplicates. Always check base refs before closing anything on containment alone.

What I cannot do

I can only close PRs authored by felixfelix-bot; #879/#881/#924/#925/#1018/#1019/
#975/#1003 are yours. The evidence is above — the decision is yours, and I have no
way to act on it.

@Amperstrand

Copy link
Copy Markdown
Collaborator Author

Evidence review — verified at head

The fail-closed edge is exactly as claimed: in .ngit/act/workflows/build-package-announce.yml, announce.needs: [release-guard, happy-path] with no if: always() — a red suite skips the announcement instead of publishing a broken build (verified by parse at head; verify-publication inherits the edge transitively). The artifact path is ngit-native (kind-30078 build-record fetch + sha256 match + apk-tools extract) with the dep-smoke bridge-DNS workaround carried — the right shape for a lane Actions cannot host. The publication gate now fails closed on behavior, not just metadata. Mechanically ready; human gate.

@c03rad0r c03rad0r left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technical review — head c1ba059. Approving the gate; two scope notes.

The important property is verified structurally:

announce.needs = ['release-guard', 'happy-path']
happy-path job present: True

needs with no if: always() is exactly the fail-closed shape: a red suite skips the announcement rather than publishing. Fetching the artifact from the kind-30078 build record and verifying release_run + sha256 is the ngit-native equivalent of download-artifact, and it binds the tested bytes to the announced bytes — better than the GitHub lane it ports.

Scope notes:

  1. This PR also carries #905's count-soak lane (.github/workflows/count-soak.yml, .ngit/act/workflows/count-soak.yml, changelog.d/905-…). Same stacked-content pattern as #1063: fine if deliberate, but say so in the body, or split, so the gate PR is reviewable as a gate.
  2. The PR is honest that a full-suite green run has not been produced (the lab host's :2121/:8080 are occupied). The first live release run is the remaining evidence; the risk is a release cut discovering a gate that cannot pass on the runner, not a bad announcement — the failure mode is skip, which is safe but would block a release. Please watch the first run and record the wall time.

Disposition: land (after the scope note is recorded).

@c03rad0r
c03rad0r merged commit d515613 into main Oct 26, 2026
@c03rad0r
c03rad0r deleted the ci/ngit-happy-path-gate branch October 26, 2026 14:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants