igneum/packaging/windows/fetch-ci-artifacts.sh
igneum-labs 911e78586f Public-testnet readiness: fee floors and pgas table analysis, testnet identity doc, G14 rewrite plan with dry run, G13 signed build inputs, testnet terms, MetaMask page
- docs/analysis/base-fee-floor.md: the base-fee floors (100 gwei per gas, 10,000 gwei per pgas), B_p 120,000 and
  S_p 30,000 pgas, the calibrated v1 pgas table (intrinsic 300, modexp 10 + 1 per 10 bytes) from the measured 44
  cycles per EVM gas and 9 cycles per pgas, with the arithmetic and a stated price assumption; spec 05 section 5.10.
  The parameters are implemented on the node fork branch testnet-params (vendor, not in this repository).
- docs/testnet/README.md: igneum-testnet-1 (chain id 4462, ports 268xx, frozen genesis 2026-10-05T00:00:00Z with
  its message and hash, mainnet finality window, every switch from genesis, no override file) and the reset policy.
  Every value proposed, for the morning sign-off.
- docs/plans/history-rewrite.md: G14, the exact git-filter-repo pass, the dry run on a throwaway mirror clone (0
  identity hits, 0 secrets, every stamp +0000, 312 commits), what breaks and the order for the morning.
- G13: app/igneum-app/src/inputs.rs and igneum-ota-sign sign-inputs / verify-inputs; push-inputs.sh signs
  payload-inputs.json with the OTA key and pins the node commit (packaging/windows/node-source.pin);
  windows.yml verifies the signature with the embedded key, the zip, every file and the pin before building and
  uploads the verified record; fetch-ci-artifacts.sh signs the update manifest only with --sign-manifest <run-id>
  after re-verifying that run's inputs. test-inputs-signing.sh (16 cases) and tools/ci/check-workflow-shell.mjs.
- site: testnet terms on the download section, wallet.html (wallet_addEthereumChain, chain ids 4462/4463, IGN, 18
  decimals), the litepaper's app paragraph (MetaMask and the coming Igneum Wallet, no hardware wallet) and the
  miner fee sentence (no protocol fee; optional 1% in the miner software, off with one flag). node site/build.mjs
  and the link check pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 22:54:56 +00:00

112 lines
8.5 KiB
Bash
Executable file

