Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions .github/workflows/fat-direct-exe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: Publish direct EXE FAT preview (manual only)

# 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:
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: read
actions: read

concurrency:
group: fat-direct-exe-preview
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
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 <<EOF
ARSAS manual public prerelease FAT preview.
Exact tested source: $TESTED_SHA
Windows Build run: $BUILD_RUN_ID
Installer run: $INSTALLER_RUN_ID (0 means not included).
No physical FAT acceptance. No production release.
EOF
cat > release-notes.md <<EOF
ARSAS engineering FAT preview only; NOT a production release.
Source SHA: $TESTED_SHA
Build run ID: $BUILD_RUN_ID
Installer run ID: $INSTALLER_RUN_ID

Direct executable files are release assets, not CI ZIP artifacts.
Verify SHA256SUMS.txt. No physical IED acceptance is implied.
Stable releases, mainline acceptance and production metadata remain unchanged.
EOF
gh release create "$PREVIEW_TAG" published/* --repo "$GITHUB_REPOSITORY" \
--target "$TESTED_SHA" --title "ARSAS FAT Preview ${TESTED_SHA:0:12}" \
--notes-file release-notes.md --prerelease
{
echo "## Direct .exe FAT preview (NOT production)"
echo "Source: $TESTED_SHA"
echo "Portable: https://github.com/$GITHUB_REPOSITORY/releases/download/$PREVIEW_TAG/ARSAS-FAT-${TESTED_SHA:0:12}-win-x64-portable.exe"
if [[ "$INSTALLER_RUN_ID" != 0 ]]; then
echo "Installer: https://github.com/$GITHUB_REPOSITORY/releases/download/$PREVIEW_TAG/ARSAS-FAT-${TESTED_SHA:0:12}-win-x64-setup.exe"
fi
} >> "$GITHUB_STEP_SUMMARY"
49 changes: 49 additions & 0 deletions docs/FAT_DIRECT_EXE_DOWNLOADS.md
Original file line number Diff line number Diff line change
@@ -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. 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.
- \`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-<SHA12>\`, never a stable release or "latest", with:
- \`ARSAS-FAT-<SHA12>-win-x64-portable.exe\`
- \`ARSAS-FAT-<SHA12>-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-<SHA12>/ARSAS-FAT-<SHA12>-win-x64-portable.exe
https://github.com/masarray/arsas/releases/download/fat-<SHA12>/ARSAS-FAT-<SHA12>-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.
61 changes: 61 additions & 0 deletions scripts/test-fat-direct-exe.py
Original file line number Diff line number Diff line change
@@ -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()
70 changes: 70 additions & 0 deletions scripts/verify-fat-direct-exe.py
Original file line number Diff line number Diff line change
@@ -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()
Loading