Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
096ff9f
feat(release): cut vX.Y.Z-rc.N when CI goes green on the tip of relea…
noahgift Sep 24, 2026
8d2cbe2
fix(release): smoke strips -rc.N and binds the asset to its tag's com…
noahgift Sep 24, 2026
804e81d
feat(release): promote vX.Y.Z-rc.N to vX.Y.Z with the rc's bytes, no …
noahgift Sep 24, 2026
b1ea2a1
feat(release): the rc cut runs the publish gate, so a crates.io defec…
noahgift Sep 24, 2026
7685efa
chore(roadmap): kind: code on the four rc-automation rows (#4285 #427…
noahgift Sep 24, 2026
0b3463d
fix(release): a promoted final's summary said the build matrix was pa…
noahgift Sep 24, 2026
a74f5b7
fix(rc-cut): state success() on the cut step so a red publish gate vi…
noahgift Sep 24, 2026
f1a97f7
fix(version): apr names its commit on both published surfaces, never …
noahgift Sep 23, 2026
fc97dc2
roadmap: GH-4110 fragment (scope: both published surfaces)
noahgift Sep 24, 2026
2b3cb55
fix(version): an rc asset prints its rc — apr 0.69.3-rc.N (<sha>), ga…
noahgift Sep 24, 2026
533c823
roadmap: PMAT-4110/4256 in progress (aprender-ea)
noahgift Sep 24, 2026
90b070f
feat(release): an rc is a draft until every fleet host runs it; no cu…
noahgift Sep 24, 2026
ca7332f
fix(promote): read the rc by listing and refuse a still-draft rc by n…
noahgift Sep 24, 2026
07e9aa0
merge origin/main into PMAT-4285-rc-cut: #4314 went DIRTY after #4338…
noahgift Sep 25, 2026
15b03ae
fix(guard): check_release_assets row 12 piped into grep -q — 75th sit…
noahgift Sep 25, 2026
7d06cad
fix(guard): rc_fleet_stage row label said 'apr verify' — check_apr_bi…
noahgift Sep 25, 2026
dffcfef
fix(test): driver_cuda_gguf initializers lacked three fields main add…
noahgift Sep 25, 2026
9c35d85
feat(release): darwin pv + x86_64 wgpu apr on every tag; the asset ga…
noahgift Sep 25, 2026
fbca016
fix(release): the mini jobs carry the macOS discriminating labels che…
noahgift Sep 25, 2026
adb895c
docs(release): six apr / ten pv / twenty-two in the prose; pv-darwin …
noahgift Sep 25, 2026
ed970f9
fix(features): apr-cli --features wgpu compiles and is clippy-clean (…
noahgift Sep 24, 2026
d2b185d
Merge branch 'main' into PMAT-4285-rc-cut
noahgift Sep 25, 2026
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
347 changes: 320 additions & 27 deletions .github/workflows/binary-release.yml

Large diffs are not rendered by default.

89 changes: 89 additions & 0 deletions .github/workflows/rc-cut.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: RC Cut

# Cut vX.Y.Z-rc.N as a GitHub PRERELEASE when CI goes green on the tip of
# release/X.Y.Z (#4285, RC-DOGFOOD-001 §4.1). The decision and every write live in
# scripts/release/rc_cut.sh; this file only wires the trigger and the token.
#
# TRIGGER. ci.yml does not run on a push to release/**. A release branch is gated by
# CI on its release PR (release/X.Y.Z -> main), which re-runs on every push with
# head_sha = the branch tip. `workflow_run` fires when any CI run completes. The
# `branches` filter matches the run's HEAD branch, so a release PR's runs arrive
# here and a feature PR's do not. rc_cut.sh then requires, on that run:
# `ci / gate` and `workspace-test` success, head repository = this repository
# (fork PRs can name a branch release/X.Y.Z), head_sha still the tip, and no rc
# tag already on that commit. Anything else is a logged skip, not a failure.
#
# `workflow_run` always runs this file as it is on the DEFAULT branch, with this
# repository's token, so a release branch cannot change what the cut does.
#
# ASSETS. A release created with GITHUB_TOKEN does not fire `release: published`
# in binary-release.yml (GitHub suppresses recursive triggers), so rc_cut.sh
# dispatches binary-release.yml with inputs.tag. That needs `actions: write`.
#
# PUBLISH GATE (#4287). Before anything is written, the job decides with --dry-run,
# checks the rc commit out beside the default-branch checkout (in RUNNER_TEMP, so no
# cargo config is inherited from this tree) and runs scripts/release/rc_publish_gate.sh
# on it: the publish graph (R6), the path-only dev-dep rule that #3425 broke v0.68.0
# on, and a `cargo package --no-verify` walk of every publishable crate. Red means the
# job fails and no rc exists; the defect is found on the rc, never at the final tag.
# The real cut then re-decides from the same run, so it tags the commit that was gated.
#
# CI cuts the tag and the prerelease. Publishing a crate stays out of CI (RP-001).

on:
workflow_run:
workflows: [CI]
types: [completed]
branches: ['release/**']

permissions:
contents: write # create the tag ref and the prerelease
actions: write # dispatch binary-release.yml for the assets

# One cut at a time per release branch, never cancelled: N is read then written,
# and two green runs racing would both read the same N. The tag ref is also the
# lock (a duplicate gets HTTP 422 and writes nothing else).
concurrency:
group: rc-cut-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: false

jobs:
cut:
# Operator rule (2026-09-10): self-hosted fleet only. The job needs bash, curl
# and python3, which clean-room carries (binary-release.yml's verify job).
runs-on: [self-hosted, Linux, clean-room]
# the package walk is ~65 s warm on lambda; a cold registry index is the rest
timeout-minutes: 30
# Cheap pre-filter only; rc_cut.sh re-judges every condition itself.
if: github.event.workflow_run.conclusion != 'cancelled'
steps:
- uses: actions/checkout@v7
with:
# the default branch's script, never the release branch's
ref: ${{ github.event.repository.default_branch }}
fetch-depth: 1
- name: Case tables (the decision and the gate must still be able to refuse)
run: |
bash scripts/release/rc_cut.sh --self-test
bash scripts/release/rc_publish_gate.sh --self-test
- name: Decide (writes nothing)
id: decide
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: bash scripts/release/rc_cut.sh --run-id "$RUN_ID" --dry-run
- name: Publish gate on the rc commit (#4287)
if: steps.decide.outputs.tag != ''
env:
SHA: ${{ steps.decide.outputs.sha }}
run: |
git fetch --no-tags --depth 1 origin "$SHA"
git worktree add --detach "$RUNNER_TEMP/rc-tree" "$SHA"
bash scripts/release/rc_publish_gate.sh "$RUNNER_TEMP/rc-tree"
- name: Cut the rc
if: success() && steps.decide.outputs.tag != '' # explicit: a red publish gate never reaches the cut
id: cut
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: bash scripts/release/rc_cut.sh --run-id "$RUN_ID"
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions contracts/apr-version-traceability-v1.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
metadata:
version: 1.1.0
version: 1.2.0
created: '2026-04-05'
author: PAIML Engineering
description: "apr --version traceability contract — the version output must always contain an informative build identifier; never the bare '(unknown)' sentinel when a CARGO_PKG_VERSION or any alternative identifier is available; AND the embedded git SHA must match the actual HEAD of the source tree at build time (including in git worktrees)"
references:
- "paiml/aprender#597 (Version string shows (unknown) instead of git hash)"
- "paiml/aprender#1862 (build.rs misses HEAD changes in worktrees because ../../.git is a file pointer, not a directory)"
- "paiml/aprender#4110 (crates.io installs print +no-git although the tarball's .cargo_vcs_info.json carries the commit)"
registry: true
tags:
- cli
Expand Down Expand Up @@ -36,11 +37,13 @@ equations:
- "Common sentinel forms are forbidden: (unknown), 0000000, <empty>, null"

fallback_hierarchy:
formula: "APR_GIT_SHA = override ∨ git_hash ∨ committed_sha ∨ 'v{version}+no-git'"
formula: "APR_GIT_SHA = override ∨ vcs_info_sha ∨ git_hash ∨ committed_sha ∨ 'v{version}+no-git'"
domain: "build-time environment"
codomain: "APR_GIT_SHA environment variable"
invariants:
- "Env var APR_GIT_SHA_OVERRIDE, if set, wins (for CI/release)"
- "Otherwise: read git.sha1 from the package's .cargo_vcs_info.json (written by cargo package into every published crate), short 9-hex form, -dirty suffix when cargo recorded a dirty tree; a value that is not 40 hex is ignored (#4110)"
- "The vcs rung precedes git rev-parse: an unpacked crate may sit inside an UNRELATED repository whose HEAD is not the crate's commit"
- "Otherwise: try git rev-parse --short HEAD"
- "Otherwise: try reading crates/apr-cli/.git-sha committed file"
- "Otherwise: emit 'v{CARGO_PKG_VERSION}+no-git' as informative fallback"
Expand Down Expand Up @@ -89,6 +92,11 @@ falsification_tests:
prediction: "When built without git context, apr --version output contains the package version string"
test: "apr --version 2>&1 | grep -qE '0\\.[1-9][0-9]*\\.[0-9]+'"
if_fails: "Version string missing entirely"
- id: FALSIFY-VERSION-005
rule: "a packaged apr-cli (no .git of its own) names the commit in its .cargo_vcs_info.json, even inside a foreign repository"
prediction: "The shipped build.rs, compiled and run as cargo runs it on a package dir holding the byte-exact apr-cli-0.69.1 vcs file, emits APR_GIT_SHA=d8a6df53a; inside an unrelated repo it still does; with the vcs rung removed or placed behind git rev-parse the table goes RED"
test: "cargo test -p apr-cli --lib build_rs_vcs_sha"
if_fails: "crates.io installs regress to '+no-git' (0.68.1/0.69.1 shape), or report an enclosing repository's HEAD"
- id: FALSIFY-VERSION-004
rule: "build.rs registers worktree-aware rerun-if-changed triggers"
prediction: "build.rs invokes `git rev-parse --git-dir` and `--git-common-dir` (not a hardcoded `../../.git/HEAD` path)"
Expand Down
5 changes: 4 additions & 1 deletion crates/apr-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ cuda = ["inference", "realizar/cuda", "entrenar/cuda", "trueno/cuda", "aprender-
# the binary was built with it (§2.1 INVALID-BUILD reads that entry).
cuda-batch = ["cuda"]
# PMAT-335: WGPU backend for AMD/Intel/Apple GPU inference via Vulkan/Metal/WebGPU
wgpu = ["inference"]
# #4056: the finetune wgpu path (commands/finetune.rs) uses entrenar items gated
# behind entrenar's own `gpu` feature. Weak dep: enabled only when `training`
# brings entrenar in, which is exactly when that path compiles.
wgpu = ["inference", "entrenar?/gpu"]
visualization = ["renacer", "trueno-viz"]
zram = ["trueno-zram-core"]
training = ["dep:entrenar", "dep:entrenar-lora", "dep:aprender-train-distill", "dep:aprender-train-common", "aprender-train-distill?/shard-batch-source"]
Expand Down
78 changes: 71 additions & 7 deletions crates/apr-cli/build.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
// Contract: apr-version-traceability-v1 F-VERSION-001..004 (paiml/aprender#597, #1862).
// Resolve APR_GIT_SHA with a multi-source fallback hierarchy:
// 1. APR_GIT_SHA_OVERRIDE env var (CI/release hook)
// 2. git rev-parse --short HEAD (dev builds from worktree or primary checkout)
// 3. committed .git-sha file (crates.io installs)
// 4. "v{CARGO_PKG_VERSION}+no-git" (informative fallback — never bare "unknown")
// 2. .cargo_vcs_info.json `git.sha1` — written by `cargo package` into every published
// tarball, so a crates.io install knows its commit (#4110). Ahead of `git rev-parse`
// because an unpacked crate may sit inside SOME OTHER repository whose HEAD is not ours
// 3. git rev-parse --short HEAD (dev builds from worktree or primary checkout; retried with
// the enclosing checkout marked safe.directory when git refuses a checkout owned by another
// user — binary-release.yml builds as root in a container over the runner's checkout)
// 4. committed .git-sha file
// 5. "v{CARGO_PKG_VERSION}+no-git" (informative fallback — never bare "unknown")
fn main() {
let sha = resolve_git_sha();
println!("cargo:rustc-env=APR_GIT_SHA={sha}");
Expand All @@ -16,6 +21,7 @@ fn main() {

println!("cargo:rerun-if-env-changed=APR_GIT_SHA_OVERRIDE");
println!("cargo:rerun-if-changed=.git-sha");
println!("cargo:rerun-if-changed=.cargo_vcs_info.json");
}

fn register_git_rerun_triggers() {
Expand Down Expand Up @@ -58,20 +64,78 @@ fn resolve_git_sha() -> String {
}
}

// 2. Live git hash from worktree (works for both primary checkouts and worktrees)
if let Some(sha) = run_git(&["rev-parse", "--short", "HEAD"]) {
// 2. The commit `cargo package` recorded (crates.io / packaged builds, no .git)
if let Some(sha) = vcs_info_sha() {
return sha;
}

// 3. Committed .git-sha fallback (updated by release script at publish time)
// 3. Live git hash from worktree (works for both primary checkouts and worktrees).
// `trusted_git_retry`: the release binaries are built as root in a container over the
// runner-owned checkout, where git refuses the repo as "dubious ownership" (#4110)
let head = ["rev-parse", "--short", "HEAD"];
if let Some(sha) = run_git(&head).or_else(|| trusted_git_retry(&head)) {
return sha;
}

// 4. Committed .git-sha fallback (updated by release script at publish time)
if let Ok(contents) = std::fs::read_to_string(".git-sha") {
let s = contents.trim().to_string();
if !s.is_empty() {
return s;
}
}

// 4. Informative fallback — never bare "unknown"
// 5. Informative fallback — never bare "unknown"
let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".to_string());
format!("v{version}+no-git")
}

/// `git.sha1` from `.cargo_vcs_info.json`, shortened to 9 hex digits (what `git rev-parse
/// --short` prints in this repository), with `-dirty` when cargo packaged a dirty tree.
/// Parsed by hand: a build script has no serde, and the file is cargo's own fixed shape.
fn vcs_info_sha() -> Option<String> {
let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let text = std::fs::read_to_string(format!("{dir}/.cargo_vcs_info.json")).ok()?;
let after = &text[text.find("\"sha1\"")? + "\"sha1\"".len()..];
let open = after.find('"')?;
let value = &after[open + 1..];
let sha = &value[..value.find('"')?];
if sha.len() != 40 || !sha.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let dirty = text.contains("\"dirty\": true") || text.contains("\"dirty\":true");
Some(format!(
"{}{}",
&sha[..9],
if dirty { "-dirty" } else { "" }
))
}

/// `git <args>` again, with the checkout that encloses this package marked `safe.directory`.
///
/// git refuses a repository owned by another user ("detected dubious ownership"), and the release
/// lane builds as root in a container over the runner-owned checkout, so every published binary
/// printed `+no-git` (#4110). The exception goes in a private GLOBAL config under OUT_DIR:
/// git ignores `safe.directory` given by `-c` or GIT_CONFIG_* before 2.39 (measured, 2.34.1).
/// Only the checkout found by walking up from CARGO_MANIFEST_DIR is trusted, never `*`, and
/// `rev-parse` runs no hooks. This build is already compiling that checkout's code.
fn trusted_git_retry(args: &[&str]) -> Option<String> {
let manifest = std::env::var("CARGO_MANIFEST_DIR").ok()?;
let top = std::path::Path::new(&manifest)
.ancestors()
.find(|d| d.join(".git").exists())?;
let config =
std::path::Path::new(&std::env::var("OUT_DIR").ok()?).join("apr-safe-directory.gitconfig");
std::fs::write(
&config,
format!("[safe]\n\tdirectory = {}\n", top.display()),
)
.ok()?;
let out = std::process::Command::new("git")
.args(args)
.env("GIT_CONFIG_GLOBAL", &config)
.output()
.ok()?;
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
(out.status.success() && !s.is_empty()).then_some(s)
}
Loading
Loading