#!/usr/bin/env bash
# Pulls the Windows installer and payload from a green run of .github/workflows/windows.yml on master and copies
# them into the downloads folder (dl/<token>/), where the other packages live. Run on the Mac:
#
# packaging/windows/fetch-ci-artifacts.sh [--deploy] [--sign-manifest] [run-id]
#
# Without --deploy it prints the deploy command for the main session to run; with --deploy it deploys the folder with
# the Vercel CLI itself. A run id (gh run list) picks a specific run instead of the latest green one.
#
# The over-the-air update manifest (packaging/ota/publish-manifest.sh, Windows entry) is NOT touched unless
# --sign-manifest is given (review round 4, R4.5.2, ledger G13: signing used to be automatic from "the latest green
# run"). --sign-manifest needs an explicit run id, and before it signs anything it downloads that run's
# igneum-windows-inputs artifact (the payload-inputs.json the runner verified, its signature and the runner's record)
# and re-verifies on this Mac: the Ed25519 signature against ~/.config/igneum/ota-signing-key.pub, the pinned node
# commit against packaging/windows/node-source.pin AT THE RUN'S COMMIT, the run's branch (master) and event (push or
# workflow_dispatch), and that the runner's record names this run, this commit and this key. Any failure stops
# before the signature. OTA_NOTES= sets the changelog line (default: the version and the run id).
# Reads ~/.config/igneum/dl-token, ~/.config/igneum/dlsite-dir (IGNEUM_DLSITE overrides) and the gh login, which must
# be igneum-labs (gh auth switch --user igneum-labs).
set -euo pipefail
REPO="igneum-network/igneum"
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
DEPLOY=0
SIGN=0
RUN_ID=""
for a in "$@"; do
case "$a" in --deploy) DEPLOY=1 ;; --sign-manifest) SIGN=1 ;; --*) echo "unknown flag $a" >&2; exit 2 ;; *) RUN_ID="$a" ;; esac
done
if [ "${OTA_SKIP:-}" = 0 ]; then SIGN=1; fi # the old spelling; OTA_SKIP=1 (the default now) leaves the manifest alone
if [ "$SIGN" = 1 ] && [ -z "$RUN_ID" ]; then
echo "--sign-manifest needs the run id it signs (gh run list --repo $REPO --workflow windows.yml); the latest green run is never signed by default" >&2
exit 2
fi
TOKEN_FILE="$HOME/.config/igneum/dl-token"
DLSITE="${IGNEUM_DLSITE:-}"
[ -n "$DLSITE" ] || { [ -f "$HOME/.config/igneum/dlsite-dir" ] && DLSITE="$(tr -d '[:space:]' < "$HOME/.config/igneum/dlsite-dir")"; } || true
[ -f "$TOKEN_FILE" ] || { echo "no $TOKEN_FILE" >&2; exit 1; }
TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
DEST="$DLSITE/dl/$TOKEN"
[ -n "$DLSITE" ] && [ -d "$DEST" ] || { echo "no downloads folder: set IGNEUM_DLSITE or ~/.config/igneum/dlsite-dir (must hold dl/<token>/)" >&2; exit 1; }
gh auth status 2>&1 | grep -q 'Active account: true' || { echo "gh is not logged in" >&2; exit 1; }
gh auth status 2>&1 | grep -B1 'Active account: true' | grep -q 'igneum-labs' || { echo "gh active account is not igneum-labs: run gh auth switch --user igneum-labs" >&2; exit 1; }
if [ -z "$RUN_ID" ]; then
RUN_ID="$(gh run list --repo "$REPO" --workflow windows.yml --branch master --status success --limit 1 --json databaseId --jq '.[0].databaseId')"
[ -n "$RUN_ID" ] && [ "$RUN_ID" != "null" ] || { echo "no green windows.yml run on master yet" >&2; exit 1; }
fi
gh run view "$RUN_ID" --repo "$REPO" --json headSha,displayTitle,updatedAt,url,conclusion --jq '"run \(.url)\n\(.displayTitle)\n\(.headSha[0:12]) \(.updatedAt) \(.conclusion)"'
RUN_JSON="$(gh run view "$RUN_ID" --repo "$REPO" --json headSha,headBranch,event,conclusion,status)"
read -r HEAD_SHA HEAD_BRANCH RUN_EVENT RUN_CONCLUSION < <(printf '%s' "$RUN_JSON" | python3 -c 'import json,sys; r=json.load(sys.stdin); print(r["headSha"], r["headBranch"], r["event"], r["conclusion"])')
[ "$RUN_CONCLUSION" = success ] || { echo "run $RUN_ID concluded '$RUN_CONCLUSION', not success" >&2; exit 1; }
TMP="$(mktemp -d)"
gh run download "$RUN_ID" --repo "$REPO" --name igneum-windows-installer --name igneum-windows-payload --dir "$TMP"
SETUP="$(find "$TMP" -name 'Igneum-Miner-Setup-*.exe' | head -1)"
PAYLOAD="$(find "$TMP" -name 'igneum-windows-app.zip' | head -1)"
[ -n "$SETUP" ] && [ -n "$PAYLOAD" ] || { echo "the run has no installer or payload artifact" >&2; ls -R "$TMP"; exit 1; }
cp "$SETUP" "$DEST/"
cp "$PAYLOAD" "$DEST/igneum-windows-app.zip"
cat > "$DEST/igneum-windows-ci.json" <<JSON
{ "run": "https://github.com/$REPO/actions/runs/$RUN_ID", "installer": "$(basename "$SETUP")", "payload": "igneum-windows-app.zip", "fetched_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" }
JSON
rm -rf "$TMP"
echo "copied into $DEST:"
ls -la "$DEST/$(basename "$SETUP")" "$DEST/igneum-windows-app.zip"
# the console's Builds tab (relay/): one build event per fetched CI run; never fatal. CONSOLE_SKIP=1 leaves it to the
# caller (tools/ship-app.mjs posts one item for the whole cut).
[ "${CONSOLE_SKIP:-0}" = 1 ] || node "$(dirname "$0")/../../tools/console.mjs" post --kind build --title "Windows CI $(basename "$SETUP") fetched (run $RUN_ID)" --body "https://github.com/$REPO/actions/runs/$RUN_ID" >/dev/null 2>&1 || true
# the over-the-air manifest (packaging/ota): only with --sign-manifest, only for the named run, and only after the
# run's verified inputs manifest re-verifies here (G13). The Mac entry of the same version is carried over.
if [ "$SIGN" = 1 ]; then
PUB="$HOME/.config/igneum/ota-signing-key.pub"
SIGNER="$ROOT/app/igneum-app/target/release/igneum-ota-sign"
[ -f "$PUB" ] || { echo "no $PUB" >&2; exit 1; }
[ -x "$SIGNER" ] || (cd "$ROOT/app/igneum-app" && nice -n 19 cargo build --release -j 4 --bin igneum-ota-sign --quiet)
[ "$HEAD_BRANCH" = master ] || { echo "refusing to sign: run $RUN_ID is on branch '$HEAD_BRANCH', not master" >&2; exit 1; }
case "$RUN_EVENT" in push|workflow_dispatch) ;; *) echo "refusing to sign: run $RUN_ID was triggered by '$RUN_EVENT'" >&2; exit 1 ;; esac
INP="$(mktemp -d)"
gh run download "$RUN_ID" --repo "$REPO" --name igneum-windows-inputs --dir "$INP" || { echo "refusing to sign: run $RUN_ID has no igneum-windows-inputs artifact (the run verified no inputs manifest)" >&2; exit 1; }
IJSON="$(find "$INP" -name payload-inputs.json | head -1)"; ISIG="$(find "$INP" -name payload-inputs.json.sig | head -1)"; IREC="$(find "$INP" -name inputs-verified.json | head -1)"
[ -n "$IJSON" ] && [ -n "$ISIG" ] && [ -n "$IREC" ] || { echo "refusing to sign: the inputs artifact is incomplete" >&2; ls -R "$INP" >&2; exit 1; }
# the pin as it stood in the commit the runner built, from this clone (fetched if the commit is not here yet)
git -C "$ROOT" cat-file -e "$HEAD_SHA^{commit}" 2>/dev/null || git -C "$ROOT" fetch --quiet origin "$HEAD_SHA" || true
PIN="$(git -C "$ROOT" show "$HEAD_SHA:packaging/windows/node-source.pin" 2>/dev/null | tr -d '[:space:]')"
[ ${#PIN} = 40 ] || { echo "refusing to sign: commit ${HEAD_SHA:0:12} carries no packaging/windows/node-source.pin" >&2; exit 1; }
"$SIGNER" verify-inputs "$PUB" "$IJSON" "$ISIG" --node-commit "$PIN" || { echo "refusing to sign: the run's inputs manifest does not verify against $PUB and the pin at ${HEAD_SHA:0:12}" >&2; exit 1; }
FP="$("$SIGNER" fingerprint "$PUB" | sed -n 2p)"
python3 - "$IREC" "$RUN_ID" "$HEAD_SHA" "$FP" <<'PYREC'
import json, sys
rec = json.load(open(sys.argv[1]))
want = {"run_id": sys.argv[2], "head_sha": sys.argv[3], "key_fingerprint": sys.argv[4]}
bad = [k for k, v in want.items() if str(rec.get(k, "")) != v]
if bad:
print("refusing to sign: the runner's record disagrees on " + ", ".join(f"{k} (record {rec.get(k)!r}, expected {want[k]!r})" for k in bad), file=sys.stderr)
sys.exit(1)
print(f"inputs verified by the runner and again here: node commit {rec.get('node_commit')}, zip {rec.get('zip_sha256')}, key {rec.get('key_fingerprint')}")
PYREC
rm -rf "$INP"
SETUP_VERSION="$(basename "$SETUP" | sed -n 's/^Igneum-Miner-Setup-\(.*\)\.exe$/\1/p')"
echo "signing the update manifest for Windows $SETUP_VERSION over run $RUN_ID (${HEAD_SHA:0:12})"
"$HERE/../ota/publish-manifest.sh" --version "$SETUP_VERSION" --win "$DEST/$(basename "$SETUP")" --notes "${OTA_NOTES:-Windows build $SETUP_VERSION from CI run $RUN_ID}" --no-deploy
else
echo "update manifest untouched (pass --sign-manifest <run-id> to sign it after the inputs check)"
fi
if [ "$DEPLOY" = 1 ]; then
(cd "$DLSITE" && npx vercel@latest --global-config "$HOME/.config/igneum/vercel" deploy --prod --yes 2>&1 | grep -v "$TOKEN" || true)
echo "live: https://dl.igneum.network/dl/<token>/$(basename "$SETUP")"
node "$(dirname "$0")/../../tools/console.mjs" sync-dl >/dev/null 2>&1 || true
else
echo "deploy: cd $DLSITE && npx vercel@latest --global-config ~/.config/igneum/vercel deploy --prod --yes"
fi