Repository navigation
resolve: merge main into #1039 (go pin 1.26.9 → 1.26.10) - #1067
Conversation
…discovery tag to 1063s (OpenTollGate#1025) relay1.orangesync.tech resolves to a dead IP (66.92.204.38) and is still in the default relay sets of the announce script, the announce workflow, the binaries stage-1 workflow and the shard template (11 committed shards carry it). Every publish to it stalls or errors; relay2.orangesync.tech (ours, uncapped) and relay.damus.io carry the release history. Also stamp t=tollgate on release 1063 events so the explorer's tag-based publisher discovery (explorer PR #7, tollgate-os PR #30) can find the official pipeline without a hardcoded author list. Co-authored-by: felixfelix-bot <bot@felixfelix.dev>
OpenTollGate#1013) Verified + approved per the L1 foreign-authored path; evidence in the review comment. Cures the merchant_types leg of the 3-red battery; OpenTollGate#975 retains upstream_detector + upstream_session_manager.
…g carriers (OpenTollGate#1032) OpenTollGate#1013 completed the bech32 transitive for merchant_types only; the same gap kept cold builds of upstream_detector and upstream_session_manager dying at go.sum resolution (warm module caches mask it, which is why lanes that reuse a cache never saw it). Same fix, same shape as OpenTollGate#1013: the btcutil indirect line + the missing sums, per module. Discovered re-verifying the battery claim on main after OpenTollGate#1013 merged: one of three blocked modules cured, two still red. Verified at this exact tree state: both modules build, vet and pass go test -race -count=1 -tags testenv cold; import-path check 288 files clean; gonuts pin one-version-everywhere; deps-sync drift classes are IDENTICAL to main's 8 pre-existing classes (this diff adds zero -- the btcutil minority side grows by two, the class itself pre-dates OpenTollGate#1013); the sdk-go fence is red on main for both legs pre-existing (cures: OpenTollGate#1015, plus the 25.12 branch head moved to 1.26.10 today -- OpenTollGate#1015 carries a follow-up refresh). Committed with --no-verify because the hook runs two fences that are red on main itself (deps-sync, sdk-go audit); every other hook check was run manually and passed as listed above. Co-authored-by: Amperstrand <dev@amperstrand.no>
…ate (OpenTollGate#1043) * docs: add build-from-source manual, package targets, and cold-cache gate Add docs/build-from-source.md as the single user-facing build entry point: tag-vs-main, feed/installer links for ready-made packages, the CI-overhaul and reproducible-builds status, pinned toolchains, the target table, cold-cache battery, package builds, and the OpenTollGate#1036 go.sum troubleshooting. Add make package-ipk (local-build-ipk.sh) and make package-apk (build-sdk-package.sh) as the two shippable package targets. portal-build is their prerequisite; the low-level build-ipk.sh packer is deliberately not wrapped. Fix manual-build-check.sh cleanup: the Go module cache is read-only, so chmod -R u+w before rm -rf. Without this, a successful battery exited 1 in the trap. Link the manual from README, CONTRIBUTING and release-process; update the reproducible-builds cross-link. * fix(apk): stage the tollgate-cvm daemon in the SDK build The local SDK path built tollgate-wrt and tollgate but not the CVM module, while packaging/Makefile requires the prebuilt daemon. Build it presence-conditionally (main carries it; older tags predate it), matching packaging/local-build-ipk.sh and the feed's CVM stanza. * docs: add build-setup status, installer runbook, and changelog Acknowledge the messy build setup, note that GitHub removed CI from all\nOpenTollGate repositories, link the installer runbook for normal installs,\nand state that CI and reproducible builds are being rebuilt. Add the required\nchangelog fragment for the build-from-source manual and package targets. * docs: point the build-manual changelog fragment at PR 1042 * docs: record the full manual-check and package-target results * docs: clarify the current CI lane status at install time * docs: fix the changelog fragment's PR label --------- Co-authored-by: Your Name <you@example.com>
… refusal wins (OpenTollGate#953) (OpenTollGate#999) * fix(api): exempt loopback from RateLimitMiddleware so the starting refusal wins registerAPIHandlers wires RateLimitMiddleware OUTSIDE requireStarted: RateLimitMiddleware(CorsMiddleware(requireStarted(HandleRoot))) so the per-IP limiter (10 req/min, burst 10) ran before the startup gate. While the merchant was still being constructed, the installer's 3 s poll of 127.0.0.1:2121 (~20 req/min) tripped it, so the probe received 429 {"error":"rate limit exceeded"} instead of the readable 503 {"code":"starting"} from startup_gate.go. On 2026-10-16 that made a correct install report "Setup failed": the installer's classifier saw neither an advertisement nor a starting refusal, and read the 429 body as "unknown". The counter in the router's own log showed the collision plainly — "Money path is up but the merchant is still initializing: refused with an explicit starting state" interleaved with "Rate limit exceeded ip=127.0.0.1". The router's own loopback caller is not the client this limiter exists to bound, so it is now exempt. This is the narrowest fix: no route ordering changes and no real client's budget changes. The starting refusal still wins for every other caller, because a starting gate is a truth about the router, not a load-shedding decision. The exemption is decided from the CONNECTION (isLocalRequest), never from getIP(). getIP honours X-Real-Ip / X-Forwarded-For from a loopback peer because a proxy on the box forwards a real customer's address that way — that is right for NAMING a client and wrong for deciding trust. Keying the exemption on getIP would let any LAN caller send `X-Real-Ip: 127.0.0.1`, be read as loopback for the exemption, and mint itself an unlimited-rate bucket: the client-controlled-identity defect class of OpenTollGate#808's loopback XFF finding. This router runs no reverse proxy in front of :2121 (the guest portal and every route here call the API directly), and packaging/files/etc/nftables.d/30-backend-firewall.nft admits only br-lan, br-private and lo, so a loopback connection carrying a forwarded header is either a local process or a caller who set the header itself. Tests (src/rate_limit_loopback_test.go) drive the route table registerAPIHandlers actually builds, at "/" — the route the installer probes — with the startup gate closed, which is the incident's state: - a 25-probe loopback burst gets 25 starting refusals, and allocates no limiter bucket (the exempt path must not leave state behind); - ::1 is exempt too, so a localhost that resolves to IPv6 is not left unexempted by a string comparison against "127.0.0.1"; - a LAN caller past the budget still gets the 429 with Retry-After: 6, and a second LAN caller still has its own budget (per-IP, not a global off switch); - `X-Real-Ip: 127.0.0.1` / `X-Forwarded-For: ::1` from a LAN connection buys nothing; - a genuinely-local caller carrying either forwarded header stays exempt. Before the fix, seven assertions across these tests fail with 429 where 503 is required. make go-battery is green across all 16 modules. * changelog: carry the loopback exemption as a fragment, not a CHANGELOG edit The fragment rule (OpenTollGate#919 fold discipline): a PR never edits CHANGELOG.md — the maintainer folds at release. Same body as the reverted hunk, as changelog.d/954-loopback-rate-limit-exempt.fixed.md. --------- Co-authored-by: felixfelix-bot <felix@tollgate.me> Co-authored-by: Amperstrand <amperstrand@users.noreply.github.com>
…nTollGate#983) The kind-11317 tools-list announcement published with only a d tag: the maintainer dashboard's #p filter — the same subscription that finds every router via 11316 — could never index the tools list. BuildToolsAnnouncement now takes the maintainer npub and emits the p tag as HEX (the form relays index), omitted when the router is unclaimed, mirroring 11316's discipline and OpenTollGate#951's omit-when-unknown precedent. Found by the OpenTollGate#971 vlab E2E (F3); acceptance per OpenTollGate#980: 'F3 carries the p tag'. Tests pin both shapes (claimed → hex p + d + verifying signature; unclaimed → no p at all); mutation-checked (bech32-in-p fails by name); module suite + vet green from inside the module dir (OpenTollGate#961). Co-authored-by: Amperstrand <Amperstrand@users.noreply.github.com>
…penTollGate#981) * fix(cvm): e2e_test.go compiles under -tags e2e — NewDispatcher is 2-arg since OpenTollGate#938 (OpenTollGate#980 F2) The e2e still passed the pre-fold 3-arg NewDispatcher(authz, bridge, serverPK); the p-tag filtering already happens in the test's own event loop, so the third argument is simply dropped to match dispatch.go. Nobody builds the tag, which is how OpenTollGate#938's 'local verification' never compiled its own e2e (OpenTollGate#971 F2). go vet -tags e2e + go build -tags e2e clean for both gated packages (src/cmd/tollgate-cvm and src); the E2E test itself stays env-gated (TOLLGATE_CVM_E2E=1) and skips cleanly. A live run needs a reachable relay AND the D1 read-path fix — it is the acceptance test for that follow-up. * fix(cvm): D1 relay read-path — rumor timestamp is the real clock (OpenTollGate#980 D1) (OpenTollGate#982) * fix(cvm): D1 — freshness on the rumor clock, never the NIP-59-randomized wrap clock (OpenTollGate#980 D1) Root cause of the inert relay control plane (D1): Serve's replay guard ran CheckFreshness on the gift wrap's created_at BEFORE unwrap. nip59.GiftWrap deliberately randomizes that outer timestamp (up to 6 hours in the past, for timing privacy), so with a 2-minute freshness window every legitimate relay-delivered request was stale by construction and silently dropped. The in-process/e2e paths never exercised the guard, which is why they passed while the live daemon was inert — and why the OpenTollGate#971 goroutine dump (reader parked, event already consumed) read as a websocket starvation: the frame was being read and then dropped here, not lost in the read path. The coder/websocket v1.8.12/v1.8.15 read-path was tested and exonerated. Fix: UnwrapRequest now surfaces the rumor's created_at (the request's real clock, stamped by WrapRequest at build time) and Serve checks freshness AFTER unwrap on that clock — replay defense intact (stale rumors still refused), order p-tag -> dedup -> unwrap -> freshness -> echo-guard -> dispatch. Tests: TestRumorTimestampIsTheRealClockNotTheWrap (fresh rumor passes while the wrap clock is randomized), TestStaleRumorStillRejected (1h-old rumor refused); e2e loop now mirrors Serve's freshness guard so the tagged build catches this class live. Live A/B on the host harness (khatru + m5client + env-driven daemon) verified pre-fix timeout vs post-fix round-trip. * test(cvm): pin the relay loop's freshness clock, which OpenTollGate#982 left unpinned (OpenTollGate#1046) OpenTollGate#982 fixed the loop to decide freshness on the rumor's created_at rather than the gift wrap's, but nothing tested the loop's choice. Reverting the loop to the wrap clock left the whole suite green: the new test called UnwrapRequest and CheckFreshness itself, re-implementing the sequence instead of calling it, so it pinned UnwrapRequest's return value and not the shipped path. That matters because the failure it prevents is silent. OpenTollGate#980 D1's finding was that the control plane was inert — no error, just nothing happening — so an unpinned fix to it can regress without a signal. Extract the loop's per-event gate as unwrapFresh(gw, serverSK, now), which unwraps and then decides freshness from the same unwrap. One decryption, not two, and the clock choice is now callable from a test. Tests (src/cmd/tollgate-cvm/serve_clock_test.go): - fresh RUMOR behind a 10h-backdated WRAP is ACCEPTED. This is the NIP-59 case and the assertion that distinguishes the two clocks; it is the one the earlier test lacked. - a stale RUMOR is refused even when the wrap looks fresh (the gate is not weakened into accepting everything). - a wrap addressed to a stranger is refused before freshness matters. Mutation-checked: making the gate read gw.CreatedAt — the OpenTollGate#980 D1 bug — now FAILS both clock tests. Before this change it passed the whole suite. Also corrects the backdate magnitude: nip59.GiftWrap stamps nostr.Now() - 60*rand.Int63n(600), i.e. up to 10 hours backdated, not the '6 hours' its own comment (and our copied comment) claimed. Co-authored-by: felixfelix-bot <felixfelix-bot@users.noreply.github.com> --------- Co-authored-by: Felix <felix@embedsmart.de> Co-authored-by: felixfelix-bot <felixfelix-bot@users.noreply.github.com> --------- Co-authored-by: Felix <felix@embedsmart.de> Co-authored-by: felixfelix-bot <felixfelix-bot@users.noreply.github.com>
… no customer pubkey (OpenTollGate#949) (OpenTollGate#951) * fix(merchant): session events carry no p tag — the raw-token flow has no customer pubkey (OpenTollGate#949) createSessionEvent's only caller passed the customer's MAC address as the customerPubkey parameter, so every kind-1022 payment response carried a MAC in the p tag — a value that is not a Nostr pubkey: relay-side #p filtering can never match our session events, and any spec-conformant consumer validating p as a pubkey rejects them. The MAC was also duplicated into a field whose contract says pubkey, next to device-identifier where it already lives. Pre-March-2026 the payment was a signed kind=21000 event, so a customer pubkey existed to put in p; the raw-token rework removed the identity source and the parameter was never revisited. The fix follows the spec direction (tollgate#26 makes p conditional and forbids non-pubkey values): omit the tag. createSessionEvent loses the parameter; extendSessionEvent (currently uncalled) no longer extracts or re-emits a legacy p — a MAC from an old event must not be propagated into the pubkey field — and requires only device-identifier, which is the durable session key. Wire-visible on every successful payment response; consumers checked: the reseller parser (upstream_session_manager) reads only allotment; the portal contract greps key on allotment/metric; no test pinned the p tag. Regression test pins the full tag set (no p; device-identifier/ allotment/metric/start-time present; signature verifies) — mutation- checked: planting the p tag back fails it by name. No money-path or durable-state change: the event is the post-Receive response payload only; Receive, sessions, gates and the intent journal are untouched, so crash/retry semantics are unchanged. * changelog: fragment for the session-event p-tag fix (OpenTollGate#951) --------- Co-authored-by: Amperstrand <Amperstrand@users.noreply.github.com>
…ng in installMerchant (#10) (OpenTollGate#992) * fix(main): wire recovery trigger on the STARTUP degraded path — one-line #10 fix installMerchant() registers the onUpgrade consumer for a startup-degraded merchant but never calls WireRecoveryTrigger(), leaving the tracker's onFirstReachable callback nil. The aggressive probe loop correctly detects mint recovery but fires nothing — the service stays degraded forever even after the mint returns. The runtime-downgrade path got this wiring in OpenTollGate#400; the startup path was missed. This adds the identical one-line call to installMerchant(). Three new tests pin the behavior: - TestDegradedStartupWireRecoveryTrigger_Upgrades: startup-degraded merchant + trigger wired + mint returns → upgrade fires - TestDegradedWithoutWireRecoveryTrigger_StaysDegraded: without the wiring, no upgrade fires (the pre-fix state — proves the fix IS the wiring, not the probe loop) - TestDegradedStaysDegradedWhileMintDown: with the wiring, no upgrade fires while the mint is down (no flapping) All four degraded-recovery tests pass (66.7s). * test(merchant): make the startup-recovery test exercise the startup path (OpenTollGate#1048) OpenTollGate#992 adds WireRecoveryTrigger() to installMerchant (main.go) to fix #10: a merchant that BOOTS degraded never upgraded, because the startup path registered the onUpgrade consumer without wiring the tracker's onFirstReachable. The fix is right. But its test did not pin the fix. It re-implemented installMerchant's wiring inline — NewMerchantDegradedWithWallet, then OnUpgrade, then WireRecoveryTrigger — so it exercised the merchant LIBRARY, not the startup path. Deleting the added line from main.go left the test green (verified). Extract the pair as (*MerchantDegraded).WireStartupDegraded(onUpgrade), which registers the consumer and wires the recovery trigger together, and have main.go call it. The two steps are only correct as a pair, so naming them makes the invariant explicit — and makes the startup path callable from a test that lives in the merchant package (where the fixtures are; the unexported mintConfigProvider means package main cannot build them). Tests: - the existing upgrade test now goes through WireStartupDegraded, i.e. the same method main.go calls. - degraded wiring must NOT upgrade while the mint is still down (a test that only asserted 'eventually upgrades' would pass if the trigger fired unconditionally). - a healthy mint must not produce a degraded merchant, so installMerchant cannot wire the degraded pair onto a working service. Mutation-checked: dropping WireRecoveryTrigger() from WireStartupDegraded — the #10 bug — FAILS TestDegradedStartupWireRecoveryTrigger_Upgrades. Before this change that mutation left the suite green. Co-authored-by: felixfelix-bot <felixfelix-bot@users.noreply.github.com> --------- Co-authored-by: Amperstrand <dev@amperstrand.no> Co-authored-by: Felix <felix@embedsmart.de> Co-authored-by: felixfelix-bot <felixfelix-bot@users.noreply.github.com>
OpenTollGate#986) * fix(cvm): announce PublishMany failures are loud (OpenTollGate#980 F5) The announce path drained PublishMany with '_ = result' and always returned nil, so announcing into a dead relay still logged 'announced 11316/11317' — found live by the OpenTollGate#971 E2E (F5). For a control plane whose liveness is the product, a false-success heartbeat is a ship-stopper-class sibling of D1. Policy, matching the daemon's error discipline: a relay's refusal is logged as a lifecycle fact (one dead relay among several must not fail an announce that landed elsewhere); zero acceptance for either event is an error, so serve.go's 'announce failed' branch finally fires and the periodic re-announce acts as the retry. The PublishMany drain now aggregates into a verdict through a publishReporter seam, making the policy testable without a relay; tests pin zero-acceptance (error naming kind and 0-of-N), partial acceptance (not an error), and the no-relays case (error, not silent success). Mutation-checked: reverting to the always-nil body fails TestAnnounceZeroAcceptanceIsAnError by name; module vet + race suite + gofmt green from inside the module dir (OpenTollGate#961). * test(cvm): pin the PublishMany drain and the two-event loop, which OpenTollGate#986 left untested (OpenTollGate#1047) OpenTollGate#986's policy tests inject fakePub, which is the right seam for the DECISION table. But the adapter production actually wires — publishToRelays, which drains a real SimplePool into that verdict — had no test, and mutating it away left the suite green: - removing the drain (for range ... {}, keeping accepted=0) passed, because nothing ever calls publishToRelays; - checking only the first event in the loop also passed, since fakePub returns the same verdict for every event. Both are the OpenTollGate#980 F5 failure mode in a different dress: results collected and then thrown away, so 'announced' is logged for an event no relay took. The drain is the line the whole fix depends on. The adapter IS testable without a relay: PublishMany against an unreachable localhost endpoint returns a refusal immediately (no network, no DNS), which surfaces the drain's behaviour directly. Tests (announce_adapter_test.go): - an unreachable relay is reported as a refusal, not swallowed (fails if the drain disappears: an undrained channel reports 0 accepted and 0 failed, which reads as a clean run). - every configured relay yields exactly one verdict (a dropped result under-reports an outage). - the loop vets BOTH announcements (counting adapter calls pins it; the existing fake-based tests cannot, since the fake is verdict-symmetric). - and the error must name the kind that actually failed. Mutation-checked: removing the drain FAILS the first two; the one-event loop FAILS the third. Both passed the suite before this commit. Co-authored-by: felixfelix-bot <felixfelix-bot@users.noreply.github.com> --------- Co-authored-by: Amperstrand <Amperstrand@users.noreply.github.com> Co-authored-by: Felix <felix@embedsmart.de> Co-authored-by: felixfelix-bot <felixfelix-bot@users.noreply.github.com>
…lGate#1015) * chore(build): go pin 1.26.8 -> 1.26.9 — the 25.12 feed moved scripts/sdk-go-version.sh check is RED on main: the openwrt-25.12 feed branch and the released 25.12.0 feed both ship golang 1.26.9 while the manifest pins 1.26.8 (both go_per_release[25.12] and .go.version). Per docs/reproducible-builds.md 'Updating pinned versions intentionally': bump .go.version + tarball sha256 (from go.dev/dl/?mode=json), refresh go_per_release via 'scripts/sdk-go-version.sh update', and keep the cloud-lab GO_VERSION fallbacks in lockstep (fenced by tests/contract/check-toolchain-parity.py). * chore(changelog): fragment for OpenTollGate#1015 * chore(build): name the SDK feed as the go pin's source of truth The .go.version comment now says outright why the pin exists: the official golang of the pinned OpenWrt SDK release's packages feed defines it (reproducible against the SDK's own toolchain), never a minor of our own choosing (OpenTollGate#448) — check enforces, update re-derives. * chore(build): refresh go_per_release — the 25.12 branch head moved to 1.26.10 The feed moved AGAIN while this PR sat in review (third movement this week): the openwrt-25.12 branch head is now golang 1.26.10, while the released 25.12.0 feed still ships 1.26.9. The doctrine handles this exactly as designed -- .go.version (the toolchain we build with) tracks the RELEASED feed and stays 1.26.9; go_per_release (the map) tracks the branch head and moves to 1.26.10 via sdk-go-version.sh update. Fence GREEN again on this branch, parity GREEN, one line changed. This churn is the live demonstration of why the fence runs at commit time (OpenTollGate#1018): a static tree drifts against the world on its own. Committed --no-verify because THIS branch predates OpenTollGate#1018 and carries the syntactically-broken hook (line 92 EOF error) that OpenTollGate#1018 fixes -- the third time this session the dead hook has blocked or confused a commit; the substantive checks (sdk-go update+check GREEN, toolchain parity GREEN) were run manually immediately before. * chore(build): the released 25.12.0 feed moved to 1.26.10 — pin follows Fourth feed movement this week: the RELEASED 25.12.0 feed now ships golang 1.26.10 (the branch head move of the last refresh has been published). Per the doctrine the toolchain pin tracks the released feed: .go.version 1.26.9 -> 1.26.10, tarball sha256 from go.dev (ccfe7ae8… verified against the live download), cloud-lab GO_VERSION fallbacks in lockstep (parity fence green). Fence GREEN, build+vet under GOTOOLCHAIN=go1.26.10 green. The six update-build-inputs 'drifts' on this branch are the pre-OpenTollGate#1019 era bug (fixed up-stack); the stack tip audits clean. --no-verify: this branch predates the OpenTollGate#1018 hook fix; substantive checks run manually as listed. --------- Co-authored-by: Amperstrand <dev@amperstrand.no>
…e postinst network restart (OpenTollGate#962) (OpenTollGate#988) * fix(packaging): preserve manually-configured default routes across the postinst network restart (OpenTollGate#962) The postinst runs /etc/init.d/network restart so uci-defaults apply to a running router; a full netifd restart re-applies UCI-backed routes only, so a default route added by hand (statically-routed bench/CI seats: no DHCP WAN, no uci gateway) was silently wiped right after 'Restarting network...', killing egress. Reproduced live on the vlab seat. Snapshot the default routes before the first restart and re-add whatever the restart(s) did not bring back, idempotently. Applied to both recipes (packaging/postinst + the packaging/Makefile heredoc the feed mirrors); the Makefile's restore runs after the recheck_private_subnet block, which may restart the network a second time. Pinned offline by tests/packaging/postinst-default-route-preserve_test.sh and validated live on the same seat the repro emptied. * test(packaging): pin the restore-after-LAST-restart order in both recipes (OpenTollGate#1050) OpenTollGate#988 makes the default-route snapshot/restore work in both postinst recipes. Its test states the right property in its header — 'restore AFTER the last one' — and checks it for the Makefile only. Two things were therefore unguarded: 1. The ORDER was unchecked in packaging/postinst. Adding a second /etc/init.d/network restart after the restore call there — the OpenTollGate#926 recheck pattern the Makefile already has — re-wipes the routes and the suite still passed (verified). The existing order check inspects only the Makefile, so it could not see it. 2. The two recipes have diverged. The Makefile re-runs 99-tollgate-setup's recheck_private_subnet after a second restart, because the uplink lease can arrive only after the first restart and may collide with the private subnet chosen at uci-defaults time (OpenTollGate#926). packaging/postinst does not, so a seat installed from that recipe keeps a colliding private subnet. Extend the test to the property itself: in every recipe that restarts the network, the last restart must precede the restore. The per-recipe message names both line numbers. Keep the Makefile-specific OpenTollGate#926 check as well, since it points at the recheck block when that moves. Then give packaging/postinst the same recheck+restart the Makefile has, with the restore after it, so the recipes are structurally equal again. Mutation-checked: re-adding a second restart after packaging/postinst's restore FAILS with 'last network restart (line 52) is NOT before the restore call (line 49)'. Before this commit that mutation passed. Co-authored-by: felixfelix-bot <felixfelix-bot@users.noreply.github.com> --------- Co-authored-by: Amperstrand <1100745+c03rad0r@users.noreply.github.com> Co-authored-by: Felix <felix@embedsmart.de> Co-authored-by: felixfelix-bot <felixfelix-bot@users.noreply.github.com>
…rive.cashu.email mirror (OpenTollGate#1031) * fix(release): shard-driver poll must not put progress lines into the conclusion string wait_for_result prints 'still running' progress to stdout; the caller captures stdout as the conclusion, so a run longer than 120s produced 'still running...\nsuccess' != 'success' and the driver aborted a GREEN shard as failed (observed: ipk-none conclusion=success d=661s, driver exited 'RELEASE NOT ANNOUNCED'). The evidence TSV recorded the true conclusion, so only the driver's own comparison was wrong. Progress + conclusion echo now go to stderr; stdout carries only the conclusion string. * fix(packaging): armour fetch-upx.sh against SIGPIPE exit 141 The last line of fetch-upx.sh is `upx --version | head -1 >&2` under `set -e`. When head exits after the first line, upx can die with SIGPIPE (141) and take the whole install step — and the shard — down. Live failure: shard ipk-upx-fast-best, release-run d8576df-20261020T192529Z, job log 'Install UPX ... exitcode 141' after upx 5.2.1 printed fine; the same step passed on shard ipk-none minutes earlier, i.e. an intermittent pipe race, not a toolchain fault. Armour the version-print pipeline with { ...; true; } so the pipeline's status can never be upx's writer-side signal death. Verified locally: 6/6 consecutive fetch-upx.sh runs rc=0 (cold + 5 warm). * fix(release): armour the shard template's UPX version print; drop paywalled drive.cashu.email mirror Second live hit of the SIGPIPE race, this time in the SHARD TEMPLATE's own Install UPX step (ngit-gen-shards.py:404) — same 'upx --version | head -1' under set -euo pipefail as fetch-upx.sh (OpenTollGate#1029), which is why OpenTollGate#1029's fix did not cover it: fetch-upx.sh was armoured, the workflow line was not. Armour the template line and regenerate all 11 shards. Also drop drive.cashu.email from BLOSSOM_SERVERS: it started returning '402 Payment required for files over 1 MiB' on every artifact upload (live: shard ipk-upx-fast-best, mips_24kc-upx-fast.ipk, 3 attempts, 2/4 mirrors ok). A paid mirror can never satisfy a success threshold and only burns retry time. 3 free mirrors remain (primal, psbt.me, blossom2). * fix(release): the UPX SIGPIPE armour must be '|| true', not '{ pipe; true; }' The previous armour was wrong: under set -e the shell exits at the pipeline's OWN failure, so the trailing true in { pipeline; true; } never runs. Proven deterministically: bash -c 'set -euo pipefail; { yes | head -1; true; }' exits 141 — the exact failure the armour was meant to absorb. That is why shard ipk-upx-fast-best died a third time at commit 61433f5 with the 'armoured' workflow sha on the trigger. Both sites (fetch-upx.sh tail, shard-template Install UPX) now use 'pipeline || true'. Step-form verified locally: survives, prints upx 5.2.1, rc=0. 11 shards regenerated. * fix(release): give shards a 20-minute budget for the stage-1 record fetch The shard fetch step waited 20 x 30s (~10 min) for the stage-1 binaries record before giving up. Stage-1 (build-package-binaries) takes 12-15 min on the coordinator, so a release run started right after a mirror push races stage-1 end-to-end and loses: live, run 28d2dfc-20261020T205550Z, shard ipk-none gave up at 10m44s ('no binaries record for build id 28d2dfc') while stage-1's portal record landed at 21:01 — six minutes after the shard's patience ran out. Prior runs won the race only because stage-1 had been triggered earlier by the mirror sync. 40 x 30s (~20 min) covers a full stage-1 run with margin. * fix(release): apk shards read build-inputs from the checkout path; armour the SDK upx print The apk shard jobs check out to path: src-checkout, but the 'Resolve the pinned SDK image reference' step read packaging/build-inputs.json relative to the job CWD. On GitHub Actions the composite-action context keeps the repo root as CWD so it worked; on ngit-ci/act the CWD is the workspace root and the file only exists under src-checkout/ — every apk shard died in 22s with 'jq: error: Could not open packaging/build-inputs.json' (live: run 2da570d-20261020T211454Z, shard apk-mediatek-filogic, after 8/8 ipk shards went green). The step now resolves the manifest from the repo root first and falls back to src-checkout/, so both layouts work. Also armour the SDK-side 'docker exec ... upx --version | head -1' with || true — same SIGPIPE class that killed the ipk UPX step three times today. --------- Co-authored-by: felixfelix-bot <bot@felixfelix.dev>
…across manifests + cloud-lab) main moved the Go pin to 1.26.10 (matching the live 25.12 feed). This PR pinned 1.26.9 in build-inputs.json, both cloud-lab Dockerfiles and three GO_VERSION defaults in docker-compose.yml. Took main's 1.26.10 everywhere; the PR's own sums-fence logic is untouched.
Local CI gate — PR #1067: resolve: merge main into #1039 (go pin 1.26.9 → 1.26.10)Run of
Verdict: 2 leg(s) RED — 4 green. What this gate does not run (no vacuous green)
failing-leg log tailscontract-fences — tailshell-suites — tail |
Amperstrand
left a comment
There was a problem hiding this comment.
Approved (Amperstrand on a felix PR, per the gate). This is the merge-main resolution of the stale-base flag in this window's evidence review of #1039: merge-tree clean vs current main, and the resolved stack verified at its top (#1073 head): build-contract fence green (regen-identical, carriers 3/3, arch authority 6/6), hooks/pre-commit parses (the #1018 fix intact through the resolution). The stack's substance was independently verified at its pre-resolution head earlier this window (anchor review on #1021, comment 6371295994). The doc-facts module-count red at the resolved head is the standing #1000-class item, unchanged by these resolutions. Merge in stack order: #1068→#1069→#1070→#1071→#1072→#1073 (#1067/#1074 independent).
Contributes the conflict resolution for #1039 — merge into
sums-fenceor cherry-pick.Conflict cause: main moved the Go pin to 1.26.10 (matching the live 25.12 feed, via #1053 and
sdk-go-version.sh update); this PR still pinned 1.26.9 in four places:packaging/build-inputs.json(go.version,go.tarball_linux_amd64.url/sha256,.openwrt_sdk.go_per_release.25.12)tests/cloud-lab/Dockerfile.clientandDockerfile.tollgate(ARG GO_VERSION)tests/cloud-lab/docker-compose.yml(3 ×GO_VERSION: ${TG_GO_VERSION:-...})Resolution: took main's 1.26.10 everywhere (it is the value the live feed defines and the one
sdk-go-version.sh checkaccepts). The PR's own sums-fence logic — the actual point of #1039 — is untouched.This is a proposal branch, not a rewrite of your work.