Site: the downloads snapshot is written only on SITE_DOWNLOADS_REFRESH=1 or in CI; a CI check against scripts writing into other worktrees

On 6 October 2026 the pre-push hook's site build fetched the live downloads index (0.3.14 since 17:47Z) and rewrote site/downloads.json and the stamped pages in five worktrees that had nothing to do with the release, as uncommitted edits to tracked files. A plain build and the hook now read live and warn when the snapshot lags; the ship step refreshes it with the flag and commits it. tools/ci/no-foreign-tree-writes.sh fails a script that builds a path from a worktree name, a glob over Projects or a worktree-list loop that writes; the eight absolute defaults into the shared checkout are listed as warnings until they move to env-only defaults. The journey, bench and index rebuilt from the merged tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-josh 2026-10-06 19:19:58 +01:00
parent c16af2cc2c
commit 063bbca26a
6 changed files with 133 additions and 30 deletions

View file

@ -77,6 +77,8 @@ jobs:
run: bash tools/ci/second-engine-check.sh
- name: no playbook quits, pauses or resumes the installed app (self-test first, then the tree)
run: bash tools/ci/playbook-quit-check.sh --self-test && bash tools/ci/playbook-quit-check.sh
- name: no script writes into another worktree or walks Projects (tools/ci/no-foreign-tree-writes.sh)
run: bash tools/ci/no-foreign-tree-writes.sh --self-test && bash tools/ci/no-foreign-tree-writes.sh
- name: the signer is never piped into head
run: bash tools/ci/signer-pipe-check.sh
- name: bash bodies in PowerShell job scripts pass bash -n, the lost-quote class (self-test first, then the tree)

File diff suppressed because one or more lines are too long

View file

@ -275,6 +275,11 @@ function stampProduct(html, file) {
const TESTNET_OPEN = false;
const DL_HOST = 'https://dl.igneum.network';
const DL_SNAPSHOT = join(here, 'downloads.json');
// The snapshot is a tracked file: the build rewrites it only on an explicit refresh (SITE_DOWNLOADS_REFRESH=1 or
// --refresh-downloads, the ship pipeline's step, committed with the release), or in CI (a throwaway checkout). A plain
// local build, and the pre-push hook, read live but never write: on 6 October 2026 the hook's build stamped 0.3.14's rows
// into five worktrees that had nothing to do with the release, as uncommitted edits to tracked files.
const DL_REFRESH = process.env.SITE_DOWNLOADS_REFRESH === '1' || process.argv.includes('--refresh-downloads') || !!process.env.CI;
async function loadDownloads() {
const snapshot = existsSync(DL_SNAPSHOT) ? JSON.parse(readFileSync(DL_SNAPSHOT, 'utf8')) : { files: {} };
if (process.env.SITE_DOWNLOADS_OFFLINE) return { ...snapshot, source: 'snapshot (offline)' };
@ -286,7 +291,8 @@ async function loadDownloads() {
const live = await r.json();
if (!live || typeof live.files !== 'object') throw new Error('no files object');
for (const [k, f] of Object.entries(live.files)) if (!/^\/public\/[a-z0-9.-]+$/.test(f.alias) || !/^\d+\.\d+\.\d+$/.test(String(f.version)) || !(f.size > 0)) throw new Error(`bad entry ${k}`);
writeFileSync(DL_SNAPSHOT, JSON.stringify(live, null, 2) + '\n');
if (DL_REFRESH) writeFileSync(DL_SNAPSHOT, JSON.stringify(live, null, 2) + '\n');
else if (JSON.stringify(live) !== JSON.stringify(snapshot)) console.warn('downloads: the live index differs from site/downloads.json; the release step refreshes it (SITE_DOWNLOADS_REFRESH=1 node site/build.mjs)');
return { ...live, source: 'live' };
} catch (e) {
console.warn(`downloads: using the snapshot (${e.message})`);

File diff suppressed because one or more lines are too long

View file

@ -50,6 +50,31 @@
}
],
"log": [
{
"date": "2026-10-06",
"text": "Counter ASIC 3.0 item 2: the per-day derivation",
"short": "Counter ASIC 3.0 item 2"
},
{
"date": "2026-10-06",
"text": "Counter ASIC 3.0 item 8: program work in the latency shadow",
"short": "Counter ASIC 3.0 item 8"
},
{
"date": "2026-10-06",
"text": "Counter ASIC 3.0 item 6: the reserve families' step costs",
"short": "Counter ASIC 3.0 item 6"
},
{
"date": "2026-10-06",
"text": "Counter ASIC 3.0 gate run, the hash side",
"short": "Counter ASIC 3.0 gate run, the hash side"
},
{
"date": "2026-10-06",
"text": "16:01Z: Ember run 6 on PC 1",
"short": "16:01Z: Ember run 6 on PC 1"
},
{
"date": "2026-10-06",
"text": "00:4xZ, the empty `/api/state` reply",
@ -224,31 +249,6 @@
"date": "2026-10-04",
"text": "First machine on the Igneum Miner app: PC 2's RTX 5090 at 118 MH/s, via Setup.exe",
"short": "First machine on the one-click app: a 5090 at 118 MH/s"
},
{
"date": "2026-10-04",
"text": "PC 2 at the 14:20 boundary: a worker stuck on the previous epoch",
"short": "PC 2 at the 14:20 boundary: a worker stuck on the previous epoch"
},
{
"date": "2026-10-04",
"text": "Ledger M30: the block and transaction floods grew the node by 256 MiB per epoch roll, fixed by sharing the PoW cache across the epochs of a day",
"short": "Ledger M30: the block and transaction floods grew the node by 256…"
},
{
"date": "2026-10-04",
"text": "Round-4 consensus items F23, F24, G12, X18 and M31: unit tests and fast-time 3-node runs against the control",
"short": "Round-4 consensus items F23, F24, G12, X18 and M31"
},
{
"date": "2026-10-04",
"text": "The software dev fee measured on a test network: 9 fee blocks in 785, the chain and the miners' counters agree",
"short": "The software dev fee measured on a test network"
},
{
"date": "2026-10-04",
"text": "Miner fault guards and the app watchdog measured against a fake worker",
"short": "Miner fault guards and the app watchdog measured against a fake worker"
}
]
}

