#!/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//), 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 the stored login (~/.config/igneum/gh-user, default igneum-labs; gh auth switch --user ). 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//)" >&2; exit 1; } gh auth status 2>&1 | grep -q 'Active account: true' || { echo "gh is not logged in" >&2; exit 1; } stored="$(cat "$HOME/.config/igneum/gh-user" 2>/dev/null | tr -d '[:space:]')"; stored="${stored:-igneum-labs}" # the keyring entry's name gh auth status 2>&1 | grep -B1 'Active account: true' | grep -q -F "$stored" || { echo "gh active account is not the stored login $stored: run gh auth switch --user $stored" >&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" </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 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//$(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