Skip to content

chore(tracker): record v0.7.2 release closeout #395

chore(tracker): record v0.7.2 release closeout

chore(tracker): record v0.7.2 release closeout #395

Workflow file for this run

# franken_ocr: cross-architecture release distribution (the `focr` binary).
#
# Builds the optimized `focr` binary for every target arch in the mission
# (AGENTS.md "Project Mission" + Toolchain), one artifact per (target, feature)
# variant. CPU is the priority; these variants enable the int8-matmul paths the
# whole project is built around:
#
# Apple Silicon / ARM64 (aarch64-apple-darwin)
# NEON (baseline on Apple Silicon) + FEAT_DotProd (SDOT) + FEAT_MATMUL_INT8
# (SMMLA / i8mm). The stdarch dotprod/i8mm intrinsics are NIGHTLY-only; this
# is one reason the toolchain is pinned to nightly (Toolchain note). Apple M-
# series ship dotprod+i8mm, so we target-cpu=apple-m1 to light them up.
#
# Intel / AMD x86-64 (x86_64-unknown-linux-gnu): one portable baseline binary.
# The audited runtime dispatch selects AVX2, AVX-VNNI, or AVX-512-VNNI only
# after CPUID detection. Compiling the whole installer-served binary with a
# global `+avx2` (or wider) feature would allow LLVM to emit unsupported
# instructions outside those guarded kernels and can SIGILL on older hosts.
#
# Phase-0 CI tracked linux arm64, darwin x86-64, and windows-msvc x86-64 as
# advisory targets; this workflow now ships real artifacts for all of them. The
# Unix/macOS variants build in the `dist` job below and native Windows builds in
# the `dist-windows` job. Every runner clones the three pinned path dependencies
# beside the source checkout, matching the repository's portable
# `../{frankentorch,frankensqlite,asupersync}` layout.
#
# Branch policy (AGENTS.md): release builds run on `main` ONLY. `master` is never
# referenced. Tag pushes (v*) cut a versioned set of artifacts.
#
# This workflow BUILDS and uploads artifacts; it does not publish a GitHub Release
# (that is a separate, gated step). Nightly is selected via `rust-toolchain.toml`.
name: dist
on:
push:
branches: [main]
tags: ["v*"]
# Allow a manual cross-arch build from main. source_ref lets the current
# workflow repair a compatible immutable tag; tag-owned build-input drift is
# rejected before sibling provisioning.
workflow_dispatch:
inputs:
source_ref:
description: "Optional compatible vX.Y.Z source tag to rebuild"
required: false
type: string
default: ""
concurrency:
group: dist-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
RUST_TOOLCHAIN: nightly-2026-07-09
# The sibling path dependencies are part of the release input. Keep these
# immutable revisions aligned with the package versions in Cargo.lock.
FRANKENTORCH_REV: 062cf3671c194f6ab184da98f0559ebc76cff7c7
FRANKENSQLITE_REV: cd9990bb16291d8c7c247b75b47faae8d7701adb
ASUPERSYNC_REV: 53aa5c72f855352148c3a88e6961f7f09adb535c
jobs:
dist:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
# ── Apple Silicon / ARM64 ──────────────────────────────────────────
# NEON+SDOT+SMMLA via target-cpu=apple-m1 (dotprod + i8mm). Built on a
# native arm64 macOS runner (macos-15), so no cross-linking needed.
- name: aarch64-apple-darwin (neon+sdot+i8mm)
os: macos-15
target: aarch64-apple-darwin
asset: focr-aarch64-apple-darwin-neon-sdot-i8mm
rustflags: "-C target-cpu=apple-m1"
glibc_floor: ""
# ── Intel macOS / x86-64 portability build ─────────────────────────
- name: x86_64-apple-darwin (baseline)
os: macos-15-intel
target: x86_64-apple-darwin
asset: focr-x86_64-apple-darwin
rustflags: ""
glibc_floor: ""
# ── Linux ARM64 / baseline AArch64 build ───────────────────────────
- name: aarch64-linux (baseline)
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
asset: focr-aarch64-unknown-linux-gnu
rustflags: ""
glibc_floor: "2.17"
# ── x86-64 / portable baseline ─────────────────────────────────────
# Keep global target features empty. Runtime CPUID dispatch reaches the
# guarded AVX2/VNNI kernels without raising the process-wide ISA floor.
- name: x86_64-linux (baseline + runtime dispatch)
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
asset: focr-x86_64-unknown-linux-gnu
rustflags: ""
glibc_floor: "2.17"
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ inputs.source_ref || github.ref }}
fetch-depth: 0
- name: Set up Python for evidence receipts
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- name: Validate release ref and Cargo version
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$manifestPath = Join-Path $env:GITHUB_WORKSPACE 'src/native_engine/unlimited_ocr_manifest.json'
$manifestSha = (Get-FileHash -LiteralPath $manifestPath -Algorithm SHA256).Hash.ToLowerInvariant()
$expectedManifestSha = '24ff1cfffe71eec6f07bfae8e8eb12b342877bccc43ad6ae4a4a4ceffb76edd3'
if ($manifestSha -ne $expectedManifestSha) {
throw "embedded census manifest bytes differ from Git: expected $expectedManifestSha, got $manifestSha"
}
$sourceRef = '${{ inputs.source_ref }}'
$workflowSha = $env:GITHUB_SHA
$evidenceScript = (Resolve-Path -LiteralPath 'scripts/gauntlet_cert.py').Path
if ($sourceRef) {
if ($env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or
$env:GITHUB_REF_TYPE -ne 'branch' -or
$env:GITHUB_REF_NAME -ne 'main') {
throw 'source_ref repairs must be dispatched from main'
}
$env:GITHUB_REF_TYPE = 'tag'
$env:GITHUB_REF_NAME = $sourceRef
}
python scripts/gauntlet_cert.py --dist-ref-preflight
if ($LASTEXITCODE -ne 0) { throw "dist ref preflight exited $LASTEXITCODE" }
if ($sourceRef) {
$head = (git rev-parse HEAD).Trim()
$tagHead = (git rev-parse "refs/tags/${sourceRef}^{commit}").Trim()
$originMain = (git rev-parse refs/remotes/origin/main).Trim()
if ($head -ne $tagHead) {
throw "source_ref checkout mismatch: expected $tagHead, got $head"
}
if ($workflowSha -ne $originMain) {
throw "repair workflow is not current origin/main: expected $originMain, got $workflowSha"
}
$evidenceScript = Join-Path $env:GITHUB_WORKSPACE 'scripts/gauntlet_cert.workflow-repair.py'
$blobSpec = "${workflowSha}:scripts/gauntlet_cert.py"
python -c 'import pathlib, subprocess, sys; pathlib.Path(sys.argv[2]).write_bytes(subprocess.check_output(["git", "cat-file", "blob", sys.argv[1]]))' $blobSpec $evidenceScript
if ($LASTEXITCODE -ne 0) { throw "failed to extract authenticated evidence producer" }
$expectedScriptBlob = (git rev-parse $blobSpec).Trim()
$actualScriptBlob = (git hash-object $evidenceScript).Trim()
if ($actualScriptBlob -ne $expectedScriptBlob) {
throw "repair evidence producer blob mismatch: expected $expectedScriptBlob, got $actualScriptBlob"
}
python $evidenceScript --dist-ref-preflight
if ($LASTEXITCODE -ne 0) { throw "authenticated dist ref preflight exited $LASTEXITCODE" }
Write-Host "repair workflow $workflowSha building exact tag $sourceRef at $head"
}
"FOCR_DIST_EVIDENCE_SCRIPT=$evidenceScript" >> $env:GITHUB_ENV
- name: Provision sibling path dependencies
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$parent = Split-Path -Parent $env:GITHUB_WORKSPACE
function Checkout-PinnedSibling([string] $repo, [string] $revision) {
$destination = Join-Path $parent $repo
if (Test-Path $destination) {
throw "refusing to overwrite existing sibling checkout: $destination"
}
git init --quiet $destination
git -C $destination remote add origin "https://github.com/Dicklesworthstone/$repo.git"
git -C $destination fetch --quiet --depth 1 origin $revision
git -C $destination checkout --quiet --detach FETCH_HEAD
$actual = (git -C $destination rev-parse HEAD).Trim()
if ($actual -ne $revision) {
throw "$repo checkout mismatch: expected $revision, got $actual"
}
Write-Host "$repo pinned at $actual"
}
Checkout-PinnedSibling "frankentorch" $env:FRANKENTORCH_REV
Checkout-PinnedSibling "frankensqlite" $env:FRANKENSQLITE_REV
Checkout-PinnedSibling "asupersync" $env:ASUPERSYNC_REV
# Honor the pinned nightly from `rust-toolchain.toml` (REQUIRED for the
# stdarch i8mm/dotprod intrinsics). Add the explicit compile target.
- name: Install pinned nightly toolchain
uses: dtolnay/rust-toolchain@efcb852328a9f50117170cc43094fb6f09eaf1ae
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
targets: ${{ matrix.target }}
- name: Cache cargo registry + build
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
# Cache keyed per variant so feature-divergent artifacts never collide.
key: ${{ matrix.target }}-${{ matrix.rustflags }}-${{ matrix.glibc_floor }}
- name: Install pinned glibc-floor linker
if: ${{ matrix.glibc_floor != '' }}
shell: bash
run: |
set -euo pipefail
python3 -m pip install --disable-pip-version-check 'ziglang==0.14.1'
(
cd "$RUNNER_TEMP"
cargo install --locked --version 0.23.0 cargo-zigbuild
)
mkdir -p "$RUNNER_TEMP/zig-bin"
printf '#!/usr/bin/env bash\nexec python3 -m ziglang "$@"\n' > "$RUNNER_TEMP/zig-bin/zig"
chmod +x "$RUNNER_TEMP/zig-bin/zig"
echo "$RUNNER_TEMP/zig-bin" >> "$GITHUB_PATH"
"$RUNNER_TEMP/zig-bin/zig" version
# Release build of ONLY the `focr` binary for this target+feature variant.
# RUSTFLAGS carries the arch-feature rationale documented in the matrix.
- name: Build focr (release)
shell: bash
env:
RUSTFLAGS: ${{ matrix.rustflags }}
run: |
set -euo pipefail
if [ -n '${{ matrix.glibc_floor }}' ]; then
cargo zigbuild --locked --release --bin focr \
--target '${{ matrix.target }}.${{ matrix.glibc_floor }}'
else
cargo build --locked --release --bin focr --target '${{ matrix.target }}'
fi
# Every Unix artifact is built on its native architecture. Execute the
# exact binary that will be staged, and parse the read-only robot payloads,
# before allowing it into the release artifact set.
- name: Smoke test focr (no weights)
shell: bash
run: |
set -euo pipefail
exe="target/${{ matrix.target }}/release/focr"
"$exe" --version
for cmd in schema health backends; do
payload=$("$exe" robot "$cmd")
python3 -c 'import json,sys; lines=[x for x in sys.stdin if x.strip()]; assert lines; [json.loads(x) for x in lines]' <<<"$payload"
echo "smoke ok: robot $cmd (exit 0, valid JSON)"
done
# Stage under the explicit release-contract name. Do not derive an asset
# name from the human-facing job label: installers consume these literals.
- name: Stage artifact
shell: bash
run: |
set -euo pipefail
out="${{ matrix.asset }}"
mkdir -p dist
cp "target/${{ matrix.target }}/release/focr" "dist/${out}"
( cd dist && shasum -a 256 "${out}" > "${out}.sha256" )
ls -l dist
- name: Bind successful target evidence
shell: bash
run: |
set -euo pipefail
python3 "$FOCR_DIST_EVIDENCE_SCRIPT" \
--dist-target-evidence '${{ matrix.name }}' \
--dist-asset 'dist/${{ matrix.asset }}' \
--dist-glibc-floor '${{ matrix.glibc_floor }}' \
--workflow-artifact-name '${{ matrix.asset }}'
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ matrix.asset }}
path: |
dist/*
.gauntlet-output/dist-*-workflow-evidence.json
.gauntlet-output/bundle/dist/**
include-hidden-files: true
if-no-files-found: error
retention-days: 14
# ── Native Windows (MSVC): x86-64 + arm64 ────────────────────────────────────
# Both Windows targets build natively on their own hosted runners: no
# cargo-xwin, no cross-linking — and the smoke step below needs a native host
# anyway, because x64 Windows cannot emulate arm64, so a cross-on-x64 arm64
# build could never be executed where it was built. windows-latest ships MSVC
# and nasm, so ring compiles its x86 assembly directly; windows-11-arm (the
# hosted arm64 label, available to public repos) carries the arm64 MSVC
# toolchain + LLVM for ring's aarch64 path. The whole stack (asupersync IOCP
# reactor, frankentorch, frankensqlite, focr) compiles clean for
# x86_64-pc-windows-msvc and the resulting focr.exe runs on real Windows 10
# with runtime ISA detection (the avx2 tier is selected); the arm64 row rides
# the same fail-fast:false matrix. Native build/no-weights smoke is proven by
# dist run 29048523156; full model OCR remains a separate release obligation.
# Both rows reuse the SAME pinned nightly and the SAME sibling path-dep
# provisioning as the `dist` job above (the provisioning step is already
# PowerShell, so it runs unchanged here). Only staging differs: the artifact
# is a raw .exe and the checksum is produced with Get-FileHash instead of
# shasum.
dist-windows:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
# x86-64 is the required Windows release target (proven on Windows 10).
- name: x86_64-pc-windows-msvc (baseline)
os: windows-latest
target: x86_64-pc-windows-msvc
asset: focr-x86_64-pc-windows-msvc.exe
rustflags: ""
# arm64 Windows (Snapdragon/Surface-class hosts, and Windows VMs on
# Apple Silicon). Baseline rustflags like the aarch64-linux row in the
# `dist` job: runtime dispatch selects SDOT/SMMLA (i8mm) when the
# silicon has it, so no target-cpu pinning is needed (bd-1aj1).
- name: aarch64-pc-windows-msvc (baseline)
os: windows-11-arm
target: aarch64-pc-windows-msvc
asset: focr-aarch64-pc-windows-msvc.exe
rustflags: ""
steps:
# v0.7.0 predates the repository .gitattributes policy. Configure the
# ephemeral runner before checkout so exact-tag repairs embed Git's LF
# bytes instead of Git-for-Windows CRLF translations.
- name: Preserve canonical Git line endings
shell: pwsh
run: git config --global core.autocrlf false
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ inputs.source_ref || github.ref }}
fetch-depth: 0
- name: Set up Python for evidence receipts
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- name: Validate release ref and Cargo version
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$manifestPath = Join-Path $env:GITHUB_WORKSPACE 'src/native_engine/unlimited_ocr_manifest.json'
$manifestSha = (Get-FileHash -LiteralPath $manifestPath -Algorithm SHA256).Hash.ToLowerInvariant()
$expectedManifestSha = '24ff1cfffe71eec6f07bfae8e8eb12b342877bccc43ad6ae4a4a4ceffb76edd3'
if ($manifestSha -ne $expectedManifestSha) {
throw "embedded census manifest bytes differ from Git: expected $expectedManifestSha, got $manifestSha"
}
$sourceRef = '${{ inputs.source_ref }}'
$workflowSha = $env:GITHUB_SHA
$evidenceScript = (Resolve-Path -LiteralPath 'scripts/gauntlet_cert.py').Path
if ($sourceRef) {
if ($env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or
$env:GITHUB_REF_TYPE -ne 'branch' -or
$env:GITHUB_REF_NAME -ne 'main') {
throw 'source_ref repairs must be dispatched from main'
}
$env:GITHUB_REF_TYPE = 'tag'
$env:GITHUB_REF_NAME = $sourceRef
}
python scripts/gauntlet_cert.py --dist-ref-preflight
if ($LASTEXITCODE -ne 0) { throw "dist ref preflight exited $LASTEXITCODE" }
if ($sourceRef) {
$head = (git rev-parse HEAD).Trim()
$tagHead = (git rev-parse "refs/tags/${sourceRef}^{commit}").Trim()
$originMain = (git rev-parse refs/remotes/origin/main).Trim()
if ($head -ne $tagHead) {
throw "source_ref checkout mismatch: expected $tagHead, got $head"
}
if ($workflowSha -ne $originMain) {
throw "repair workflow is not current origin/main: expected $originMain, got $workflowSha"
}
$evidenceScript = Join-Path $env:GITHUB_WORKSPACE 'scripts/gauntlet_cert.workflow-repair.py'
$blobSpec = "${workflowSha}:scripts/gauntlet_cert.py"
python -c 'import pathlib, subprocess, sys; pathlib.Path(sys.argv[2]).write_bytes(subprocess.check_output(["git", "cat-file", "blob", sys.argv[1]]))' $blobSpec $evidenceScript
if ($LASTEXITCODE -ne 0) { throw "failed to extract authenticated evidence producer" }
$expectedScriptBlob = (git rev-parse $blobSpec).Trim()
$actualScriptBlob = (git hash-object $evidenceScript).Trim()
if ($actualScriptBlob -ne $expectedScriptBlob) {
throw "repair evidence producer blob mismatch: expected $expectedScriptBlob, got $actualScriptBlob"
}
python $evidenceScript --dist-ref-preflight
if ($LASTEXITCODE -ne 0) { throw "authenticated dist ref preflight exited $LASTEXITCODE" }
Write-Host "repair workflow $workflowSha building exact tag $sourceRef at $head"
}
"FOCR_DIST_EVIDENCE_SCRIPT=$evidenceScript" >> $env:GITHUB_ENV
# Identical to the `dist` job: clone the three path-dep siblings. On Windows
# the checkout lives at e.g. D:\a\franken_ocr\franken_ocr, so the siblings
# land next to it and the `../{frankentorch,frankensqlite,asupersync}` path
# deps resolve exactly as they do on Unix.
- name: Provision sibling path dependencies
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$parent = Split-Path -Parent $env:GITHUB_WORKSPACE
function Checkout-PinnedSibling([string] $repo, [string] $revision) {
$destination = Join-Path $parent $repo
if (Test-Path $destination) {
throw "refusing to overwrite existing sibling checkout: $destination"
}
git init --quiet $destination
git -C $destination remote add origin "https://github.com/Dicklesworthstone/$repo.git"
git -C $destination fetch --quiet --depth 1 origin $revision
git -C $destination checkout --quiet --detach FETCH_HEAD
$actual = (git -C $destination rev-parse HEAD).Trim()
if ($actual -ne $revision) {
throw "$repo checkout mismatch: expected $revision, got $actual"
}
Write-Host "$repo pinned at $actual"
}
Checkout-PinnedSibling "frankentorch" $env:FRANKENTORCH_REV
Checkout-PinnedSibling "frankensqlite" $env:FRANKENSQLITE_REV
Checkout-PinnedSibling "asupersync" $env:ASUPERSYNC_REV
# Same pinned nightly as the Unix/macOS jobs (REQUIRED for the stdarch
# int8 intrinsics), plus the explicit MSVC compile target.
- name: Install pinned nightly toolchain
uses: dtolnay/rust-toolchain@efcb852328a9f50117170cc43094fb6f09eaf1ae
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
targets: ${{ matrix.target }}
- name: Cache cargo registry + build
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
# Distinct target keeps this cache from colliding with the Unix jobs.
key: ${{ matrix.target }}-${{ matrix.rustflags }}
# Native release build of ONLY the `focr` binary. No cargo-xwin: this is a
# first-class MSVC host build.
- name: Build focr (release)
env:
RUSTFLAGS: ${{ matrix.rustflags }}
run: cargo build --locked --release --bin focr --target ${{ matrix.target }}
# No-weights smoke (bd-wp8.5 contract; scripts/e2e_smoke.sh is the shell
# analogue): the freshly built binary must RUN on this host, not just
# link, before it is staged. `--version` and every read-only robot
# surface must exit 0, and each robot payload must be one parseable JSON
# object on stdout (src/cli.rs `emit`: data-only stdout, one object/line).
# Runs pre-staging so a broken binary never becomes an artifact. Both
# matrix rows execute this natively — the arm64 row is why this job uses
# windows-11-arm rather than cross-compiling on x64.
- name: Smoke test focr (no weights)
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$exe = "target/${{ matrix.target }}/release/focr.exe"
& $exe --version | Out-Host
if ($LASTEXITCODE -ne 0) { throw "focr --version exited $LASTEXITCODE" }
foreach ($cmd in @("schema", "health", "backends")) {
$lines = @(& $exe robot $cmd)
if ($LASTEXITCODE -ne 0) { throw "focr robot $cmd exited $LASTEXITCODE" }
$null = ($lines -join "`n") | ConvertFrom-Json
Write-Host "smoke ok: robot $cmd (exit 0, valid JSON)"
}
# The Windows artifact is a raw focr.exe (not a zip). Stage it under the
# canonical per-target name `focr-<target>.exe` and emit a "<hex> <name>"
# sidecar that matches the shasum/sha256sum format used by the Unix jobs:
# lowercase hex, two spaces, basename only, trailing newline. Get-FileHash
# returns uppercase hex, so lower it.
- name: Stage artifact
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$target = "${{ matrix.target }}"
$asset = "${{ matrix.asset }}"
New-Item -ItemType Directory -Force -Path dist | Out-Null
Copy-Item "target/$target/release/focr.exe" (Join-Path "dist" $asset)
$hash = (Get-FileHash -Algorithm SHA256 -Path (Join-Path "dist" $asset)).Hash.ToLower()
$line = "$hash ${asset}`n"
[System.IO.File]::WriteAllText((Join-Path (Resolve-Path "dist") "${asset}.sha256"), $line)
Get-ChildItem dist
- name: Test exact staged asset through offline install.ps1
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$asset = (Resolve-Path 'dist/${{ matrix.asset }}').Path
$releaseDir = (Resolve-Path dist).Path
$versionOutput = @(& $asset --version)
$versionExit = $LASTEXITCODE
$versionLine = if ($versionOutput.Count -gt 0) { ([string]$versionOutput[0]).Trim() } else { '' }
if ($versionExit -ne 0 -or $versionLine -notmatch '^focr ([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$') {
throw "staged asset returned a noncanonical version: $versionLine"
}
$version = "v$($Matches[1])"
$installDir = Join-Path $env:RUNNER_TEMP 'focr-offline-install-${{ matrix.target }}'
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
$caseFailureLog = Join-Path $env:RUNNER_TEMP '${{ matrix.asset }}.version-case-failure.log'
$uppercaseVersion = "V$($version.Substring(1))"
& pwsh -NoLogo -NoProfile -NonInteractive -File ./install.ps1 `
-Version $uppercaseVersion -Dir $installDir -OfflineAssetDir $releaseDir `
-NoPull -Quiet -Force *> $caseFailureLog
if ($LASTEXITCODE -eq 0) {
throw 'installer accepted a release tag with a noncanonical uppercase V prefix'
}
$installed = Join-Path $installDir 'focr.exe'
Copy-Item -LiteralPath $asset -Destination $installed
$oldStream = [System.IO.File]::Open(
$installed,
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Write,
[System.IO.FileShare]::None
)
try {
$null = $oldStream.Seek(0, [System.IO.SeekOrigin]::End)
$oldStream.WriteByte(0)
} finally {
$oldStream.Dispose()
}
$before = (Get-FileHash -LiteralPath $installed -Algorithm SHA256).Hash
$stagedHash = (Get-FileHash -LiteralPath $asset -Algorithm SHA256).Hash
if ($before -eq $stagedHash) {
throw 'preservation fixture is not distinct from the staged release asset'
}
$failureLog = Join-Path $env:RUNNER_TEMP '${{ matrix.asset }}.atomic-failure.log'
$partialFailureLog = Join-Path $env:RUNNER_TEMP '${{ matrix.asset }}.partial-replace-failure.log'
function Assert-FailedReplacementPreservesOld([string] $failpoint, [string] $logPath) {
try {
$env:FOCR_INSTALL_TEST_MODE = '1'
$env:FOCR_INSTALL_TEST_FAILPOINT = $failpoint
& pwsh -NoLogo -NoProfile -NonInteractive -File ./install.ps1 `
-Version $version -Dir $installDir -OfflineAssetDir $releaseDir `
-NoPull -Quiet -Force *> $logPath
$failureExit = $LASTEXITCODE
} finally {
Remove-Item Env:FOCR_INSTALL_TEST_FAILPOINT -ErrorAction SilentlyContinue
Remove-Item Env:FOCR_INSTALL_TEST_MODE -ErrorAction SilentlyContinue
}
if ($failureExit -eq 0) { throw "injected $failpoint failure unexpectedly passed" }
if (-not (Test-Path -LiteralPath $installed -PathType Leaf)) {
throw "injected $failpoint failure removed the previously working executable"
}
$afterFailure = (Get-FileHash -LiteralPath $installed -Algorithm SHA256).Hash
if ($afterFailure -ne $before) {
throw "injected $failpoint failure changed the previously working executable"
}
}
Assert-FailedReplacementPreservesOld 'before-replace' $failureLog
Assert-FailedReplacementPreservesOld 'replace-target-missing' $partialFailureLog
$successLog = Join-Path $env:RUNNER_TEMP '${{ matrix.asset }}.install-success.log'
& pwsh -NoLogo -NoProfile -NonInteractive -File ./install.ps1 `
-Version $version -Dir $installDir -OfflineAssetDir $releaseDir `
-NoPull -Quiet -Force -Verify *> $successLog
if ($LASTEXITCODE -ne 0) {
Get-Content -LiteralPath $successLog | Write-Host
throw "offline install.ps1 exited $LASTEXITCODE"
}
$installedHash = (Get-FileHash -LiteralPath $installed -Algorithm SHA256).Hash
if ($installedHash -ne $stagedHash) {
throw 'offline install.ps1 output differs from the exact staged asset'
}
$lockReady = Join-Path $env:RUNNER_TEMP '${{ matrix.asset }}.lock-ready'
$lockHolderOut = Join-Path $env:RUNNER_TEMP '${{ matrix.asset }}.lock-holder.out.log'
$lockHolderErr = Join-Path $env:RUNNER_TEMP '${{ matrix.asset }}.lock-holder.err.log'
$lockContentionLog = Join-Path $env:RUNNER_TEMP '${{ matrix.asset }}.lock-contention.log'
$scriptPath = (Resolve-Path ./install.ps1).Path
$pwshPath = (Get-Command pwsh).Source
try {
$env:FOCR_INSTALL_TEST_MODE = '1'
$env:FOCR_INSTALL_TEST_HOLD_LOCK_SECONDS = '10'
$env:FOCR_INSTALL_TEST_LOCK_READY_PATH = $lockReady
$holder = Start-Process -FilePath $pwshPath -PassThru `
-RedirectStandardOutput $lockHolderOut -RedirectStandardError $lockHolderErr `
-ArgumentList @(
'-NoLogo', '-NoProfile', '-NonInteractive', '-File', $scriptPath,
'-Version', $version, '-Dir', $installDir,
'-OfflineAssetDir', $releaseDir, '-NoPull', '-Quiet'
)
} finally {
Remove-Item Env:FOCR_INSTALL_TEST_LOCK_READY_PATH -ErrorAction SilentlyContinue
Remove-Item Env:FOCR_INSTALL_TEST_HOLD_LOCK_SECONDS -ErrorAction SilentlyContinue
Remove-Item Env:FOCR_INSTALL_TEST_MODE -ErrorAction SilentlyContinue
}
function Write-LockHolderLogs {
foreach ($path in @($lockHolderOut, $lockHolderErr)) {
if (Test-Path -LiteralPath $path -PathType Leaf) {
Get-Content -LiteralPath $path | Write-Host
}
}
}
for ($attempt = 0; $attempt -lt 300 -and -not (Test-Path -LiteralPath $lockReady); $attempt++) {
if ($holder.HasExited) {
Write-LockHolderLogs
throw "lock holder exited before acquiring the lock: $($holder.ExitCode)"
}
Start-Sleep -Milliseconds 100
}
if (-not (Test-Path -LiteralPath $lockReady -PathType Leaf)) {
if (-not $holder.HasExited) { Stop-Process -Id $holder.Id -Force }
$holder.WaitForExit()
Write-LockHolderLogs
throw 'lock holder did not publish deterministic readiness within 30 seconds'
}
try {
& pwsh -NoLogo -NoProfile -NonInteractive -File ./install.ps1 `
-Version $version -Dir $installDir -OfflineAssetDir $releaseDir `
-NoPull -Quiet *> $lockContentionLog
$contentionExit = $LASTEXITCODE
if ($contentionExit -eq 0) {
throw 'concurrent install unexpectedly bypassed the destination lock'
}
} finally {
if (-not $holder.HasExited) { Stop-Process -Id $holder.Id -Force }
$holder.WaitForExit()
}
$fastPathLog = Join-Path $env:RUNNER_TEMP '${{ matrix.asset }}.verified-fast-path.log'
& pwsh -NoLogo -NoProfile -NonInteractive -File ./install.ps1 `
-Version $version -Dir $installDir -OfflineAssetDir $releaseDir `
-NoPull -Quiet -Verify *> $fastPathLog
if ($LASTEXITCODE -ne 0) {
Get-Content -LiteralPath $fastPathLog | Write-Host
throw "verified exact-version fast path exited $LASTEXITCODE after lock-owner death"
}
$selftestEvent = $null
foreach ($line in (Get-Content -LiteralPath $fastPathLog)) {
try { $event = $line | ConvertFrom-Json -ErrorAction Stop } catch { continue }
if ($event.command -eq 'robot.selftest') { $selftestEvent = $event }
}
if ($null -eq $selftestEvent -or $selftestEvent.verdict -ne 'pass' -or
$selftestEvent.all_ok -ne $true) {
throw 'exact-version -Verify path did not emit a passing robot.selftest event'
}
$combinedLog = Join-Path $env:RUNNER_TEMP '${{ matrix.asset }}.installer-e2e.log'
@(
'=== case-sensitive release-version rejection proof ==='
(Get-Content -LiteralPath $caseFailureLog)
'=== injected atomic-failure preservation proof ==='
(Get-Content -LiteralPath $failureLog)
'=== injected partial-replace restoration proof ==='
(Get-Content -LiteralPath $partialFailureLog)
'=== successful offline install proof ==='
(Get-Content -LiteralPath $successLog)
'=== cross-process contention rejection proof ==='
(Get-Content -LiteralPath $lockContentionLog)
'=== crash-release + exact-version verification proof ==='
(Get-Content -LiteralPath $fastPathLog)
) | Set-Content -LiteralPath $combinedLog -Encoding utf8
"FOCR_DIST_INSTALLED_ASSET=$installed" >> $env:GITHUB_ENV
"FOCR_DIST_INSTALLER_LOG=$combinedLog" >> $env:GITHUB_ENV
- name: Bind successful target evidence
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
python $env:FOCR_DIST_EVIDENCE_SCRIPT `
--dist-target-evidence '${{ matrix.name }}' `
--dist-asset 'dist/${{ matrix.asset }}' `
--dist-installed-asset $env:FOCR_DIST_INSTALLED_ASSET `
--dist-installer-e2e-log $env:FOCR_DIST_INSTALLER_LOG `
--workflow-artifact-name '${{ matrix.asset }}'
if ($LASTEXITCODE -ne 0) { throw "dist evidence producer exited $LASTEXITCODE" }
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ matrix.asset }}
path: |
dist/*
.gauntlet-output/dist-*-workflow-evidence.json
.gauntlet-output/bundle/dist/**
include-hidden-files: true
if-no-files-found: error
retention-days: 14