View file

@ -0,0 +1,41 @@
#!/usr/bin/env bash
# A script writes only under its own repository (git rev-parse --show-toplevel of its own path), the downloads folder
# and the scratch dirs. It never builds a target path from another worktree's name, from a list of worktrees or from a
# walk over $HOME/Projects. Ruled 6 October 2026: five worktrees held 0.3.14's site rows as uncommitted edits to tracked
# files after the pre-push hook's site build fetched the live downloads index and rewrote its snapshot in whatever tree
# the push ran from (site/build.mjs now writes the snapshot only on SITE_DOWNLOADS_REFRESH=1 or in CI). Runs in CI and
# locally; --self-test shows it firing.
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
# a path built from a worktree list or a Projects walk: `git worktree list` piped into a loop with a write, or
# $HOME/Projects, ~/Projects, /Users/*/Projects used in a path (comments and docs excluded; strings in tests excluded)
PAT='(\$HOME|~|/Users/[a-z]+)/Projects/igneum-wt-|(\$HOME|~|/Users/[a-z]+)/Projects/(\*|igneum\*|igneum-wt-\*)|git worktree list[^|]*\|[^#]*(cp|mv|tee|>|writeFileSync|install )'
# the shared checkout as an absolute default (/Users/<user>/Projects/igneum/...) is the lesser class: a read of a binary
# or a script there, overridable by an environment variable. Listed as a warning; the row of 6 October 2026 moves each
# to an env-only default (no fallback path) and this pattern then joins PAT.
WARN='/Users/[a-z]+/Projects/igneum/'
check_file() {
local f="$1" bad=0
while IFS= read -r line; do
local code="${line%%#*}"
[[ "$code" =~ ^[[:space:]]*(//|\*|/\*) ]] && continue
[[ "$code" =~ $PAT ]] || continue
echo "foreign-tree: $f builds a path into another worktree or a Projects walk: ${line:0:140}"; bad=1
done < "$f"
return $bad
}
if [ "${1:-}" = "--self-test" ]; then
t="$(mktemp -d)"; trap 'rm -rf "$t"' EXIT
printf 'cp out.json "$HOME/Projects/igneum-wt-other/site/downloads.json"\nfor d in ~/Projects/igneum*/; do echo "$d"; done\n' > "$t/bad.sh"
printf 'for w in $(git worktree list | cut -d" " -f1); do cp x "$w/site/x"; done\n' > "$t/bad2.sh"
printf 'ROOT="$(git rev-parse --show-toplevel)"; cp out.json "$ROOT/site/downloads.json"\n# ~/Projects/igneum is fine in a comment\n' > "$t/good.sh"
check_file "$t/bad.sh" && { echo "self-test failed: bad.sh passed"; exit 1; }
check_file "$t/bad2.sh" && { echo "self-test failed: bad2.sh passed"; exit 1; }
check_file "$t/good.sh" || { echo "self-test failed: good.sh flagged"; exit 1; }
echo "self-test passed: a Projects path fails, a worktree-list loop with a write fails, an own-toplevel write passes"; exit 0
fi
fail=0
while IFS= read -r f; do grep -nE "$WARN" "$f" | grep -vE '^[0-9]+:\s*(#|//)' | sed "s|^|foreign-tree (warning, env-only default owed): $f:|" | cut -c1-200; done < <(git ls-files 'tools/**' 'packaging/**' 'infra/**' 'relay/**' | grep -E '\.(sh|mjs|js|py|ps1)$' | grep -v '^tools/ci/no-foreign-tree-writes.sh$')
while IFS= read -r f; do check_file "$f" || fail=1; done < <(git ls-files 'tools/**' 'packaging/**' 'site/*.mjs' 'infra/**' 'relay/**' | grep -E '\.(sh|mjs|js|py|ps1)$' | grep -v '^tools/ci/no-foreign-tree-writes.sh$')
[ "$fail" = 0 ] && echo "foreign-tree: every script writes under its own toplevel"
exit $fail