From 4de1d63b39c57cace112cfecb31b5f3051080a48 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 08:42:49 +0700 Subject: [PATCH 1/6] build(fat): independent exact-run and PE signature gates for direct EXE previews --- scripts/verify-fat-direct-exe.py | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 scripts/verify-fat-direct-exe.py diff --git a/scripts/verify-fat-direct-exe.py b/scripts/verify-fat-direct-exe.py new file mode 100644 index 000000000..0d58c9f17 --- /dev/null +++ b/scripts/verify-fat-direct-exe.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Fail-closed FAT EXE release input verification; never publishes anything.""" +from __future__ import annotations + +import argparse +import hashlib +import json +import re +from pathlib import Path + +SHA40 = re.compile(r"[a-f0-9]{40}\Z") +RUNS = {"Build ARSAS", "Validate ARSAS Windows installer"} + + +def validate_run(run: dict, sha: str, workflow: str) -> None: + if not SHA40.fullmatch(sha): + raise ValueError("Expected exact 40-character lowercase source SHA") + if workflow not in RUNS: + raise ValueError("Unrecognized verified Windows workflow") + if (run.get("head_sha") != sha or run.get("name") != workflow + or run.get("status") != "completed" + or run.get("conclusion") != "success"): + raise ValueError("Run is unqualified or is not the exact requested revision") + if run.get("event") not in {"pull_request", "push", "workflow_dispatch"}: + raise ValueError("Unrecognized source-build event") + + +def check_exe(path: Path) -> str: + if not path.is_file() or path.stat().st_size < 1024 * 1024: + raise ValueError("Missing or unexpectedly small Windows executable") + with path.open("rb") as stream: + header = stream.read(64) + if len(header) != 64 or header[:2] != b"MZ": + raise ValueError("File is not a Windows PE executable") + pe_offset = int.from_bytes(header[0x3C:0x40], "little") + if pe_offset < 64 or pe_offset > 1024 * 1024: + raise ValueError("Invalid Windows PE header offset") + stream.seek(pe_offset) + if stream.read(4) != b"PE\x00\x00": + raise ValueError("Missing Windows PE signature") + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def main() -> None: + cli = argparse.ArgumentParser() + cli.add_argument("--run-json", type=Path, required=True) + cli.add_argument("--sha", required=True) + cli.add_argument("--workflow", choices=sorted(RUNS), required=True) + cli.add_argument("--exe", type=Path, required=True) + cli.add_argument("--output", type=Path, required=True) + args = cli.parse_args() + run = json.loads(args.run_json.read_text(encoding="utf-8")) + validate_run(run, args.sha, args.workflow) + digest = check_exe(args.exe) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps({ + "testedSourceSha": args.sha, "workflow": args.workflow, + "workflowRunId": run.get("id"), "buildEvent": run.get("event"), + "file": args.exe.name, "fileSizeBytes": args.exe.stat().st_size, + "sha256": digest, "channel": "public-prerelease-FAT-only", + "fieldAcceptance": False, "productionPromotion": False, + }, indent=2) + "\n", encoding="utf-8") + + +if __name__ == "__main__": + main() From 22fa50e6c75f53e7095ef798c5636ea6f66daa36 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 08:43:57 +0700 Subject: [PATCH 2/6] test(ci): negative exact SHA, failed run, non-PE and disguised ZIP rejected --- scripts/test-fat-direct-exe.py | 61 ++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 scripts/test-fat-direct-exe.py diff --git a/scripts/test-fat-direct-exe.py b/scripts/test-fat-direct-exe.py new file mode 100644 index 000000000..9d6ecc157 --- /dev/null +++ b/scripts/test-fat-direct-exe.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Offline regression for the manual FAT EXE publication guard.""" +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).with_name("verify-fat-direct-exe.py") +spec = importlib.util.spec_from_file_location("fat_direct_exe", SCRIPT) +guard = importlib.util.module_from_spec(spec) +spec.loader.exec_module(guard) + +SHA = "a" * 40 + + +def run(name="Build ARSAS", sha=SHA, status="completed", conclusion="success"): + return {"id": 123, "head_sha": sha, "name": name, "status": status, + "conclusion": conclusion, "event": "pull_request"} + + +class FatDirectExeGuardTests(unittest.TestCase): + def test_exact_qualified_build_and_installer(self): + guard.validate_run(run(), SHA, "Build ARSAS") + guard.validate_run(run("Validate ARSAS Windows installer"), + SHA, "Validate ARSAS Windows installer") + + def test_reject_incomplete_failed_stale_and_wrong_workflow(self): + variants = [run(sha="b" * 40), run(status="in_progress"), + run(conclusion="failure"), run(name="Unrelated workflow")] + for r in variants: + with self.subTest(r=r), self.assertRaises(ValueError): + guard.validate_run(r, SHA, "Build ARSAS") + with self.assertRaises(ValueError): + guard.validate_run(run(), "abc", "Build ARSAS") + + def test_check_pe_signature_and_sha(self): + with tempfile.TemporaryDirectory() as directory: + exe = Path(directory, "qualified.exe") + data = bytearray(1024 * 1024 + 1) + data[:2] = b"MZ" + data[0x3C:0x40] = (0x80).to_bytes(4, "little") + data[0x80:0x84] = b"PE\x00\x00" + exe.write_bytes(data) + import hashlib + self.assertEqual(hashlib.sha256(data).hexdigest(), guard.check_exe(exe)) + data[0x80] = 0 + exe.write_bytes(data) + with self.assertRaises(ValueError): + guard.check_exe(exe) + + def test_reject_zip_disguised_as_exe(self): + with tempfile.TemporaryDirectory() as directory: + exe = Path(directory, "fake.exe") + exe.write_bytes(b"PK\x03\x04" + b"\0" * (1024 * 1024)) + with self.assertRaises(ValueError): + guard.check_exe(exe) + + +if __name__ == "__main__": + unittest.main() From 8a1d681478e322175f309384beae8d2e0ab6b39c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 08:46:32 +0700 Subject: [PATCH 3/6] ci(fat): opt-in full-CI-gated direct EXE GitHub prerelease assets, no automatic publishing --- .github/workflows/fat-direct-exe.yml | 143 +++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 .github/workflows/fat-direct-exe.yml diff --git a/.github/workflows/fat-direct-exe.yml b/.github/workflows/fat-direct-exe.yml new file mode 100644 index 000000000..ab16d1e92 --- /dev/null +++ b/.github/workflows/fat-direct-exe.yml @@ -0,0 +1,143 @@ +name: Publish direct EXE FAT preview (manual only) + +# CI artifact downloads are always ZIP. Only a manually approved +# non-production GitHub prerelease can provide a direct raw .exe URL. +on: + workflow_dispatch: + inputs: + tested_sha: + description: CI-qualified exact 40-character source SHA + type: string + required: true + build_run_id: + description: Successful exact-SHA Build ARSAS workflow run ID + type: string + required: true + installer_run_id: + description: Optional successful exact-SHA installer run ID; 0 skips + type: string + required: true + default: '0' + public_preview_ack: + description: Type PUBLISH PUBLIC NONPRODUCTION FAT EXE to authorize public FAT-only assets + type: string + required: true + +permissions: + contents: write + actions: read + +concurrency: + group: fat-direct-exe-preview + cancel-in-progress: false + +jobs: + preview: + name: Verify tested PE bytes before manual immutable FAT prerelease + if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 20 + environment: fat-preview-publication + env: + GH_TOKEN: ${{ github.token }} + TESTED_SHA: ${{ inputs.tested_sha }} + BUILD_RUN_ID: ${{ inputs.build_run_id }} + INSTALLER_RUN_ID: ${{ inputs.installer_run_id }} + PUBLIC_ACK: ${{ inputs.public_preview_ack }} + steps: + - name: Checkout approved workflow and verifier, not untrusted candidate scripts + uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + persist-credentials: false + fetch-depth: 1 + + - name: Verify explicit consent and exact HEAD full CI + shell: bash + run: | + set -euo pipefail + python3 scripts/test-fat-direct-exe.py + [[ "$PUBLIC_ACK" == "PUBLISH PUBLIC NONPRODUCTION FAT EXE" ]] || { + echo "An explicit public preview acknowledgement is required." >&2; exit 1; + } + [[ "$TESTED_SHA" =~ ^[0-9a-f]{40}$ ]] || exit 1 + [[ "$BUILD_RUN_ID" =~ ^[0-9]+$ ]] || exit 1 + [[ "$INSTALLER_RUN_ID" =~ ^[0-9]+$ ]] || exit 1 + gh api "repos/$GITHUB_REPOSITORY/actions/runs?head_sha=$TESTED_SHA&per_page=100" > ci.json + jq --arg sha "$TESTED_SHA" -e ' + [.workflow_runs[] | select(.head_sha == $sha) | + select(.event == "pull_request" or .event == "push")] as $runs | + ($runs | length >= 10) and + ([$runs[] | select(.status != "completed" or .conclusion != "success")] | length == 0) + ' ci.json || { echo "All exact-SHA CI workflows must be successful." >&2; exit 1; } + TAG="fat-${TESTED_SHA:0:12}" + echo "PREVIEW_TAG=$TAG" >> "$GITHUB_ENV" + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "Immutable preview already exists; never overwrite an asset." >&2; exit 1 + fi + + - name: Verify downloaded Windows portable is the actual CI-tested EXE + shell: bash + run: | + set -euo pipefail + mkdir -p staged/portable published + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$BUILD_RUN_ID" > build-run.json + gh run download "$BUILD_RUN_ID" --repo "$GITHUB_REPOSITORY" \ + --name ARSAS-win-x64-portable-single-exe --dir staged/portable + mapfile -t files < <(find staged/portable -type f -name '*.exe') + [[ "${#files[@]}" -eq 1 ]] || { echo "Expected exactly one EXE." >&2; exit 1; } + python3 scripts/verify-fat-direct-exe.py \ + --run-json build-run.json --sha "$TESTED_SHA" --workflow "Build ARSAS" \ + --exe "${files[0]}" --output published/portable-provenance.json + cp "${files[0]}" "published/ARSAS-FAT-${TESTED_SHA:0:12}-win-x64-portable.exe" + + - name: Optionally verify separately successful exact-SHA installer + shell: bash + run: | + set -euo pipefail + if [[ "$INSTALLER_RUN_ID" == 0 ]]; then exit 0; fi + mkdir -p staged/installer + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$INSTALLER_RUN_ID" > installer-run.json + gh run download "$INSTALLER_RUN_ID" --repo "$GITHUB_REPOSITORY" \ + --pattern 'ARSAS-*-win-x64-installer' --dir staged/installer + mapfile -t files < <(find staged/installer -type f -name '*-setup.exe') + [[ "${#files[@]}" -eq 1 ]] || { echo "Expected exactly one installer." >&2; exit 1; } + python3 scripts/verify-fat-direct-exe.py \ + --run-json installer-run.json --sha "$TESTED_SHA" \ + --workflow "Validate ARSAS Windows installer" \ + --exe "${files[0]}" --output published/installer-provenance.json + cp "${files[0]}" "published/ARSAS-FAT-${TESTED_SHA:0:12}-win-x64-setup.exe" + + - name: Publish only approved non-production direct EXE preview assets + shell: bash + run: | + set -euo pipefail + (cd published && sha256sum ./*.exe > SHA256SUMS.txt && sha256sum -c SHA256SUMS.txt) + cat > published/NOT-PRODUCTION.txt < release-notes.md <> "$GITHUB_STEP_SUMMARY" From fb0fbdd98962da23101164c72b6e67d8d03f67b5 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 08:47:18 +0700 Subject: [PATCH 4/6] docs(fat): direct EXE release-asset workflow, public consent boundary and exact-SHA links --- docs/FAT_DIRECT_EXE_DOWNLOADS.md | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/FAT_DIRECT_EXE_DOWNLOADS.md diff --git a/docs/FAT_DIRECT_EXE_DOWNLOADS.md b/docs/FAT_DIRECT_EXE_DOWNLOADS.md new file mode 100644 index 000000000..8e9dd2573 --- /dev/null +++ b/docs/FAT_DIRECT_EXE_DOWNLOADS.md @@ -0,0 +1,49 @@ +# Direct EXE downloads for engineering FAT — opt-in public preview + +## Why a PR build link still returns ZIP + +The Windows Build ARSAS workflow already produces a real x64 single-file portable \`.exe\`. GitHub Actions stores and **downloads all workflow artifacts as ZIP**, even when only one EXE is uploaded. Changing upload-artifact names or extensions does not change this behavior. Do not label an Actions artifact URL as a direct EXE. + +Published GitHub **Release assets**, by contrast, give direct binary URLs ending in \`.exe\`. ARSAS stable v1.6.41 already offers official direct portable/installer EXEs; these are **historical stable files**, not newer #659 FAT binaries. + +## Proposed safe workflow + +\`.github/workflows/fat-direct-exe.yml\` is a **manual-only, opt-in** preview publisher. It has no push, tag, PR, or workflow_run trigger. It can only be dispatched after the infrastructure file exists on the default branch and after the owner has explicitly approved **public non-production distribution**. Do not merge any unfinished P0 application/engine changes merely to install this infrastructure. An optional GitHub Environment named \`fat-preview-publication\` can require a human reviewer. + +Required inputs: +- \`tested_sha\`: exact qualified 40-char candidate source commit. +- \`build_run_id\`: completed successful Build ARSAS run for that SHA; the workflow verifies its run identity, full set of exact-SHA workflow gates, downloaded PE signatures, and SHA-256. +- \`installer_run_id\`: optional separate successful installer validation run for the **same exact SHA**; use \`0\` when no installer exists. +- \`public_preview_ack\`: exactly \`PUBLISH PUBLIC NONPRODUCTION FAT EXE\`. This is a deliberate public-distribution authorization; without it the workflow aborts. + +A successful explicitly authorized run publishes a **public GitHub prerelease** named \`fat-\`, never a stable release or "latest", with: +- \`ARSAS-FAT--win-x64-portable.exe\` +- \`ARSAS-FAT--win-x64-setup.exe\` **only if a fully qualified exact-SHA installer exists** +- \`SHA256SUMS.txt\`, source/build provenance receipts and a NOT-PRODUCTION warning. + +Direct URLs then have the form: + +\`\`\`text +https://github.com/masarray/arsas/releases/download/fat-/ARSAS-FAT--win-x64-portable.exe +https://github.com/masarray/arsas/releases/download/fat-/ARSAS-FAT--win-x64-setup.exe +\`\`\` + +These are **URL templates, not existing download links** until the explicitly approved run finishes. Every SHA gets a fresh immutable tag; never overwrite existing preview binaries or stable v1.6.40/v1.6.41 assets. Do not change \`.release/windows.json\`, publish-verified production release metadata or \`main\` engineering acceptance through this workflow. + +## Immediate #659 status + +Current P0 Data Model #659 exact tested source: +\`1371c2990c40356acb1794bfa42c76e408421b47\`. +Windows 11/11 CI green, portable single-file artifact verified, but **Actions ZIP**: +https://github.com/masarray/arsas/actions/runs/39525163418/artifacts/12312721694 + +There is **no #659 direct EXE release asset yet**. Publishing a public prerelease (even explicitly marked FAT-only) must be approved by the repository owner before it runs. Installer for this exact candidate must pass its own Windows installer workflow before the optional setup.exe is made public. Do not substitute an older stable installer. + +As a separate reminder, a different, already published production version currently has direct EXEs: +https://github.com/masarray/arsas/releases/download/v1.6.41/ARSAS-Windows-x64-Portable.exe +https://github.com/masarray/arsas/releases/download/v1.6.41/ARSAS-Windows-x64-Setup.exe +These assets do NOT contain the latest #659 Data Model patch. + +## Security and regression + +Run \`python scripts/test-fat-direct-exe.py\` before any preview publication. The tests reject SHA mismatch, incomplete/failing workflow run, disguised ZIP, and wrong/missing PE signature. The publication script also fails closed if fewer than ten exact-SHA workflow checks succeeded, if any are incomplete/failing, if the preview tag exists, or if any candidate executable failed independent local SHA sealing. No IED connection, read, write, release promotion or application runtime change is made. From 84bfb4ab9d587f9f1fb0d6194df59da73b95ac1d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 08:49:06 +0700 Subject: [PATCH 5/6] test(ci): PR-only least-privilege direct EXE validator; publisher stays manual and gated --- .github/workflows/fat-direct-exe.yml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/fat-direct-exe.yml b/.github/workflows/fat-direct-exe.yml index ab16d1e92..6b31236b3 100644 --- a/.github/workflows/fat-direct-exe.yml +++ b/.github/workflows/fat-direct-exe.yml @@ -1,8 +1,13 @@ name: Publish direct EXE FAT preview (manual only) -# CI artifact downloads are always ZIP. Only a manually approved -# non-production GitHub prerelease can provide a direct raw .exe URL. +# CI artifact downloads are always ZIP. PRs run ONLY read-only tests; +# publishing a public, non-production GitHub prerelease is manual-only. on: + pull_request: + paths: + - '.github/workflows/fat-direct-exe.yml' + - 'scripts/verify-fat-direct-exe.py' + - 'scripts/test-fat-direct-exe.py' workflow_dispatch: inputs: tested_sha: @@ -24,7 +29,7 @@ on: required: true permissions: - contents: write + contents: read actions: read concurrency: @@ -32,8 +37,25 @@ concurrency: cancel-in-progress: false jobs: + verifier-tests: + name: Reject stale, failed and disguised-fat-build artifacts + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout for safe offline verifier tests + uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Run positive and negative direct EXE publication guard tests + run: python3 scripts/test-fat-direct-exe.py + preview: name: Verify tested PE bytes before manual immutable FAT prerelease + permissions: + contents: write + actions: read if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 20 From 6cd9a1d0469071b2fc8dbd3b4a8a2ba1166c0b2d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 08:49:22 +0700 Subject: [PATCH 6/6] docs(fat): explain safe PR-only negative tests vs manual publisher job --- docs/FAT_DIRECT_EXE_DOWNLOADS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/FAT_DIRECT_EXE_DOWNLOADS.md b/docs/FAT_DIRECT_EXE_DOWNLOADS.md index 8e9dd2573..90f5a84cd 100644 --- a/docs/FAT_DIRECT_EXE_DOWNLOADS.md +++ b/docs/FAT_DIRECT_EXE_DOWNLOADS.md @@ -8,7 +8,7 @@ Published GitHub **Release assets**, by contrast, give direct binary URLs ending ## Proposed safe workflow -\`.github/workflows/fat-direct-exe.yml\` is a **manual-only, opt-in** preview publisher. It has no push, tag, PR, or workflow_run trigger. It can only be dispatched after the infrastructure file exists on the default branch and after the owner has explicitly approved **public non-production distribution**. Do not merge any unfinished P0 application/engine changes merely to install this infrastructure. An optional GitHub Environment named \`fat-preview-publication\` can require a human reviewer. +\`.github/workflows/fat-direct-exe.yml\` is a **manual-only, opt-in** preview publisher. Pull requests may run only **read-only offline verifier tests**, with `contents: read`; the privileged publishing job has **no push, tag, PR, or workflow_run publishing trigger** and is gated to a manual dispatch on the default branch. It can only be dispatched after the infrastructure file exists on the default branch and after the owner has explicitly approved **public non-production distribution**. Do not merge any unfinished P0 application/engine changes merely to install this infrastructure. An optional GitHub Environment named \`fat-preview-publication\` can require a human reviewer. Required inputs: - \`tested_sha\`: exact qualified 40-char candidate source commit.