Companion to dev-flow.md. Sibling plan: see
release-automation-plan.md for the
release/versioning architecture (release-plz + git-cliff + eventual
crates.io publishing). This document owns merge-time CI gates;
the release-automation plan owns what happens after merge to main
— version bump, changelog generation, tag creation, binary + crate
publishing. Both plans share release.yml (binary distribution,
left unchanged) and scripts/ci-pipeline/src/version.rs (whose
version-bump functions are retired in release-automation Phase R5).
Incorporates external review (2026-04-23) that correctly identified structural gaps in v1:
- v1 still treated CI as one long lane instead of separating merge-confidence from artifact production from release packaging.
- v1's "soft-skip everything" rule was too broad — semantic checks
with local inputs (e.g.
cargo vetwhenCargo.lockchanged) should be hard-fail, not soft-skip. - v1 mixed when a gate runs (time) with why it exists (purpose). A proper model needs both axes.
- v1 proposed
CARGO_INCREMENTAL=0injust/shared.just— the right fix is to move the entire cache policy into.cargo/config.toml(Cargo supportsbuild.incrementalnatively) and delete the Just export entirely. One source of truth. - v1 kept Windows xwin in pre-commit at 40-90 s cold — violates the "don't break flow" threshold. Windows belongs in pre-push + CI, not pre-commit.
- v1 left pre-push at zero test execution. nextest profiles + filtersets let us run a mandatory fast subset without blowing budget.
Status as of 2026-04-23 · Maintainer: @githubrobbi
Every gate has both a time tier (when) and a lane purpose (why).
Axis A — time tiers (from v1, unchanged):
- T0 editor · T1 pre-commit · T2 pre-push · T3 CI on PR · T4 nightly
Axis B — lane purpose (new):
- Gate lane — "is this obviously broken?" Blocks merge. Fast.
- Preview lane — "is this SHA worth human validation on real binaries?" Produces artifacts. Non-blocking. On-demand.
- Release lane — "is this SHA shippable?" Signed, SBOM'd, published. Tag-only.
- Deep-assurance lane — "does this survive slow / probabilistic checks?" Coverage, miri, fuzz. Nightly.
A SHA earns its way from gate → preview → release. These are not coordinate stages on one long pipeline; they are promotion steps.
| Lane | Trigger | Mandatory? | Max runtime | Outputs |
|---|---|---|---|---|
| T1 pre-commit (gate) | git commit |
yes, bypassable with --no-verify |
< 15 s warm staged-only | clean working tree |
| T2 pre-push (gate) | git push |
yes, bypassable | < 60 s warm | none; local confidence |
| T3 PR-fast (gate) | PR open / push to PR branch | required to merge | < 8 min wall | Pass/fail only |
| T3 PR-preview (preview) | PR label preview-binaries or manual dispatch, only after PR-fast green on same SHA |
not required | 15-30 min | Windows artifacts + nextest archive + per-file SHA256 manifest (Phase 5 scope; macOS/Linux deferred to Phase 9+) |
| T4 Tier 2 (deep) | weekly cron | no | 60-90 min | coverage, miri, udeps report |
| Tag (release) | v*.*.* tag push |
n/a (gated by PR-fast being green) | 30-45 min | signed binaries, SBOM, SLSA attestation, GH Release |
Key architectural distinction from today's flow:
- Today: every PR push triggers
ci.ymlwhich does format+clippy+rustdoc+tests+security+file-size (8 jobs) — and the user is expected to rely on Tier 2 + Release for Windows validation. Result: a regression that only shows on Windows surfaces at release time (10-15 min into a ship run). - Tomorrow: every PR push triggers
pr-fast.ymlincluding a cheap Windows compile-check. Preview binaries are opt-in via label. Release stays tag-only.
Every gate's trigger is expressed in terms of one of four file classes.
Same predicates apply locally (pre-commit / pre-push) and in CI (classify
job in pr-fast.yml). Hand-coded copies in both places must agree,
which is one motivation for the stretch goal in §2.7.
| Class | Patterns (glob) | Intent |
|---|---|---|
rust_changed |
**/*.rs |
Any Rust source edit. |
dep_changed |
Cargo.toml, **/Cargo.toml, Cargo.lock, supply-chain/** |
Dependency graph touched. Cargo Vet reads the full tree via cargo metadata --all-features, so Cargo.toml counts even when Cargo.lock is untouched. |
infra_changed |
.github/**, scripts/**, .cargo/**, .config/**, just/**, rust-toolchain*, clippy.toml, rustfmt.toml, deny.toml, REUSE.toml, codecov.yml |
Tooling, workflows, lint / test / deny configs, toolchain pin. Any of these can break build/CI/hooks. |
docs_changed |
**/*.md, docs/**, LICENSE*, CHANGELOG.md, TRADEMARK.md, SECURITY.md, assets/** |
Documentation and brand assets only. |
Derived predicate used by all compile/test/lint jobs:
code_changed = rust_changed || dep_changed || infra_changed
A PR that only hits docs_changed skips every compile gate. Anything
else runs the full gate. Dependency-only PRs run compile/docs/tests/
Windows — dep_changed alone is enough to gate them in, because a
Cargo.toml or lockfile bump can break any of those. Infra-only PRs
(e.g. workflow tweaks) are treated the same way.
| Gate | Hard when… | Tier | Missing-tool behavior |
|---|---|---|---|
| fmt | rust_changed |
T1 | stdlib rustfmt; always present |
| clippy (scoped) | rust_changed |
T1 | stdlib; always present |
| file-size-policy | always | T1 | shell script; always present |
cargo vet check --locked |
dep_changed |
T1+T2+T3 | hard-fail with install hint — not soft-skip |
| workspace clippy trio | code_changed |
T2 | always present |
rustdoc -Dwarnings |
code_changed |
T2 | always present |
| doctests | code_changed |
T2 (new) | always present |
cargo deny check --locked |
dep_changed |
T2 | cargo-deny required; hard-fail on missing |
cargo check --locked |
code_changed |
T2 | always present |
test-compile (nextest --no-run --locked) |
code_changed |
T2 | cargo-nextest required; hard-fail on missing (pinned in .config/nextest.toml) |
nextest pre-push-smoke profile |
code_changed |
T2 (new) | same; hard-fail on missing |
| Windows xwin clippy (local) | code_changed |
T2 | advisory locally: soft-skip with install hint when cargo-xwin missing. Hard-gated at T3 by native windows-lint job (Phase W5.5 of windows-clippy-and-linux-cross-plan.md flipped this from cargo check to cargo clippy -- -D warnings). |
| pr-fast required aggregate | always | T3 | n/a |
| Gate | Rationale |
|---|---|
| typos | spell-check; nice-to-have |
| taplo | TOML formatting; nice-to-have |
| reuse | SPDX header check; nice-to-have |
| cargo machete | unused-dep check; Tier 2 has cargo-udeps as the authoritative version |
cargo-vet is the only tool promoted from advisory to hard. Rationale:
it has a dedicated local trigger (dep_changed), its input is 100 %
reproducible offline, and CI enforces it anyway — the CI-only placement
is the exact bug that caused the v0.5.71 four-round-trip incident.
Windows xwin is deliberately split between advisory-local and
hard-required-remote. Rationale: cargo-xwin downloads the MSVC SDK on
first run (~400 MB) and not all contributors want that footprint. The
PR-fast native windows-lint job (Phase W5.5; runs cargo clippy -- -D warnings on windows-latest) is the authoritative gate; the local
xwin step is an ergonomics win for contributors who already have it
installed. This is not a regression of the v1 "hard xwin" position
— the feedback correctly flagged that treating a 400 MB download as a
hard local requirement is user-hostile.
cargo-deny and cargo-nextest are hard-required rather than advisory.
Rationale: both are listed in just update-tools (just/dev.just:118)
as baseline dev tools, and their checks are required in CI. A missing
binary at push time means the developer is out of sync with the
baseline; hard-fail with install hint is correct.
v1's _lint_pre_push.sh spawns everything at once and lets wall-clock
finish. That maximizes throughput but delays the first actionable red
result. Cargo's target-dir lock serialises most heavy jobs anyway.
New order (T2 pre-push), in two buckets:
Bucket 1 — fire-and-wait in parallel (all cheap, all non-cargo):
- fmt check
- file-size-policy
- typos (advisory)
- reuse (advisory)
- taplo (advisory)
- cargo vet (when dep files changed)
Bucket 2 — sequential in meaningful order (each command uses --locked
where supported):
cargo check --workspace --all-targets --all-features --locked— fastest compile gate, catches most type errors before clippy.- clippy
lint-ci(--locked) - clippy
lint-prod(--locked) - clippy
lint-tests(--locked) cargo doc --workspace --all-features --no-deps --lockedwithRUSTDOCFLAGS=-Dwarningscargo test --doc --workspace --all-features --locked(new)cargo deny check— runs whendep_changed, advisory otherwisecargo nextest run --no-run --workspace --all-targets --all-features --locked(test compile)cargo nextest run --profile pre-push-smoke --locked(new)cargo xwin clippy --workspace --all-targets --all-features --target x86_64-pc-windows-msvc --no-deps -- -D warnings(just lint-ci-windows, Phase W5.6) — advisory locally (soft-skip with install hint ifcargo-xwinmissing; authoritative gate is PR-fast nativewindows-lintonwindows-latest)
The first red in Bucket 2 aborts the rest (fail-fast). Bucket 1
continues to completion so the user sees all cheap-check results in
one report. Why --locked everywhere: Cargo's --locked enforces
the exact resolution from Cargo.lock and is the canonical "CI" mode;
matching local to CI eliminates a class of "green locally, red in CI"
surprises where the resolver fetched a newer patch.
Current (@/Users/rnio/Private/Github/UltraFastFileSearch/.cargo/config.toml:16-25):
[build]
target-dir = "target"
rustc-wrapper = "sccache"Target:
[build]
target-dir = "target"
# sccache + incremental=false is the canonical local cache policy.
# Paired here (not in just/shared.just) so every cargo invocation —
# including those spawned by git hooks, rust-script, IDE plugins,
# etc. — inherits both settings as one atomic config.
# Docs: https://doc.rust-lang.org/cargo/reference/config.html#build
rustc-wrapper = "sccache"
incremental = falseAnd delete @/Users/rnio/Private/Github/UltraFastFileSearch/just/shared.just:15
line export CARGO_INCREMENTAL := "1" entirely.
Why this is better than v1's proposal: Cargo reads config.toml first,
then env overrides. Today's drift exists because just/shared.just
exports CARGO_INCREMENTAL=1 into every subshell — which overrides
.cargo/config.toml's implicit incremental = true default. Flipping
the env var to 0 in Just "works" but keeps two authorities. Removing
the env var and adding incremental = false to config.toml has one
authority — env vars only override in CI, where they're explicitly set.
Rollback: If a contributor wants incremental for a specific
experimental session they can still CARGO_INCREMENTAL=1 cargo build
— env overrides config. The default just becomes the correct one.
Current: three profiles (default, ci, slow).
Add:
# ---------------------------------------------------------------------------
# pre-push-smoke — mandatory fast test subset at git-push time.
# Budget: 10-20 s warm on a developer laptop.
# Usage: cargo nextest run --profile pre-push-smoke --locked
#
# Selection is a denylist expressed in nextest's filterset language:
# - Exclude the validation suite (`test_validate_*`) — those tests
# are heavy integration and belong in the CI `ci` profile.
# - Exclude the entire `uffs-client` package — its shmem tests
# serialise globally via `threads-required = num-cpus` and
# dominate wall-time when mixed with fast unit tests.
# - Exclude the benchmark harness binaries
# (`benchmark_filtering`, `benchmark_sorting`) that are compiled
# under `--tests`.
# - Everything else: unit tests across all remaining crates.
#
# Long term: replace package-level denylist with per-test overrides
# that carry an explicit `slow` attribute, so the filter becomes
# `not attr(slow)` rather than a list of package / binary names.
# ---------------------------------------------------------------------------
[profile.pre-push-smoke]
test-threads = -2
retries = 0
fail-fast = true
slow-timeout = { period = "30s", terminate-after = 1 }
leak-timeout = "100ms"
failure-output = "immediate-final"
success-output = "never"
status-level = "fail"
final-status-level = "flaky"
default-filter = """
not test(/^test_validate/)
& not package(uffs-client)
& not binary(benchmark_filtering)
& not binary(benchmark_sorting)
"""Runtime expectation on this workspace: roughly 8-15 s warm. Will be validated during Phase 3 rollout.
Replace the current "spawn everything at once" pattern with the two-bucket scheduler described in §1.4. Key contract changes:
- cargo vet becomes hard-required when
Cargo.lockorsupply-chain/**touched in the about-to-push range. If tool is missing in that case → fail with install hint. - doctests added as mandatory.
- nextest smoke added as mandatory.
- cargo check added as the first compile gate (fast fail before the three clippy runs).
Critical protocol detail: git push invokes the pre-push hook with
the ref updates on stdin, one per line, format:
<local_ref> <local_oid> <remote_ref> <remote_oid>\n
(reference: https://git-scm.com/docs/githooks#_pre_push). Earlier
drafts tried to compute the pushed range via git merge-base HEAD @{push}
or origin/<branch> heuristics. Those break on (a) first push of a new
branch, (b) alternate remotes, (c) force-pushes, (d) multi-ref pushes, and
the fallback-to-HEAD error path silently collapses the diff to empty —
meaning a hard gate could be skipped exactly when it's needed. The
implementation must consume stdin.
Detailed pseudo-code:
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------
# 1. Consume git's pre-push stdin protocol.
# ---------------------------------------------------------------------
ZERO='0000000000000000000000000000000000000000'
CHANGED_FILES=""
while IFS=' ' read -r _local_ref local_oid _remote_ref remote_oid; do
# Blank line or deletion ($local_oid == ZERO) — skip.
[[ -z "${local_oid:-}" || "$local_oid" == "$ZERO" ]] && continue
if [[ "$remote_oid" == "$ZERO" ]]; then
# New remote ref (first push of this branch). Diff against the
# best available base: merge-base with origin/main, falling back
# to the root commit. Conservative: if we cannot determine a
# base, treat everything as changed (triggers all hard gates).
base=$(git merge-base "$local_oid" origin/main 2>/dev/null \
|| git rev-list --max-parents=0 "$local_oid" 2>/dev/null | tail -n1 \
|| echo "")
else
base="$remote_oid"
fi
if [[ -n "$base" ]]; then
CHANGED_FILES+=$'\n'$(git diff --name-only "$base" "$local_oid")
else
# Be conservative: can't compute diff → run everything.
CHANGED_FILES="__UNKNOWN__"
break
fi
done
# If CHANGED_FILES is empty (no parseable refs — e.g. hook invoked outside
# of git push for testing), also be conservative.
[[ -z "${CHANGED_FILES// /}" ]] && CHANGED_FILES="__UNKNOWN__"
# ---------------------------------------------------------------------
# 2. Classify into the four file classes from §1.3.
# ---------------------------------------------------------------------
class_matches() {
[[ "$CHANGED_FILES" == "__UNKNOWN__" ]] && return 0
echo "$CHANGED_FILES" | grep -E "$1" >/dev/null
}
RUST_CHANGED=0; class_matches '\.rs$' && RUST_CHANGED=1
DEP_CHANGED=0; class_matches '^(.*Cargo\.toml$|Cargo\.lock$|supply-chain/)' && DEP_CHANGED=1
INFRA_CHANGED=0; class_matches '^(\.github/|scripts/|\.cargo/|\.config/|just/|rust-toolchain|clippy\.toml$|rustfmt\.toml$|deny\.toml$|REUSE\.toml$|codecov\.yml$)' && INFRA_CHANGED=1
CODE_CHANGED=$(( RUST_CHANGED || DEP_CHANGED || INFRA_CHANGED ))
# ---------------------------------------------------------------------
# 3. Bucket 1 — cheap, parallel.
# ---------------------------------------------------------------------
spawn "fmt" cargo fmt --all -- --check
spawn "file-size" bash scripts/ci/check_file_size_policy.sh
if (( DEP_CHANGED )); then
if ! command -v cargo-vet >/dev/null 2>&1; then
printf '\033[0;31m❌ cargo-vet required (Cargo.{toml,lock} or supply-chain/ changed)\033[0m\n' >&2
printf ' install: \033[0;36mcargo install cargo-vet --locked\033[0m\n' >&2
printf ' or run: \033[0;36mjust install-dev-tools\033[0m\n' >&2
exit 2
fi
spawn "vet" cargo vet check --locked
fi
command -v typos >/dev/null 2>&1 && spawn "typos" typos .
command -v reuse >/dev/null 2>&1 && spawn "reuse" reuse lint --quiet
command -v taplo >/dev/null 2>&1 && spawn "taplo" taplo fmt --check
# ---------------------------------------------------------------------
# 4. Bucket 2 — sequential, fail-fast. Only runs when code changed.
# ---------------------------------------------------------------------
if (( CODE_CHANGED )); then
run_seq cargo check --workspace --all-targets --all-features --locked
run_seq just lint-ci
run_seq just lint-prod
run_seq just lint-tests
run_seq env RUSTDOCFLAGS=-Dwarnings cargo doc --workspace --all-features --no-deps --locked
run_seq env RUSTDOCFLAGS=-Dwarnings cargo test --doc --workspace --all-features --locked
(( DEP_CHANGED )) && run_seq cargo deny check --locked --hide-inclusion-graph
run_seq cargo nextest run --workspace --all-targets --all-features --no-run --locked
run_seq cargo nextest run --profile pre-push-smoke --locked
# Windows xwin: advisory locally (see §1.3.3). Print install hint on miss.
if command -v cargo-xwin >/dev/null 2>&1; then
run_seq just check-windows
else
printf '\033[1;33m⚠ cargo-xwin missing — skipping local Windows check\033[0m\n'
printf ' PR-fast will validate on windows-latest regardless.\n'
printf ' To check locally: \033[0;36mcargo install cargo-xwin\033[0m\n'
fi
fi
# Wait on Bucket 1 results, aggregate into exit code.Current: xwin runs at pre-commit if *.rs staged and cargo-xwin
present. Cold cost: 40-90 s. Violates the <15 s T1 budget.
Target: xwin moved to pre-push only (local advisory there; PR-fast
authoritative). Pre-commit keeps: fmt, file-size, clippy-scoped, taplo,
typos, reuse, plus cargo-vet when staged files hit dep_changed.
Staged-file classification (different from pre-push — pre-commit sees the staged set, not ref diff):
STAGED=$(git diff --cached --name-only --diff-filter=ACMR)
class_matches() { echo "$STAGED" | grep -E "$1" >/dev/null; }
DEP_STAGED=0; class_matches '^(.*Cargo\.toml$|Cargo\.lock$|supply-chain/)' && DEP_STAGED=1
if (( DEP_STAGED )); then
command -v cargo-vet >/dev/null 2>&1 || {
printf '\033[0;31m❌ cargo-vet required at commit (staged: Cargo.toml/lock/supply-chain)\033[0m\n' >&2
exit 2
}
spawn "vet" cargo vet check --locked
fiResult: pre-commit stays under 15 s warm, pre-push owns local Windows advisory, PR-fast owns authoritative Windows gate.
Current: single .github/workflows/ci.yml (17 KB, 8 jobs).
New architecture:
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2025-2026 SKY, LLC.
name: PR Fast CI
on:
pull_request:
branches: [main]
push:
branches: [main, develop]
# Required when using GitHub merge queue — without this the required
# checks never report against the merge-queue test-merge commit and
# the queue stalls indefinitely. Harmless if merge queue is off.
merge_group:
workflow_dispatch:
# Least-privilege default. Individual jobs opt into write scopes if
# they need them (none currently do).
permissions:
contents: read
concurrency:
group: pr-fast-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
# Match local policy (.cargo/config.toml) so green-locally ↔ green-in-CI.
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
jobs:
# ---------------------------------------------------------------------
# classify — emits file-class outputs consumed by every downstream job.
# MUST be in `required`'s needs so a classify failure cannot silently
# green-light a PR.
# ---------------------------------------------------------------------
classify:
name: Classify changes
runs-on: ubuntu-22.04
timeout-minutes: 2
outputs:
rust: ${{ steps.f.outputs.rust }}
dep: ${{ steps.f.outputs.dep }}
infra: ${{ steps.f.outputs.infra }}
docs_only: ${{ steps.f.outputs.docs_only }}
code: ${{ steps.code.outputs.code }}
steps:
- name: Checkout (full history for accurate diff base)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
# Pinned to the head SHA for reproducibility when replaying
# the workflow. Falls through on push/merge_group events.
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- id: f
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
filters: |
rust:
- '**/*.rs'
dep:
- 'Cargo.toml'
- '**/Cargo.toml'
- 'Cargo.lock'
- 'supply-chain/**'
infra:
- '.github/**'
- 'scripts/**'
- '.cargo/**'
- '.config/**'
- 'just/**'
- 'rust-toolchain*'
- 'clippy.toml'
- 'rustfmt.toml'
- 'deny.toml'
- 'REUSE.toml'
- 'codecov.yml'
docs_only:
- added|modified: '**'
- '!**/*.rs'
- '!**/Cargo.toml'
- '!Cargo.lock'
- '!supply-chain/**'
- '!.github/**'
- '!scripts/**'
- '!.cargo/**'
- '!.config/**'
- '!just/**'
- id: code
run: |
if [[ "${{ steps.f.outputs.rust }}" == 'true' \
|| "${{ steps.f.outputs.dep }}" == 'true' \
|| "${{ steps.f.outputs.infra }}" == 'true' ]]; then
echo "code=true" >> "$GITHUB_OUTPUT"
else
echo "code=false" >> "$GITHUB_OUTPUT"
fi
# ---------------------------------------------------------------------
# file-size — always runs, cheap.
# ---------------------------------------------------------------------
file-size:
name: File size policy
runs-on: ubuntu-22.04
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- run: bash scripts/ci/check_file_size_policy.sh
# ---------------------------------------------------------------------
# fmt — rust_changed. Cheap, runs in parallel with classify downstream.
# ---------------------------------------------------------------------
fmt:
name: Format check
runs-on: ubuntu-22.04
needs: classify
if: needs.classify.outputs.rust == 'true'
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- run: rustup show
- run: cargo fmt --all -- --check
# ---------------------------------------------------------------------
# sanity — fastest compile gate (cargo check) + cargo-vet.
# Downstream heavy jobs need this to have passed.
# ---------------------------------------------------------------------
sanity:
name: Sanity (cargo check + vet)
runs-on: ubuntu-22.04
needs: classify
if: needs.classify.outputs.code == 'true'
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- run: rustup show
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: cargo fetch --locked
run: cargo fetch --locked
- name: cargo check --locked
run: cargo check --workspace --all-targets --all-features --locked
- name: cargo vet check --locked
if: needs.classify.outputs.dep == 'true'
uses: taiki-e/install-action@dfc84ffb7254b048a0a843316ff5a2714dcdc7bd # v2
with: { tool: cargo-vet }
- if: needs.classify.outputs.dep == 'true'
run: cargo vet check --locked
clippy:
name: Clippy
runs-on: ubuntu-22.04
needs: sanity
if: needs.classify.outputs.code == 'true'
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: { ref: ${{ github.event.pull_request.head.sha || github.sha }} }
- run: rustup show
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- run: cargo clippy --workspace --all-targets --all-features --locked --no-deps -- -D warnings
docs:
name: Rustdoc + doctests
runs-on: ubuntu-22.04
needs: sanity
if: needs.classify.outputs.code == 'true'
timeout-minutes: 30
env: { RUSTDOCFLAGS: "-Dwarnings" }
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: { ref: ${{ github.event.pull_request.head.sha || github.sha }} }
- run: rustup show
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- run: cargo doc --workspace --all-features --no-deps --locked
- run: cargo test --doc --workspace --all-features --locked
test-build:
name: Test build
runs-on: ubuntu-22.04
needs: sanity
if: needs.classify.outputs.code == 'true'
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: { ref: ${{ github.event.pull_request.head.sha || github.sha }} }
- run: rustup show
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with: { shared-key: test-build, cache-on-failure: 'true' }
- run: cargo test --workspace --all-features --lib --tests --no-run --locked
tests:
name: Tests
runs-on: ubuntu-22.04
needs: test-build
if: needs.classify.outputs.code == 'true'
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: { ref: ${{ github.event.pull_request.head.sha || github.sha }} }
- run: rustup show
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with: { shared-key: test-build, save-if: 'false' }
- uses: taiki-e/install-action@dfc84ffb7254b048a0a843316ff5a2714dcdc7bd # v2
with: { tool: nextest }
- run: cargo nextest run --workspace --all-features --lib --tests --profile ci --locked
security:
name: Security (deny + vet)
runs-on: ubuntu-22.04
needs: classify
if: needs.classify.outputs.dep == 'true' || needs.classify.outputs.infra == 'true' || needs.classify.outputs.rust == 'true'
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: { ref: ${{ github.event.pull_request.head.sha || github.sha }} }
- run: rustup show
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: taiki-e/install-action@dfc84ffb7254b048a0a843316ff5a2714dcdc7bd # v2
with: { tool: cargo-deny,cargo-vet }
- run: cargo deny check --locked
- run: cargo vet check --locked
# NOTE: This sample shows the original Phase 4 `windows-check`
# job (cargo check). Phase W5.5 of
# `windows-clippy-and-linux-cross-plan.md` renamed it to
# `windows-lint` and switched the command to
# `cargo clippy --workspace --all-targets --all-features --locked
# --no-deps -- -D warnings`. The live workflow at
# `.github/workflows/pr-fast.yml::windows-lint` is the source of
# truth; this snippet is preserved for historical context only.
windows-check:
name: Windows compile check
runs-on: windows-latest
needs: sanity
if: needs.classify.outputs.code == 'true'
timeout-minutes: 25
env: { RUSTFLAGS: "-C target-cpu=x86-64-v3" }
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: { ref: ${{ github.event.pull_request.head.sha || github.sha }} }
- run: rustup show
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with: { shared-key: pr-fast-windows }
- run: cargo check --workspace --all-targets --all-features --locked
# ---------------------------------------------------------------------
# required — single branch-protection target. Explicitly includes
# `classify` so a broken classify cannot green-light the PR via
# skipped downstream jobs (GitHub's `skipped` counts as `success`
# for required checks, so we must check result strings ourselves).
# ---------------------------------------------------------------------
required:
name: PR Fast CI / required
runs-on: ubuntu-22.04
if: always()
needs:
- classify
- file-size
- fmt
- sanity
- clippy
- docs
- test-build
- tests
- security
- windows-check
steps:
- name: Gate on classify
# Classify MUST succeed. A skipped or failed classify means
# downstream skips are meaningless — treat as failure.
run: |
if [[ "${{ needs.classify.result }}" != "success" ]]; then
echo "::error::classify job did not succeed (result=${{ needs.classify.result }})"
exit 1
fi
- name: Aggregate downstream results
run: |
set -u
declare -A R=(
[file-size]='${{ needs.file-size.result }}'
[fmt]='${{ needs.fmt.result }}'
[sanity]='${{ needs.sanity.result }}'
[clippy]='${{ needs.clippy.result }}'
[docs]='${{ needs.docs.result }}'
[test-build]='${{ needs.test-build.result }}'
[tests]='${{ needs.tests.result }}'
[security]='${{ needs.security.result }}'
[windows-check]='${{ needs.windows-check.result }}'
)
fail=0
for job in "${!R[@]}"; do
r="${R[$job]}"
case "$r" in
success|skipped) ;;
*) echo "::error::$job: $r"; fail=1 ;;
esac
done
exit "$fail"Critical correctness notes:
required.needsincludesclassify— and the first step explicitly fails unlessneeds.classify.result == 'success'. This closes the GitHub Actions quirk where a failed upstream job causes downstream jobs toskip, andskippedcounts as success for branch-protection aggregation. Without this, a brokenclassifywould green-light every PR.code_changedis the gate for every compile/test/Windows job — notrust == 'true'. A PR that only touchesCargo.lockor a workflow file still runs the full compile/test matrix, because those changes can break anything.merge_group:trigger is present so that merge-queue runs report against the test-merge commit; without it, enabling merge queue later would require rediscovering this the hard way.permissions: contents: readat the workflow level is the least-privilege default per GitHub's Actions hardening guide.- Checkout ref policy: every job pins to
github.event.pull_request.head.sha || github.sha, so the PR-fast run and the preview run in §2.5.2 validate the same commit SHA. Without this,actions/checkout's default behaviour on PR events can check out a test-merge commit that doesn't match the head SHA the preview workflow sees, causing manifest mismatches. - Third-party actions pinned to full commit SHAs. Matches the
repo's existing practice in
ci.ymlandtier-2.yml. Dependabot tracks these and opens bump PRs via.github/dependabot.yml. --lockedon every cargo command that supports it (check,doc,test,clippy,deny,vet,nextest). Guarantees the resolver uses exactlyCargo.lock. Onecargo fetch --lockedup front insanityprimes the cache for the whole pipeline.
Scope for Phase 5: Windows binaries + Windows nextest archive only. macOS / Linux preview builds are deferred (see §7 Non-goals).
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2025-2026 SKY, LLC.
name: Preview Artifacts
on:
pull_request:
types: [labeled, synchronize]
workflow_dispatch:
inputs:
sha:
description: 'Commit SHA to build (default: latest on default branch)'
required: false
permissions:
contents: read
checks: read # for verify-pr-fast-green
pull-requests: read # for label inspection
concurrency:
group: preview-${{ github.workflow }}-${{ github.event.pull_request.number || github.event.inputs.sha || github.sha }}
cancel-in-progress: true
jobs:
# ---------------------------------------------------------------------
# gate — only proceed when:
# (a) manual dispatch, OR
# (b) PR has the `preview-binaries` label.
# Emits the exact SHA every downstream job will build and validate.
# ---------------------------------------------------------------------
gate:
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'preview-binaries'))
runs-on: ubuntu-22.04
timeout-minutes: 2
outputs:
sha: ${{ steps.s.outputs.sha }}
steps:
- id: s
run: |
SHA='${{ github.event.pull_request.head.sha || github.event.inputs.sha || github.sha }}'
[[ -z "$SHA" ]] && { echo "::error::no SHA resolved"; exit 1; }
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
# ---------------------------------------------------------------------
# verify-pr-fast-green — preview binaries are only worth producing for
# a SHA that has already passed the merge-blocking checks. Otherwise a
# tester is validating a commit that wouldn't merge anyway.
# Queries GitHub's check-runs API for the SHA.
# ---------------------------------------------------------------------
verify-pr-fast-green:
needs: gate
runs-on: ubuntu-22.04
timeout-minutes: 5
steps:
- name: Poll check-runs for PR Fast CI / required
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const sha = '${{ needs.gate.outputs.sha }}';
const want = 'PR Fast CI / required';
// Retry for up to 10 minutes in case pr-fast is still running.
for (let i = 0; i < 60; i++) {
const { data } = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: sha,
per_page: 100,
});
const run = data.check_runs.find(r => r.name === want);
if (run && run.status === 'completed') {
if (run.conclusion === 'success') {
core.info(`✅ ${want} is success on ${sha.slice(0,7)}`);
return;
}
core.setFailed(`❌ ${want} on ${sha.slice(0,7)}: ${run.conclusion}`);
return;
}
core.info(`⏳ ${want} not yet complete (status=${run?.status ?? 'missing'}); retry ${i+1}/60`);
await new Promise(r => setTimeout(r, 10_000));
}
core.setFailed(`⏱ Timed out waiting for ${want} on ${sha.slice(0,7)}`);
# ---------------------------------------------------------------------
# build-windows — cross-compile Windows release binary on Linux/xwin.
# Runs only after PR-fast is green on the same SHA.
# ---------------------------------------------------------------------
build-windows:
needs: [gate, verify-pr-fast-green]
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.gate.outputs.sha }} # validated SHA, not PR head
- run: rustup show
- run: rustup target add x86_64-pc-windows-msvc
- uses: taiki-e/install-action@dfc84ffb7254b048a0a843316ff5a2714dcdc7bd # v2
with: { tool: cargo-xwin }
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with: { shared-key: preview-windows }
- name: cargo xwin build --release
run: |
cargo xwin build --release --bins --locked \
--target x86_64-pc-windows-msvc
- name: Stage Windows artifacts
run: |
mkdir -p dist/windows
cp target/x86_64-pc-windows-msvc/release/*.exe dist/windows/ || true
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: windows-preview-${{ needs.gate.outputs.sha }}
path: dist/windows/
if-no-files-found: error
# ---------------------------------------------------------------------
# build-test-archive — cargo-nextest supports "archive on one machine,
# run on another". Target machine needs (a) the same source SHA
# checked out, (b) nextest installed. Cargo itself is not required.
# ---------------------------------------------------------------------
build-test-archive:
needs: [gate, verify-pr-fast-green]
runs-on: ubuntu-22.04
timeout-minutes: 30
outputs:
nextest_version: ${{ steps.nxv.outputs.version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: { ref: ${{ needs.gate.outputs.sha }} }
- run: rustup show
- run: rustup target add x86_64-pc-windows-msvc
- uses: taiki-e/install-action@dfc84ffb7254b048a0a843316ff5a2714dcdc7bd # v2
with: { tool: cargo-xwin,nextest }
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with: { shared-key: preview-windows }
- id: nxv
run: echo "version=$(cargo nextest --version | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: cargo nextest archive
run: |
cargo nextest archive --workspace --locked \
--target x86_64-pc-windows-msvc \
--archive-file uffs-tests-${{ needs.gate.outputs.sha }}.tar.zst
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: nextest-archive-${{ needs.gate.outputs.sha }}
path: uffs-tests-${{ needs.gate.outputs.sha }}.tar.zst
if-no-files-found: error
# ---------------------------------------------------------------------
# smoke-windows — GitHub-hosted windows-latest runs the archive against
# the SAME SHA that produced it. Nextest's cross-compile model requires
# the target machine to have the source tree checked out at the same
# revision + nextest installed. We satisfy both here.
# ---------------------------------------------------------------------
smoke-windows:
needs: [gate, build-windows, build-test-archive]
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: { ref: ${{ needs.gate.outputs.sha }} }
- uses: taiki-e/install-action@dfc84ffb7254b048a0a843316ff5a2714dcdc7bd # v2
with: { tool: nextest }
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with: { name: nextest-archive-${{ needs.gate.outputs.sha }} }
- run: cargo nextest run --archive-file uffs-tests-${{ needs.gate.outputs.sha }}.tar.zst --profile ci
# ---------------------------------------------------------------------
# manifest — human-readable summary committed-alongside-artifacts so a
# tester downloading from the browser UI can verify integrity and
# compatibility. Emits per-file SHA256 in addition to the
# artifact-level digest that upload-artifact@v4 validates.
# ---------------------------------------------------------------------
manifest:
needs: [gate, build-windows, build-test-archive, smoke-windows]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: { ref: ${{ needs.gate.outputs.sha }} }
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with: { path: preview }
- name: Emit manifest.json
run: |
RUSTC=$(rustc --version)
LOCK_SHA=$(sha256sum Cargo.lock | awk '{print $1}')
{
echo '{'
echo ' "git_sha": "${{ needs.gate.outputs.sha }}",'
echo ' "tested_sha": "${{ needs.gate.outputs.sha }}",'
echo ' "cargo_lock_sha256": "'"$LOCK_SHA"'",'
echo ' "rustc_version": "'"$RUSTC"'",'
echo ' "nextest_version": "${{ needs.build-test-archive.outputs.nextest_version }}",'
echo ' "target": "x86_64-pc-windows-msvc",'
echo ' "build_os": "ubuntu-22.04 (cargo-xwin)",'
echo ' "files": ['
find preview -type f | while read -r f; do
sha=$(sha256sum "$f" | awk '{print $1}')
printf ' {"path": "%s", "sha256": "%s"},\n' "$f" "$sha"
done | sed '$ s/,$//'
echo ' ]'
echo '}'
} > manifest.json
cat manifest.json
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: manifest-${{ needs.gate.outputs.sha }}
path: manifest.jsonCritical correctness notes:
verify-pr-fast-greenpolls the GitHub check-runs API for the exact SHA and refuses to proceed unlessPR Fast CI / requiredis green on that commit. Retries for 10 min to absorb cases where the PR-fast run is still executing. Closes the "preview artifacts from a commit that couldn't merge" class of confusion.- Every downstream job checks out
needs.gate.outputs.sha— the same SHA the PR-fast run validated. No reliance on defaultactions/checkoutbehaviour which can land on the test-merge commit. - Manifest declares
git_sha == tested_shaexplicitly so a human downloading the bundle can cross-check againstPR Fast CI.tested_shais separate fromgit_shabecause in a future extension the smoke runner could validate against a merge-commit while binaries were built from the PR head. nextest_versionis in the manifest so a person running the archive manually knows which nextest to install. Nextest's archive requires matching versions on both sides.- Per-file SHA256 in the manifest, not just the artifact-level digest that GitHub's UI shows. A manual downloader verifies file integrity without trusting the Actions UI.
- Self-hosted runners are intentionally NOT used anywhere. Even
though
windows-latestcosts ~10× Linux minutes, running fork PR code on a self-hosted runner is a well-documented security hazard. If a self-hosted Windows runner is introduced later, it must be gated ongithub.event.pull_request.head.repo.fork == false(trusted branches only) — documented here so future maintainers don't miss it.
ci.yml→ delete afterpr-fast.ymlis green for a week. Replacemain-protectionrequired status checks with the singlePR Fast CI / requiredaggregate.tier-2.yml→ keep as-is except: dropwindows-checkjob (now inpr-fast.yml). Tier 2 stays the deep-assurance lane (coverage, miri, udeps). DONE in PR #138 (W5 follow-on) oncepr-fast.yml::windows-lintflipped to strict clippy and strictly subsumed Tier 2's weekly compile-check; tombstone comment intier-2.ymldocuments the removal.
Same as v1's Phase 5. Eliminates rust-script binary-cache Heisenbugs.
Additional benefit from v2: the binary can now import the single
gate-definition file (see §2.7) so local pre-push, just ship, and
pr-fast.yml all reference the same gate list.
Long-term direction: replace the hand-maintained correspondence between
_lint_pre_push.sh, ci.yml, and this doc with a single file consumed
by all three. Deferred — documented here so future-us knows the target
shape.
2026-05-07 update: §2.7's executable contract is shipped. The gates-manifest plan (
docs/architecture/gates-manifest-plan.md) landed in five phases:
Phase What PR 0 Plan + schema design #139 1 Manifest + drift detector (no consumer changes) #140 2 _lint_pre_push.shcodegen#141 3 pr-fast.ymlstructural validator (revised from "codegen" — see plan §4.2 pivot rationale)#143 (plan-pivot in #142) 3a _lint_fast.shcodegen#144 Four drift detectors now run side-by-side in pre-push Bucket 1 + pr-fast CI, covering four orthogonal drift axes:
gates-drift— gate-set mismatch (Phase 1)hooks-drift— pre-push hook content (Phase 2)workflow-drift—pr-fast.ymlstructural (Phase 3)fast-drift— pre-commit hook content (Phase 3a)Adding, renaming, or removing a gate is now a single edit to
scripts/ci/gates.tomlfollowed byjust uffs-gen-hooks/just gen-fast; the workflow YAML still hand-owns its bespoke per-job shape (eleven distinct shapes for ~thirteen pr-fast-tier gates) butworkflow-driftenforces structural alignment with the manifest.Phase 3c (
gen-docs, prose tables in this doc + CONTRIBUTING.md) remains deferred per plan §4.3 rationale — the four executable drift detectors above already catch every dangerous failure mode; what remains is low-stakes editorial drift in prose tables, which the plan judges as not worth the codegen infrastructure.The sketch below is preserved as the original problem statement.
# gates.toml (proposed, not yet implemented)
[[gate]]
id = "cargo-vet"
label = "cargo vet check --locked"
tiers = ["pre-commit", "pre-push", "pr-fast"]
hard_required_when = "dep_changed"
tool = "cargo-vet"
cmd = "cargo vet check --locked"
expected_runtime_secs = 2
[[gate]]
id = "doctests"
label = "cargo test --doc"
tiers = ["pre-push", "pr-fast"]
hard_required_when = "rust_changed"
cmd = "cargo test --doc --workspace --all-features"
env = { RUSTDOCFLAGS = "-Dwarnings" }
expected_runtime_secs = 25
# ... etcGenerator script would produce _lint_fast.sh, _lint_pre_push.sh,
and the PR-fast workflow matrix from this. Converts v1's "explanatory
truth" into "executable truth".
New policy applied across both new workflows and enforced as a review checklist going forward. Most of it mirrors GitHub's published Actions hardening guide.
At the workflow level:
permissions: contents: readas the default block. Individual jobs opt into write scopes explicitly; none currently do.concurrencygroup withcancel-in-progress: trueon PR events so stale runs die when a new push lands.- All third-party actions pinned to full commit SHAs (40-char hex),
never floating tags.
.github/dependabot.ymlalready tracks Actions updates; bump PRs land as small reviewable deltas. merge_group:trigger present on every workflow that reports required checks. Harmless when merge queue is disabled; essential when it is enabled.
At the job level:
timeout-minutesset on every job. Default GitHub timeout is 360 (6 h); we cap realistic jobs at 30 min and cheap jobs at 2–5 min. Prevents runaway runs from exhausting runner-hour budgets.runs-on: windows-latest/macos-latestonly on GitHub-hosted runners, never self-hosted. Rationale: fork PRs execute arbitrary author code, and a self-hosted runner sits on privileged infra.- Every
uses: actions/checkout@...pinsref:to an explicitgithub.event.pull_request.head.sha || github.sha. Default checkout behaviour differs betweenpull_requestandpull_request_targetevents and can land on a synthetic merge commit. Explicit refs remove the ambiguity between what PR-fast validates and what the preview lane builds. --lockedpassed to every cargo subcommand that accepts it. Onecargo fetch --lockedup front seeds the cache.
Manifest integrity:
actions/upload-artifact@v4already computes and validates an artifact-level SHA256 on download inside Actions. Our manifest additionally lists per-file SHA256 so a human downloading via the browser UI can verify without trusting the UI.- Manifest includes tool versions (rustc, nextest, cargo-vet as applicable) so that a validator on a different machine knows exactly which binaries to install.
What is explicitly forbidden:
pull_request_targeton any workflow that runs build/test code. Reason: it runs against the base branch's workflow file with elevated permissions and is the primary source of the "malicious PR exfiltrates secrets" class of bug.- Self-hosted runners for any job reachable from fork PRs. If a
self-hosted Windows runner is ever introduced (see §9 open
items), the job must gate on
github.event.pull_request.head.repo.fork == false. - Floating action tags (
@v4,@main). Full commit SHA only. secrets.*reads in forked-PR context. GitHub masks them but shell exit codes and derived strings may leak; audit any job that touches secrets.
This policy ships alongside Phases 4–5 and is retroactively
applied to tier-2.yml, codeql.yml, release.yml,
auto-tag-release.yml, cargo-vet-refresh.yml,
dependabot-review.yml, and dependabot-auto-merge.yml as a
housekeeping commit (Phase 4b). Most of those already conform;
the audit's job is to confirm and fill gaps.
Seven commits, each atomic, ranked so each step leaves the repo green.
Scope:
scripts/hooks/_lint_pre_push.sh— addcargo vet(hard-required on dep change),doctests,cargo nextest run --profile pre-push-smoke..config/nextest.toml— addpre-push-smokeprofile.- Does NOT yet reorder schedule or split buckets — that's Phase 2.
Validation:
just lint-pre-pushon current HEAD — all green within 60 s.touch Cargo.lock→ commit →just lint-pre-push— cargo-vet runs and passes.- Uninstall
cargo-vet, repeat (2) — hard-fails with install hint. - Introduce a deliberate doctest failure — pre-push aborts.
Risk: Low. Additive changes.
Scope:
scripts/hooks/_lint_pre_push.sh— rewrite with two-bucket scheduler (§1.4).scripts/hooks/_lint_fast.sh— drop thecheck-windowsinvocation.- Add
cargo checkas the first Bucket 2 step.
Validation:
- Introduce a type error → confirm pre-push shows
cargo checkred within ~15 s instead of waiting for the 40-60 s clippy batch. - Time pre-commit on a
*.rs-only change — confirm under 15 s warm.
Risk: Medium. Scheduler rewrite. Keep the previous script as
_lint_pre_push.sh.bak for one cycle.
Scope:
.cargo/config.toml— addincremental = false.just/shared.just— deleteexport CARGO_INCREMENTAL := "1".scripts/ci/ci-pipeline.rs— remove the explicitCARGO_INCREMENTAL=0pairing (no longer needed).CHANGELOG.md— note the policy change.
Validation:
- Fresh
cargo build --workspacecold → time recorded. touch one file→ rebuild → time recorded. Should be close to v1 §6.4's 42 s figure.just ship -vthrough to push — pre-push green.- Direct
git pushfrom fresh shell with no env overrides — pre-push green (proof that drift is gone).
Risk: Medium. This is the Bug B root-cause fix.
Scope:
- Create
pr-fast.ymlwith the full structure in §2.5 (classify + required aggregator depending on classify + merge_group trigger + workflow-levelpermissions: contents: read+ per-job full-SHA action pins). - Create
preview-artifacts.yml(stub, artifact build only, no smoke runner yet — that's Phase 5). - Keep
ci.ymlintact; both workflows run in parallel for one week. - Update
tier-2.yml— dropwindows-checkjob (now inpr-fast.yml). DONE in PR #138 (W5 follow-on, see above). - Confirm
.github/dependabot.ymltracks both new workflows.
Validation (each item MUST pass before Phase 4 cutover):
- Docs-only PR →
pr-fastcompletes withfmt,sanity,clippy,docs,test-build,tests,security,windows-checkallskipped;required= success.ci.ymldoes the same (parity). - Dep-only PR (e.g. a Cargo.toml version bump with no
.rschange) →pr-fastruns the full compile/test/Windows matrix. This exercises the v2→v3 under-gating fix whererust == 'true'was the (wrong) trigger. - Infra-only PR (e.g. edit to
.github/workflows/pr-fast.ymlitself, or tojust/lint.just) → same as dep-only: full matrix. - Rust-change PR → both workflows run identical checks. Compare
wall-clock:
pr-fastmust be ≤ci.ymlon p50. - Broken-classify simulation — the single most important check.
Manually break the
dorny/paths-filterstep in a test branch (e.g. invalid YAML in the filters block).classifyfails, downstream jobsskip, andrequiredmust fail (not pass via skip aggregation). Ifrequiredgoes green, theGate on classifystep regressed — do not cutover. - merge_group dry-run (only if merge queue is enabled) → a
queued PR runs
pr-fastagainst the test-merge commit; result reports back to the queue. - Branch-protection cutover: after 7 days green on items 1–5,
replace the 8 required
Tier 1 / *checks with the singlePR Fast CI / requiredaggregate and deleteci.ymlin the same commit so GitHub never enforces a check from a deleted workflow.
Risk: Medium-high. Branch protection drift possible during transition. Two-workflow parallel window is the mitigation. The classify-failure simulation (validation step 5) is the critical check — it proves the v3 correctness fix actually works.
Scope:
- Flesh out
preview-artifacts.ymlwith the full structure in §2.5:gatejob (label / dispatch guard + SHA pinning).verify-pr-fast-greenjob (polls check-runs API, up to 10 min).build-windows(cargo xwin build --release --locked).build-test-archive(cargo nextest archive --locked).smoke-windows(windows-latest, same-SHA checkout, runs the archive).manifest(emitsmanifest.jsonwithgit_sha,tested_sha,cargo_lock_sha256,rustc_version,nextest_version, and per-file SHA256).
- Add a repo label
preview-binaries(maintainer-apply only). - Document the label-trigger workflow in
dev-flow.md.
Validation (each item MUST pass before Phase 5 is done):
- Label-trigger path: apply
preview-binariesto an open PR whosePR Fast CI / requiredis green → full preview workflow runs → artifacts appear in Actions UI with the SHA in their names. - Same-SHA integrity: download the Windows bundle +
manifest.json.manifest.git_shamust equal the PR's head SHA. Everyfiles[].sha256in the manifest must matchsha256sumof the downloaded file. - Nextest archive round-trip: on a real Windows box with
cargo-nextestat exactlymanifest.nextest_version,git checkout <sha>the repo and runcargo nextest run --archive-file <file>— must pass. - Pre-fast-gate enforcement — the critical check. Open a PR
that will fail
PR Fast CI(e.g. deliberate fmt violation). Applypreview-binarieslabel.verify-pr-fast-greenmust fail within 10 min; artifact jobs must not start. If the build proceeds, the gate is broken — do not merge Phase 5. - Fork-PR behaviour: open a PR from a forked repo. Label with
preview-binaries. Workflow must execute on GitHub-hosted runners only (verify by grepping forself-hostedin the workflow YAML). - Concurrency behaviour: push a new commit to a PR that already has a preview run in progress. The in-progress run must cancel, a new run must start against the new SHA, and no artifacts from the cancelled run should publish.
Risk: Medium. New workflow surface; isolated impact. Biggest risk is validation step 4 silently passing when it shouldn't — the poll-based check-runs gate is subtle. Always run a deliberate-failure test before trusting it.
Scope: exactly v1 §6.3. The git rev-list --count guard at the top
of step 11 in scripts/ci/ci-pipeline.rs.
Validation: reproduce the v0.5.71 "committed after push, re-ran ship, HEAD never landed" sequence. Before fix: exits in 2 s. After fix: re-runs step 11, push lands.
Risk: Low. Read-only git check + targeted state invalidation.
Scope: v1 §6.5.B. Move to scripts/ci-pipeline/ with Cargo.toml,
update just/workflow.just to cargo run -p ci-pipeline --release --.
Validation:
just ship --fresh— works as before, no rust-script cache.- Edit
ci-pipeline.rs, re-run — change takes effect immediately.
Risk: Medium. ~90 min restructuring; clean once done.
Deferred to a future session. See §2.7 for the original sketch and
docs/architecture/gates-manifest-plan.md
for the full implementation plan (schema, generator interface,
3-phase migration order, golden-file verification strategy, risk
analysis).
Branch protection on main is enforced via the rulesets API
(ID 11889528, name main-protection), not classic branch protection.
Classic /branches/main/protection returns 404 — verified
2026-04-23. Inspect or update via:
gh api repos/skyllc-ai/UltraFastFileSearch/rulesets/11889528
gh api --method PUT repos/skyllc-ai/UltraFastFileSearch/rulesets/11889528 --input <body.json>Verified against the ruleset on 2026-04-21, 6 entries (not 8 —
Tier 1 / Doc Tests and Tier 1 / Test Build were never required,
they run informationally):
Tier 1 / Clippy
Tier 1 / Tests
Tier 1 / Security
Tier 1 / File Size Policy
Tier 1 / Format
Tier 1 / Rustdoc
Ruleset was updated 2026-04-23 11:38 PDT to expand the required-check
set from 6 entries (the §4.1 baseline) to 7 entries, running
ci.yml and pr-fast.yml side-by-side as required lanes:
Tier 1 / Clippy
Tier 1 / Tests
Tier 1 / Security
Tier 1 / File Size Policy
Tier 1 / Format
Tier 1 / Rustdoc
PR Fast CI / required ← added for the parallel window
Both lanes had to pass; neither alone unblocked merge. This is the
state the (now-removed) pr-fast.yml:22-25 comment anticipated for
the 7-day bake-in. The window actually lasted ~2h35m (compressed
same-day — see §10.5 deviations for the decision rationale); cutover
executed 14:13 PDT the same day. PRs that merged during this window:
#45 (Phase 1-7 rollout, mixed rust+dep+infra), #46 (docs-only plan-
doc reconcile), #47 (Phase 4b infra-only actions hardening). Zero
disagreements observed between ci.yml and pr-fast.yml on any of
the three.
The plan v1 language "do everything in a single commit" was a
category error — the ruleset PUT is an API call, not a commit, so
it cannot be bundled with the ci.yml deletion into one atomic unit
reviewable via GitHub PR. Worse: if ci.yml is deleted in a PR, the
6 Tier 1 / * required checks reference a workflow that will not
run on that PR's head SHA → mergeStateStatus: BLOCKED with zero
failing and zero pending. GitHub will not merge until the ruleset
stops requiring checks that no workflow produces. Therefore the
ruleset PUT must precede the ci.yml-deletion PR merge.
Correct sequence (verified executed 2026-04-23):
- Open a PR that deletes
.github/workflows/ci.ymland updatesCHANGELOG.md. Let it bake green onPR Fast CI / requiredalone (the legacyTier 1 / *lane still runs against the PR branch via the unchanged ruleset, but is about to become no-signal). - PUT the ruleset with only
PR Fast CI / requiredin therequired_status_checksarray (drop all 6Tier 1 / *). The PR'smergeStateStatusflipsBLOCKED→CLEANwithin seconds. - Squash-merge the PR immediately (ideally within seconds of the PUT — see §10.5 deviation for the window-size rationale).
- Verify post-merge ruleset still has exactly 1 required check.
End-state required-check set (1 entry):
PR Fast CI / required
Evidence (2026-04-23):
- PR #48 squash-merge commit:
6f99b86aaat 14:13:41 PDT. - Ruleset PUT timestamp: 14:13:25 PDT (16-second window between PUT and merge; no new PRs opened in-window).
- Post-cutover verify:
gh api repos/skyllc-ai/UltraFastFileSearch/rulesets/11889528 | jq '[.rules[]|select(.type=="required_status_checks") | .parameters.required_status_checks[]|.context]'returns["PR Fast CI / required"]— single entry confirmed. - Ruleset backup pre-PUT saved to
/tmp/ruleset-rollback.jsonfor emergency reverse. Not committed — discard after 7 days if stable.
Rollback (if the cutover regresses post-landing):
git revert 6f99b86aa— restores.github/workflows/ci.ymlverbatim (squash-merge preserved full diff).gh api --method PUT repos/skyllc-ai/UltraFastFileSearch/rulesets/11889528 --input /tmp/ruleset-rollback.json— restores the 7-entry parallel-window shape.- Verify
gh pr view <N> --json statusCheckRollupon any open PR shows both lanes reporting.
Both steps are required; the revert alone is insufficient because the ruleset is separate state.
The context string matches the job's name: attribute, NOT the UI
display string. GitHub's PR UI concatenates <workflow-name> / <job-name> for display (e.g. PR Fast CI / PR Fast CI / required),
but branch-protection matches against check_run.name which is the
raw job name. Worked examples:
| Workflow | Job name: |
UI display | Ruleset context |
|---|---|---|---|
🧪 UFFS Tier 1 Nightly CI |
Tier 1 / Clippy |
🧪 UFFS Tier 1 Nightly CI / Tier 1 / Clippy |
Tier 1 / Clippy |
PR Fast CI |
PR Fast CI / required |
PR Fast CI / PR Fast CI / required |
PR Fast CI / required |
Getting this wrong produces mergeStateStatus: BLOCKED with zero
failing and zero pending checks — the protection engine waits forever
for a check-name that no workflow produces. Verify via:
gh pr view <N> --json statusCheckRollup \
--jq '.statusCheckRollup[] | select(.name | contains("required")) | .name'The returned value is the correct context string. This was hit and resolved live on 2026-04-23 (see §10.5 Deviations log).
| Risk | Mitigation |
|---|---|
Phase 3 flips CARGO_INCREMENTAL default; daily dev feels different |
One-line revert in .cargo/config.toml. Benchmarks in v1 §6.4 support the direction. |
| Phase 4 branch-protection drift during cutover | 7-day parallel window; both workflows required. |
| Phase 5 preview workflow triggered on every label event → runner cost | Gate on if: contains(... 'preview-binaries') and concurrency cancel-in-progress. |
| Phase 7 changes the ship entry point | Keep rust-script scripts/ci/ci-pipeline.rs ship working as a thin wrapper for one release cycle. |
| Smoke profile tests flaky on some machines | retries = 0, fail-fast = true means flakes block push — but the profile deliberately excludes integration tests which are the usual flaky class. |
Measured against current v0.5.71 baseline:
| Metric | Baseline | Target |
|---|---|---|
Median time to first actionable failure after git push |
CI (~5 min to first red) | T2 pre-push (< 30 s) |
| PR-blocking full-pipeline wall time | 8-15 min (ci.yml) | < 8 min (pr-fast) |
| Windows regression detection latency | release-time (~15 min into just ship) |
PR-time (pr-fast windows-lint, post-W5.5: clippy -D warnings not just compile-check) |
| Cargo-vet round-trip incidents per release | 1-3 today | 0 (hard-gated at T1) |
| Ship resumable state bugs per month | ≥1 (v0.5.71) | 0 after Phase 6 |
- Does not introduce a remote sccache. That's a future optimization ("Phase 9") worth evaluating only after Phases 1-7 land and baselines are measured.
- Does not add
cargo mutants/ fuzz. Deep-assurance lane expansion is out of scope for this plan. - Does not change Tier 2 cadence. Weekly cron stays weekly.
- Does not touch
release.yml. The release lane is correct today; the issue was artifact-producing lanes bleeding into PR critical path. - Does not adopt a pre-commit framework (e.g.
pre-committhe Python tool). Bash +juststays; rationale: one less dependency for new contributors. - Does not own release automation. Version bumping, changelog
generation, release PR cadence, tag creation semantics, and eventual
crates.io publishing are the concern of
release-automation-plan.md. Phases in that plan (R0-R9) are sequenced independently of Phases 1-8 here; they share onlyrelease.yml(unchanged by both) and the version- bump bits ofscripts/ci-pipeline/src/version.rs(retired in release-automation R5). - Does not own cross-target strict-lint convergence. Upgrading the
Windows lint gate from
cargo checktocargo clippy(1,346-lint backlog as of 2026-04-24) and adding a native macOS → Linux cross-check path viacargo-zigbuildare the concern ofwindows-clippy-and-linux-cross-plan.md. The dev-flow plan owns the 4-layer gate model (IDE / pre-commit / pre-push / PR CI); the cross-target plan owns which flag stack runs on which target at each of those layers.
Prerequisites before Phase 1:
- Current repo is green on
main. -
cargo-vetinstalled locally (cargo install cargo-vet --locked). - Confirmed
cargo nextestversion ≥ 0.9.130 (already pinned in.config/nextest.toml).
Order of operations: 1 → 2 → 3 → 6 → 4 → 5 → 7.
Rationale: hardened gates (1–3) first make local work reliable. Fix the ship-pipeline bug (6) next so release-time iteration doesn't fight us. Then CI split (4) once local is solid. Preview lane (5) plumbs in on top of the split. Binary promotion (7) is the clean-up step.
Phase 8 (gates.toml manifest) lands when the dust settles and we've lived with the split lanes for a release or two.
- Smoke profile selection rule — does
default-filterin thepre-push-smokenextest profile need to exclude more / fewer tests? Validate during Phase 1 rollout; tune before merge. - Windows preview smoke runner cost — GitHub-hosted
windows-latestat 10× the Linux minute rate. Consider a self-hosted runner if preview-label usage exceeds ~5×/week. - Preview-artifacts security surface — a malicious PR author could trigger preview builds. Today's model: gated on label which only maintainers apply. Re-evaluate if the contributor base grows.
- Bazel / Buck2 / build-server-protocol — not on today's roadmap but the gates.toml direction (§2.7) is a stepping stone if we ever want it.
Live dashboard. This section is updated as work lands, not in one go at the end. Future readers should be able to audit exactly how the plan was executed by reading this section — commits, dates, PRs, deviations, and measured metrics.
Status legend: ⬜ not started · 🟡 in progress · 🔵 blocked (see notes) · ✅ done · ❌ reverted
- Repo is green on
main—ci.ymlTier 1 all checks passing. -
cargo-vetinstalled locally (cargo install cargo-vet --locked). -
cargo-nextest≥ 0.9.130 confirmed (already pinned in.config/nextest.toml). -
cargo-xwininstalled for local Windows advisory (optional but recommended). - Dropbox / backup of current
.cargo/config.toml,just/shared.just,scripts/hooks/_lint_*.shin case of revert.
| # | Phase | Status | Started | Completed | Commit(s) | PR |
|---|---|---|---|---|---|---|
| 1 | Hardened pre-push gates (cargo vet + doctests + smoke profile) | ✅ | 2026-04-23 | 2026-04-23 | 780c1dbb1 (squash) |
#45 |
| 2 | Bucket-ordered scheduler + xwin removed from pre-commit | ✅ | 2026-04-23 | 2026-04-23 | 780c1dbb1 (squash) |
#45 |
| 3 | Cache policy single source (.cargo/config.toml owns incremental) |
✅ | 2026-04-23 | 2026-04-23 | 780c1dbb1 (squash) |
#45 |
| 6 | Resumable-push fix in ci-pipeline.rs |
✅ | 2026-04-23 | 2026-04-23 | 780c1dbb1 (squash) |
#45 |
| 4 | Split ci.yml → pr-fast.yml + preview-artifacts.yml |
✅ | 2026-04-23 | 2026-04-23 | 780c1dbb1 (parallel lane) + 6f99b86aa (cutover) |
#45 + #48 |
| 4b | Actions hardening retrofit across existing workflows | ✅ | 2026-04-23 | 2026-04-23 | eef3359b2 (squash) |
#47 |
| 5 | Preview lane fleshed out (smoke runner + manifest) | ✅ | 2026-04-23 | 2026-04-24 | 780c1dbb1 (plumb-in) + b9a67f2dc (robustness) + 2d3a7f5b3 (windows-latest + RC_PATH) + 0e811d0bb (test-vector fixes → full green bake) |
#45 + #51 + #52 + #55 |
| 7 | ci-pipeline.rs promoted to workspace binary |
✅ | 2026-04-23 | 2026-04-23 | 780c1dbb1 (squash) |
#45 |
| 8 | (stretch) gates.toml machine-readable manifest |
⬜ |
Phase 4 sub-status (2026-04-23, end-of-day): fully cutover.
Static implementation landed in 780c1dbb1 (PR #45, morning);
broken-classify simulation executed live on PR #45 and passed with
full fidelity (required=failure propagated correctly through 8
skipped downstream jobs); parallel window ran 11:38 → 14:13 PDT
(~2h35m; compressed from the planned 7 days, see §10.5); cutover
executed via PR #48 squash 6f99b86aa at 14:13:41 PDT; ruleset
main-protection PUT at 14:13:25 PDT reduced required-check set
from 7 entries (parallel) to 1 entry (PR Fast CI / required).
.github/workflows/ci.yml deleted. See §4.3 for the step-by-step
sequence and rollback procedure.
Phase 5 sub-status (2026-04-24, end-of-day): fully baked green
end-to-end. Static implementation landed in 780c1dbb1 (PR #45)
alongside the Phase 4 split. Three follow-up PRs closed out the
live validation:
- PR #51 (
b9a67f2dc, 2026-04-23) — robustness fixes surfaced by the first real full-matrix preview attempt:awk 'NR==1'guard on thecargo nextest --versionmulti-line output (bug #1), andverify-pr-fast-greenpolling budget bumped from 10 min to 30 min (bug #2, miscalibration for infra PRs that run the full matrix). - PR #52 (
2d3a7f5b3, 2026-04-24) — preview-lane bugs #3 and #4: movedbuild-test-archivefrom ubuntu+cargo-xwintowindows-latest(native MSVC) becausecargo-xwindoes NOT wrapnextest archive; and added anRC_PATHlookup pre-step tobuild-windowsbecausewinresource v0.1.31hardcodesPathBuf::from("llvm-rc")oncfg(unix)without PATH setup fromcargo-xwin. Preview run 24873800282 on this PR ran to completion for the first time;smoke-windowsexecuted 1322 tests with 1320 passing. - PR #55 (
0e811d0bb, 2026-04-24) — fixed bugs #5 and #6 (the two Windows-gated unit tests with stale hardcoded expected values that had NEVER executed in CI before the preview lane ran them; neither is actually platform-dependent — the original issue theories are withdrawn). Preview run 24889490616 on this PR: all 6 jobs green, 1322/1322 tests pass, realmanifest.jsonemitted with integrity data for all 17 artifacts (16 release binaries + 73.9 MB nextest archive).
See §10.5 for the full deviation log and §10.6 for Resolved blocker entries. Items #2, #3, #5 in §10.3 ticked with real evidence on 2026-04-24.
Legend: ✅ = complete and validated; 🟡 = static-complete (actionlint clean, ruby-yaml clean, pins/permissions correct, classify-aggregation gated) — remaining work is live-PR bake-in (broken-classify simulation, 7-day parallel window, branch-protection cutover) which cannot be validated pre-merge; ⬜ = not started.
Recommended execution order: 1 → 2 → 3 → 6 → 4 (+4b) → 5 → 7. See §8 Execution readiness for the rationale behind the reorder (Phase 6 before Phase 4 so ship-pipeline iteration doesn't fight us mid-cutover).
Copy-pasted from §3. Check items off as they pass in practice. Record any surprises under "Notes".
-
just lint-pre-pushon current HEAD — all green within 60 s. - Manual-mode (
bash scripts/hooks/_lint_pre_push.sh) triggersDEP_CHANGED=1fallback; cargo-vet runs and passes. - Uninstall
cargo-vet, repeat — hard-fails with install hint. (Skipped in rollout; validated by code review — hard-fail path is a 3-lineif ! command -v cargo-vet … exit 2.) - Introduce a deliberate doctest failure — pre-push aborts.
(Skipped in rollout; validated by code review —
doc-testsgate spawnscargo test --docwhich surfaces rustdoc failures.) - Measured pre-push runtime (warm): 43 s (target < 60 s).
- Smoke profile runtime (nested): 5.9 s wall / 1140 tests run / 11 skipped via profile.
Notes: default-filter in nextest profiles treats unknown binary
names as parse errors, not warnings, so the original plan's exclusion
of benchmark_filtering / benchmark_sorting had to be dropped —
those criterion-style benches aren't surfaced as nextest binaries.
Filter now denylists only the test-name prefix (test_validate_) and
the uffs-client package. Runtime remains within budget.
- Introduce a type error →
cargo checkshows red within ~15 s (before the clippy batch). Skipped in rollout; fail-fast logic validated by code review —run_seqsetsSEQ_FIRST_FAILon first failure, subsequent calls short-circuit toskip. - Pre-commit on staged-empty change: 3 s warm (base load: file-size + fmt-check + typos + reuse).
- Pre-push (warm, manual mode, CODE_CHANGED=1): 40 s total.
Per-Bucket-2 step:
cargo-check1 s,lint-ci8 s,lint-prod4 s,lint-tests7 s,rustdoc1 s,doc-tests9 s,tests2 s,smoke7 s,deny0 s,check-windows1 s. -
_lint_pre_push.sh.bakkept as rollback for one cycle. Superseded by git history — reverts are atomic at the commit level.
Notes:
- Removed
taplofrom pre-push Bucket 1 — runningtaplo fmt --checkworkspace-wide surfaces pre-existing TOML drift that is out of scope for the push-being-validated. Taplo stays at pre-commit where the staged-scope is natural. cargo deny checkdoes not accept--locked(unlike cargo-vet); cargo-deny readsCargo.lockdirectly. Removed the--lockedsuffix. The doc's "--lockedon every cargo subcommand that supports it" is still accurate —cargo denyis not a cargo subcommand in the resolver sense.- Bucket 2 ordering proved out: cargo-check runs in 1 s, so a type error reports in ~1-2 s instead of waiting ~40 s for clippy.
-
cargo check --workspace --all-targets --all-features --lockedafterincremental = falselanded: 8.36 s (unchanged from baseline — config change didn't invalidate sccache). - Pre-push warm after Phase 3: 42 s (same as Phase 2; config change did not degrade cache reuse).
- Pre-push cold-ish (first run after config edit): 72 s (rustdoc jumped 1 s → 18 s on the invalidation wave, then back to 1 s on the subsequent warm run — cache re-established).
-
just ship -vthrough to push — pre-push green. Deferred until after Phase 6 lands (Phase 6 is the ship-bug fix; testing ship with the Bug A present is circular). - Direct
bash scripts/hooks/_lint_pre_push.shfrom fresh shell with no env overrides — pre-push green. Proves drift is gone. -
shared.justno longer exportsCARGO_INCREMENTAL. -
.cargo/config.tomlhasincremental = falsepaired withrustc-wrapper = "sccache". -
ci-pipeline.rssimplified — theCARGO_INCREMENTAL=0explicit push is gone; pipeline only re-assertsRUSTC_WRAPPER=sccachebecause git itself reads no Cargo config. Diagnostic[ci-pipeline][sccache-fix]eprintln removed too (served its purpose as stale-binary detector during v0.5.71 triage). -
rust-script --test scripts/ci/ci-pipeline.rscompiles clean.
Notes:
- Left
export RUSTC_WRAPPER := ""injust/shared.justuntouched. The plan didn't mandate its removal. It encodes an intentional "just recipes opt out of sccache by default" policy; specific recipes that need sccache (just cache:*,just ship) set it back explicitly. Changing that semantic is out of scope for Phase 3 — it would require auditing every recipe for cache assumptions. - Env var still overrides Cargo config per Cargo's resolution
order, so any user who prefers incremental can still
CARGO_INCREMENTAL=1 cargo buildone-off. Default is the right default; override stays available.
- Reproduce the v0.5.71 sequence: ship → commit more → re-run ship. Deferred to next real ship run (requires an in-flight release PR; no safe way to dry-run without altering remote state).
-
count_unpushed_commitshelper added,git rev-list --count origin/<branch>..HEADparsed safely. Conservative fallback returns 1 when remote ref doesn't exist (first push). -
invalidate_stepmethod added toWorkflowState— drops fromcompleted_stepsandfailed_steps, saves state only if something actually changed. - Guard at Step 11 call site:
unpushed > 0 && state.is_step_completed(STEP_GIT_PUSH)→ invalidate + re-run. -
rust-script --test scripts/ci/ci-pipeline.rscompiles clean.
Notes:
- Validation step 1 (reproduce v0.5.71 sequence) is deferred because the natural test requires an actual in-flight release branch with a follow-up commit, which would perturb the git history on origin. Will exercise on the next legitimate ship run where a CI-detected failure triggers a follow-up commit.
- Helper uses
tokio::process::Command(imported asCommandin this module — the async version). Syncstd::process::Commandwould also work here but the helper isasyncto fit the pipeline style used throughout.
-
.github/workflows/pr-fast.ymlcreated — 11 jobs:classify,file-size,fmt,sanity,clippy,docs,test-build,tests,security,windows-check,required, plusnotify-failure. -
.github/workflows/preview-artifacts.ymlcreated as full Phase 5 implementation (label gating + verify-pr-fast-green + xwin build + nextest archive + smoke + manifest). -
merge_group:trigger present inpr-fast.yml. - Concurrency:
cancel-in-progress: pull_request only. - Least-privilege workflow permissions:
contents: read. - All action uses pinned to full commit SHAs (reuses existing
ci.yml/release.yml/tier-2.ymlpins). - Both workflows pass
actionlintclean. - Both workflows pass
ruby -ryamlparse check. -
requiredjob: * Hasif: always(). * Has explicitneeds: [classify, ...]. * First step gates onneeds.classify.result == 'success'— this is the static fix for the highest-leverage sanity check (plan doc § 10.3 classify-failure bullet). * Second step aggregates downstream results,success|skippedpass, anything else fails. - Broken-classify simulation on a real PR — ✅ executed
2026-04-23 on PR #45. Inserted
exit 1as the FIRST step of theclassifyjob (before outputs were populated), observed:classify=failure (4 s), 8 downstream jobs skipped (emptyneeds.classify.outputs.*),required=failure (4 s, via the explicitGate on classifystep), legacyTier 1 / *lane stayed all green. Reverted same day; PR #45 merged green. This proves therequiredaggregator fails correctly when downstream jobs are skipped (the exact GitHub Actions "skipped counts as success" trap the plan warned about). - Docs-only PR real-world bake:
pr-fastskips the heavy jobs (code == 'false');required= success. 🟡 Exercised by this very PR (the plan-doc update). - Dep-only PR real-world bake: Cargo.toml bump runs full matrix + cargo-vet in sanity. Deferred to next dependabot or explicit version-bump PR.
- Infra-only PR real-world bake: workflow/justfile edit runs full matrix. Partially covered by PR #45 (mixed rust+dep+infra change — infra classification branch exercised green). Still want a pure-infra bake for completeness.
- Rust-change PR real-world bake (mixed rust+dep+infra on
PR #45):
pr-fastp50 wall time ~7 min (rustdoc+doctests dominates; next-longest isclippy~1 min,sanity~1 min,testsburied insidetest-build+testspipeline). Legacyci.ymlon the same PR: comparable ~5–7 min p50. No regression. - Parallel window with
ci.yml(dates: 2026-04-23 11:38 PDT → 2026-04-23 14:13 PDT, ~2h35m). Compressed from the planned 7 days — see §10.5 for decision rationale. Zero disagreements observed between the two lanes on PRs #45, #46, and #47 (the three PRs that merged in-window). - Branch-protection cutover:
ci.ymldeleted (PR #48 squash6f99b86aa, 14:13:41 PDT); rulesetmain-protectionPUT 14:13:25 PDT reducedrequired_status_checksfrom 7 entries to 1 (PR Fast CI / required). The plan v1 wording "in the same commit" was corrected to "ruleset PUT precedes the merge" — see §4.3 and §10.5.
Notes:
- Rather than pulling in
dorny/paths-filter,classifyuses a nativegit diff --name-only BASE..HEADshell step. Pros: no new third-party action to vet (supply-chain); transparent regex we own. Cons: slightly more shell code. Trade-off documented here so future maintainers don't swap back without discussion. securityjob re-runscargo vet check --lockedeven thoughsanityalso runs it whendep_changed. This is intentional: external observers (branch protection, GitHub search for "vet") expect a distinctSecuritycheck in the check-runs list. Cost is ~10 s on warm cache.windows-checkoriginally usedcargo check(notbuild) — the PR-fast lane only needs compile-confidence. Full release-shaped builds live inpreview-artifacts.ymlwhere they belong. Phase W5.5 update:windows-checkwas renamed towindows-lintand the command flipped tocargo clippy --workspace --all-targets --all-features --locked --no-deps -- -D warningsonce the Windows clippy backlog hit zero (PR #62 cleared 1346 errors → 0). PR-fast Windows now catches both compile errors AND lint regressions.
Audit each existing workflow against §2.8 policy:
-
tier-2.yml— narrowed workflow-levelpermissionsfromcontents: read, issues: writeto justcontents: read(notify-failure re-declaresissues: writeon its own block, so the broader scope was over-privileged for the other 7 jobs); addedtimeout-minutes: 2to notify-failure (only job that was missing one); added--lockedto all 5 cargo invocations (coverage / windows-check / build-validation / udeps / miri). -
codeql.yml— added explicitref: github.event.pull_request.head.sha || github.shaon the checkout so CodeQL analyses the exact bytes pushed / proposed instead of the synthetic merge commit thatactions/checkoutdefaults to onpull_requestevents. -
release.yml— added--lockedto the matrixcargo build --releasestep. Deliberately did NOT refactor the workflow-level permissions block (sophisticatedcontents/actions/id-token/attestations/issuesgrants are all commented + justified; tightening is a distinct change best kept out of a housekeeping retrofit commit). -
auto-tag-release.yml— already conformant; verified (1) no PR trigger somerge_group:N/A, (2) minimum-privilegecontents: read, actions: writewith comments, (3) single job withtimeout-minutes: 5, (4) no cargo commands (pure git + gh CLI). Zero changes. -
cargo-vet-refresh.yml— already conformant; verified (1) no PR trigger, (2) minimum-privilegecontents: write, pull-requests: write(both needed for refresh-PR creation, both justified with comments), (3)timeout-minutes: 10, (4)cargo vet check --lockedalready present, (5)cargo vet regenerate imports/cargo vet prunedeliberately run unlocked (they mutate the lockfile by design). Zero changes. -
dependabot-review.yml— added explicitref: github.event.pull_request.head.sha || github.shaon checkout sogit show HEAD~1:Cargo.lockis deterministically the pre-bump lockfile, not whatever the synthetic merge commit resolves to. Other §2.8 properties already conformant (contents:read + pull-requests:read, actor-gated to Dependabot only,timeout-minutes: 3, full-SHA action pins). -
dependabot-auto-merge.yml— already conformant; verified (1) PR trigger but actor-gated somerge_group:wouldn't fire anyway, (2) minimum-privilegecontents: write, pull-requests: writeforgh pr merge --auto, (3)timeout-minutes: 5, (4) no checkout step (uses dependabot/ fetch-metadata + gh CLI), (5) full-SHA action pins. Zero changes.
Notes:
merge_group:triggers were NOT added retroactively because §2.8's requirement is scoped to "workflows that report required checks". Today that's onlypr-fast.yml(and the soon-to-be- deletedci.yml). The 7 workflows audited here don't report required checks, so merge-queue compatibility is moot until that changes.- The permissions refactor in
release.yml(workflow-levelcontents: write→ workflow-levelcontents: read+ per-job write grants oncreate-github-release) was deliberately scoped out of this pass. Release infra is critical path; a per-job permissions restructure deserves its own focused PR with a full release dry-run. - Pre-existing shellcheck
style/warningnotes in unmodified shell blocks (tier-2-summary, release-preparation summary, dependabot-review summary) are explicitly out of scope — cleaning them up would balloon the diff and obscure the actual hardening. Track them separately if ever desired.
Post-retrofit verification (2026-04-23):
actionlintexits 0 on all 4 modified files (remaining warnings are pre-existing style-level SC2129 / SC2010 in unmodified blocks).ruby -ryaml -e 'YAML.load_file(...)'passes on all 4 files.- Diff touches only the 4 files listed above; zero behavioural changes on already-conformant workflows.
Original notes:
-
gatejob guards onpreview-binarieslabel ORworkflow_dispatch. -
gatepins the SHA (needs.gate.outputs.sha) for every downstream checkout / artifact name — prevents drift onsynchronizeevents. -
verify-pr-fast-greenpolls GitHub Checks API forPR Fast CI / requiredon the pinned SHA; up to 60 × 10 s retry; fails fast on non-success. -
build-windowsusescargo-xwinon ubuntu-22.04 targetingx86_64-pc-windows-msvc; caches viapreview-windowsshared-key; uploadsdist/windows/*.exe. -
build-test-archivebuilds nextest archive (uffs-tests-<sha>.tar.zst); exposesnextest_versionas job output so the manifest can advertise it. -
smoke-windowsruns on GitHub-hostedwindows-latest(not self-hosted) and executes the archive against the same-SHA checkout — both nextest archive requirements satisfied. -
manifestemitsmanifest.jsonwithgit_sha,tested_sha,cargo_lock_sha256,rustc_version,nextest_version, per-file SHA256 for every downloaded artifact. -
Concurrency:
group: preview-${workflow}-${pr#|sha},cancel-in-progress: true— push during in-progress preview cancels it (satisfies plan validation bullet). -
Permissions:
contents: read,checks: read,pull-requests: read(needed for Checks API + label lookup). -
Both workflows pass
actionlintclean. -
Label-trigger path real PR validation: apply
preview-binarieslabel to a green PR → workflow fires → artifacts appear in Actions UI. ✅ Baked on PR that landed this edit (scratchtest/phase-5-preview-bake, 2026-04-23). -
Same-SHA integrity real validation:
manifest.git_sha== PR head SHA; everyfiles[].sha256matchessha256sumof downloaded file. ✅ Validated 2026-04-24 on PR #55 preview run 24889490616 (SHAb3c73166e1709aba91c225232def9ff44d255fcc). First end-to-end green preview bake ever — all 6 jobs succeeded includingmanifestemission. The emittedmanifest.jsoncontains: -git_sha=b3c73166e1709aba91c225232def9ff44d255fcc(exactly the PR head SHA) -tested_sha= same (tests ran against the same commit whose artifacts were built) -cargo_lock_sha256=1b5d20222501417f5392280efdb8483b94088c15600ace8b63010270755317cc-rustc_version=rustc 1.97.0-nightly (913e4bea8 2026-04-22)-nextest_version=0.9.132-target=x86_64-pc-windows-msvc-build_os=ubuntu-22.04 (cargo-xwin)-files[]= 17 entries (16 release binaries + 73.9 MB nextest archive), each withpath,sha256,bytesThe manifest was downloaded locally on 2026-04-24 and every `files[].sha256` was verified against `sha256sum` of the downloaded file — all matched. Earlier out-of-band evidence from PR #52 run 24873800282 (before bugs #5/#6 were fixed) is superseded by this run but remains in the §10.5 audit trail. -
Nextest archive round-trip validation: Windows box with matching
nextest_versionfrom manifest,git checkout <sha>,cargo nextest run --archive-file. ✅ Validated on PR #55 preview run 24889490616 (smoke-windowsjob) with 1322/1322 tests passing after bugs #5 / #6 were fixed. First-time execution on PR #52 (run 24873800282) surfaced two stale test vectors (hardcodedassert_eq!values that had never been validated anywhere because the containing modules were#[cfg(windows)]-gated — see §10.5 bug #5/#6 root-cause update 2026-04-25). Fixed in PR #55 (closes issues #53 / #54). The round-trip MECHANISM itself was already proven on PR #52 with 1320/1322 passing; this is the final 2/2 that completes the picture and unlocks themanifestjob (which isneeds:-coupled tosmoke-windows). Full external-box round-trip (a separate Windows dev box, not a GitHub runner) still deferred until such a box is available, but the GitHub-hostedwindows-latestbake is sufficient evidence that the archive format is portable across two distinct Windows environments (the build machine runningcargo-xwinon ubuntu-22.04 and the test machine on native MSVC). -
Pre-fast-gate enforcement validation: deliberate
PR Fast CIfailure blocks preview build. ✅ Baked on same PR via a temporary sabotage commit (exit 1as the first step of thefile-sizejob — unconditional, fast-failing, cheapest);PR Fast CI / required= FAILURE on SHA0600ce674, andverify-pr-fast-greencorrectly detected the red aggregator at poll retry 48/120 and setcore.setFailed, sobuild-windows/build-test-archive/smoke-windows/manifestall stayedskipped— zero Windows runner minutes spent. Sabotage reverted before merge. -
Fork-PR behaviour validation: completed via static analysis on 2026-04-24; live fork-PR bake will naturally occur with the first external contribution. The preview lane is designed to be fork-PR-safe by construction:
1. **Runner safety**: all jobs use `runs-on: ubuntu-22.04` or `windows-latest` (no `self-hosted` anywhere in the workflow). Verified by grep on 2026-04-24. A fork PR cannot be used to exfiltrate self-hosted runner state or access internal networks. 2. **Secret safety**: the workflow uses **no secrets** — grep-verified 2026-04-24 (no `secrets.*` references, no hardcoded PATs, no references to repository or organization secrets). The only credential used is the auto-provisioned `GITHUB_TOKEN`, which GitHub downgrades to **read-only** for fork PRs. That read-only posture is still sufficient for our workflow's needs: - `gate` and `verify-pr-fast-green` only `gh api`-read PR metadata and check-run statuses (read-only). - `build-windows` / `build-test-archive` / `smoke-windows` do not call `gh api` at all. - `manifest` only reads the downloaded artifacts and uploads its own. - `actions/upload-artifact` uses the Actions runtime credential (not `GITHUB_TOKEN`), which IS available on fork PRs. Confirmed 2026-04-24 by re-reading GitHub's [fork PR permissions doc](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-conditions-to-control-job-execution#permissions-for-fork-pull-requests). 3. **Label-trigger safety**: fork PRs cannot apply labels themselves (labels require write access to the target repo). Only a maintainer can apply `preview-binaries`, which means no adversarial fork PR can self-trigger a build — the label acts as both a trigger AND an implicit maintainer approval for running the workload. 4. **Concurrency safety**: the `concurrency.group` key is `preview-${workflow}-${pr_number|sha}` with `cancel-in-progress: true`. Multiple fork-PR label events on the same PR cancel prior runs, preventing resource exhaustion. Remaining deferred for a future fork PR (natural occurrence, not proactively simulated): (a) confirm the `labeled` event fires on fork PRs exactly as on in-repo PRs; (b) confirm `pull_request.head.sha` is correctly set to the fork's head (not a merge-commit SHA); (c) confirm artifact upload succeeds end-to-end with the downgraded token. If any of these fail in practice, document in §10.5 and add a regression step to this list.
Notes:
- The
verify-pr-fast-greenjob is the critical gate that prevents wasting runner minutes on non-mergeable SHAs. Polling budget: 120 × 15 s = 30 min,timeout-minutes: 32. Originally 10 min; recalibrated 2026-04-23 after the first full-matrix preview attempt timed out withPR Fast CI / requiredstill atstatus=missing(the aggregator hadn't been registered as a check-run yet — tests job still running). See §10.5 deviations log. If a future change pushes PR Fast CI beyond 30 min at p99, bump the cap — don't lower the preview gate's retry budget. build-windowscross-compiles viacargo-xwinonubuntu-22.04(preview-windowscache).build-test-archivebuilds natively onwindows-latest(preview-test-archive-windowscache). The two jobs intentionally do NOT share a cache — Linux and Windows runner caches cannot cross, andcargo nextest archiveis unsupported bycargo-xwinanyway (xwin only wrapsbuild|check| test|run|clippy|rustc). See §10.5 2026-04-24 correction entry for the Windows-native rationale.- The manifest's
cargo_lock_sha256is a stronger integrity anchor than trusting the artifact alone — if someone later asks "which crates versions were in this preview", they can reconstruct deterministically from this + the git SHA.
-
scripts/ci-pipeline/Cargo.tomlcreated. -
scripts/ci-pipeline/src/main.rsis the authoritative implementation (1735 lines, previously 1752 in the rust-script minus the 17-line shebang / deps header). - Workspace
membersinCargo.tomlupdated:"scripts/ci-pipeline"added alongside the crates. -
Cargo.lockregenerated and tracksuffs-ci-pipeline v0.5.71. -
cargo check -p uffs-ci-pipeline --locked→ clean (6.77 s cold, 0.17 s warm). -
cargo run -q -p uffs-ci-pipeline -- --help→ prints the same subcommand table as before the promotion. -
just/workflow.just,just/dev.just,just/bench_ci.just— everyrust-script scripts/ci/ci-pipeline.rs <cmd>replaced withcargo run -q --release -p uffs-ci-pipeline -- <cmd>. -
ci.ymlpath filter additionally watchesscripts/ci-pipeline/**so the parallel window catches changes to the new crate. - Old
scripts/ci/ci-pipeline.rskept as a thin wrapper (49 lines) that prints a deprecation notice and re-execscargo run -q --release -p uffs-ci-pipeline -- "$@". Verified executable viarust-script scripts/ci/ci-pipeline.rs --help. MarkedREMOVE-AFTER: v0.5.73in the header. - Real-world bake-in on a live
just shiprun. Deferred to the next release cycle.
Notes:
- The new crate intentionally does not opt into
[lints] workspace = true. The workspace lints are calibrated for production library/CLI code (deny unwrap, denyprintln!, deny missing_docs_in_private_items). Inheriting them for this tool would require thousands of lines of edits without making the tool materially better. Opting out is documented in a header comment on the crate's Cargo.toml so future maintainers don't silently re-enable them. - Package name:
uffs-ci-pipeline(notci-pipelineas named in the plan spec). Matches the workspace convention that every workspace-local crate isuffs-*, and avoids a future collision with any upstream crate published to crates.io asci-pipeline.publish = falseis set to enforce we never accidentally publish. tokioworkspace baseline is net-free; this crate re-enables theprocessfeature viafeatures = ["process"]on the workspace alias. Feature unions are additive, so no other crate sees this addition.
- Deferred — re-evaluate after 2 release cycles with lanes split.
Notes:
Fill the baseline column at Phase 1 start; fill each phase column
at its completion. Empty cells mean "not yet measured"; — means
"metric does not apply to this phase". Record actual values, not
pass/fail, so regressions are visible across the refactor.
| Metric | Baseline | P1 | P2 | P3 | P4 | P5 |
|---|---|---|---|---|---|---|
| pre-push wall (warm) | 23–45 s | — | — | |||
| pre-push wall (cold) | 60–90 s | — | — | |||
| pre-commit wall (Rust change, warm) | 15–25 s | — | — | — | ||
| Pre-push time to first red on vet failure | ∞ (CI-only) | ~1 s | — | — | ||
| Pre-push time to first red on type error | ~40 s (clippy) | ~15 s | — | — | ||
Cold cargo build --workspace |
— | — | — | — | — | |
| Warm rebuild after single-file touch | — | — | — | ~42 s target | — | — |
| PR-fast wall (warm, p50) | 8–12 min (ci.yml) | — | — | — | ≤ 8 min | — |
| Preview-artifacts wall (warm) | n/a | — | — | — | — | ≤ 30 min |
| Windows regression detection latency | release-time | — | — | — | PR-time | PR-time |
Any step that diverged from the plan — record here so future readers don't infer the wrong reason from commit messages.
| Date | Phase | Deviation | Resolution |
|---|---|---|---|
| 2026-04-23 | 4 | Plan §4 claimed 8 Tier 1 / * checks were required. Actual ruleset had 6 (no Doc Tests, no Test Build). |
Corrected baseline to 6 entries in §4.1; parallel-window math in §4.2 adjusted accordingly. |
| 2026-04-23 | 4 | First ruleset PUT used context "PR Fast CI / PR Fast CI / required" (matching the UI display string). PR entered mergeStateStatus: BLOCKED with zero failing checks because no real check reports under that name — the protection engine matches check_run.name = job name: attribute only. |
Second PUT used "PR Fast CI / required" (bare job name). PR went to CLEAN/MERGEABLE. Gotcha documented in §4.4 to prevent recurrence at Tier 1 cutover. |
| 2026-04-23 | 4 | Plan §10.3 scheduled a 7-day parallel window (2026-04-23 → 2026-04-30). Compressed to ~2h35m same-day on explicit maintainer direction. | Rationale: the confidence budget had already been exhausted by the same morning — broken-classify simulation executed and passed on PR #45, all four classification paths (mixed-code, docs-only, infra-only, broken-classify) validated with zero disagreements between lanes, and continuing the window would have burned ~5–7 min of runner time per PR for no additional signal. Rollback path preserved: git revert 6f99b86aa restores ci.yml verbatim (no squash-merge loss), and /tmp/ruleset-rollback.json restores the 7-entry ruleset shape. §4.3 updated with the two-step reverse sequence. |
| 2026-04-23 | 4 | Plan v1 §4.3 instructed "do everything in a single commit" for the cutover. This is a category error — the ruleset PUT is an API call, not a commit, so it cannot share atomicity with .github/workflows/ci.yml deletion. Worse: with ci.yml deleted in a PR, the 6 Tier 1 / * required checks gate on a workflow that will never run on the PR's head SHA, producing mergeStateStatus: BLOCKED indefinitely. |
Correct sequence (now in §4.3): (1) open the ci.yml-deletion PR and let it bake green on PR Fast CI / required; (2) PUT the ruleset BEFORE merge to drop the 6 Tier 1 / * checks; (3) squash-merge the PR; (4) verify. Executed in a 16-second PUT→merge window on 2026-04-23 14:13:25 → 14:13:41 PDT with no PRs opened mid-window. |
| 2026-04-23 | 5 | preview-artifacts.yml's build-test-archive step captured the nextest version with echo "version=$(cargo nextest --version | awk '{print $2}')" >> "$GITHUB_OUTPUT". cargo nextest --version on 0.9.132 emits multiple lines where $2 evaluates to 0.9.132 on more than one of them; the command substitution preserves the inner newlines, and GitHub Actions rejects the resulting multi-line value with Error: Unable to process file command 'output' successfully. Error: Invalid format '0.9.132'. Found by the live Phase 5 bake on the scratch PR that ticks items #1/#2/#4. |
Constrained awk to NR==1 so only the first output line is considered. Minimal one-word edit; no change to the rest of the step. Regression-guard comment added to the workflow explaining the failure mode so the fragility is not silently reintroduced. Unblocks the bake; the same PR that fixes the bug also lands the resulting Phase 5 ticks in §10.3. |
| 2026-04-23 | 5 | build-test-archive failed on ubuntu-22.04 with cc-rs: failed to find tool "lib.exe" from ring v0.17.14's build.rs. Proximal cause: cargo-xwin wraps build|check|test|run|clippy|rustc but NOT nextest archive, so the bare cargo nextest archive --target x86_64-pc-windows-msvc invocation ran without MSVC env. Deeper cause surfaced while diagnosing the fix: cargo nextest archive defaults to debug profile, and debug xcompile for Windows is currently blocked at a separate layer by the polars-ops 5.5 GB rlib → COFF archive string-table-offset limit (see docs/xwin-msvc-rlib-size-root-cause-and-workarounds.md). Even moving build-test-archive to windows-latest (native MSVC) would not resolve this because the COFF archive size ceiling is format-level and link.exe shares it. Release-mode xcompile works (confirmed locally on mac), which is why the sibling build-windows job — cargo xwin build --release --bins — succeeds. |
Deferred to the concurrent branch that fixes the polars-ops rlib size via an xwin-dev profile + per-package overrides (from §6 of the root-cause doc). Rolled back my attempt to move build-test-archive to windows-latest — it would have shifted the failure mode without fixing anything, and it would have collided with the concurrent branch's xwin-centric approach. Phase 5 checklist item #2 (same-SHA integrity) reverted to un-ticked with a cross-reference back to this entry; item #3's "partially satisfied by smoke-windows" note softened to "will be, once the polars blocker is resolved". |
| 2026-04-25 | 5 | Bug #5 / #6 root-cause update. Investigation of issues #53 / #54 revealed that BOTH "Windows portability" bugs surfaced by smoke-windows on PR #52 were mis-framed by the issue reports. Neither is actually platform-dependent behavior. Both are just stale hardcoded expected values that had never been executed in CI before the preview lane ran them on 2026-04-24: (a) fnv1a_known_vector hardcoded 0x8584899336065430 but the impl produces the canonical FNV-1a-64 of "foobar" = 0x85944171F73967E8 (matching isthe.com/chongo/src/fnv/test_fnv.c reference); the hardcoded value is simply wrong and would fail on any platform the test actually ran on. (b) test_pipelined_reader_creation hardcoded 64 * 1024 for all three drive types but DriveType::optimal_chunk_size() returns 2 MiB / 1 MiB / 1 MiB for Ssd/Hdd/Unknown \u2014 the hardcoded value was a stale prototype number, never updated when the chunk-size tuning was revised. Both tests are inside #[cfg(windows)] gates (pipe.rs at the module level, pipelined at the mod pipelined; declaration in readers/mod.rs), so pr-fast.yml's Tests job (ubuntu-only) never compiled them, and Windows compile check compiles but does not execute. Result: the assertions had never executed anywhere. The platform-dependency theories in the original #53 / #54 issue bodies are withdrawn. The preview lane earned its keep more than advertised \u2014 it found untested code, not platform-specific bugs. |
Single combined fix PR (supersedes separate fix PRs for #53 / #54): fix(tests): correct stale expected values in Windows-gated unit tests. Changes: (a) \u00a710.5 bug #5 expected value \u2192 0x8594_4171_F739_67E8; (b) \u00a710.5 bug #6 expected values derived from DriveType::optimal_chunk_size() itself, eliminating the hardcoded drift vector so future tuning changes cannot silently break this test again. Both fixes carry block comments anchoring them to this row + the referenced issues. Lesson: any test gated behind a platform cfg that is NOT exercised by pr-fast.yml is effectively dead code; the Phase 5 preview lane is currently the only CI location that ever exercises cfg(windows) test bodies. See \u00a710.3 Phase 5 follow-up items for the plan to close this gap systemically (beyond fixing these two specific tests). |
| 2026-04-23 | 5 | verify-pr-fast-green's polling budget (60 × 10 s = 10 min, timeout-minutes: 12) was calibrated assuming the target PR would be docs-only / short-circuited. On the first real full-matrix preview attempt (this same scratch PR, SHA 3ef74bd5f — which counts as an infra change because it touches .github/workflows/*.yml), PR Fast CI's tests job was still running at minute 10 so the poller kept seeing status=missing (the PR Fast CI / required aggregator had not yet been registered as a check-run). Preview failed at verify-pr-fast-green with ⏱ Timed out waiting for PR Fast CI / required on 3ef74bd, which aborted build-windows / build-test-archive / smoke-windows — a false-negative gate: the PR would have gone green ~5 min later. §10.3 Phase 5 notes (v2 wording) anticipated this with "if PR-fast is slower than 10 min, increase the cap in one commit", but nobody had exercised it against a real full-matrix PR before. |
Bumped polling to 120 × 15 s = 30 min and timeout-minutes: 32. Factored the magic numbers into named MAX_RETRIES / RETRY_DELAY_MS constants so the next calibration isn't a hunt. Expanded the job's header comment with the rationale and an explicit "don't drop below p99 PR Fast CI wall-clock" guardrail. Same PR as the nextest fix above. |
Active blockers visible at a glance. Move to "Resolved" below with a one-line outcome when cleared.
Active:
- Polars-ops
xwin-debug DX blocker (narrowed scope) — debug-profile cross-compile from macOS viacargo xwintox86_64-pc-windows-msvcproduces a ~5.5 GBpolars-ops.rlibthat exceeds the COFF archive format's string-table offset capacity, yieldinglld-link: truncated or malformed archiveat final link. Previously catalogued as a CI-lane blocker for the preview workflow; verified 2026-04-24 (see §10.5) to be specific to the LLVM cross-compile toolchain (lld-link/llvm-libused bycargo-xwin), not the native MSVC toolchain (link.exe/lib.exeused bywindows-latestrunners and local VS 2026 dev boxes). The preview CI lane is unaffected becausebuild-test-archivenow runs onwindows-latest. Scope remaining: macOS developers who want to debug-xcompile the workspace locally. Root cause + fix recipe stays documented indocs/xwin-msvc-rlib-size-root-cause-and-workarounds.md(dedicatedxwin-devprofile + per-package polars overrides reducing debuginfo / codegen-units / opt-level) for that path. - Real-world bake gaps — pure
Dep-only PRand pureInfra-only PRclassification paths still want a natural exercise post-cutover (onpr-fast.ymlalone, now thatci.ymlis gone). PR #47 was the last infra-only bake in the parallel-window era; first post-cutover dependabot run and first post-cutover infra-only PR will close these opportunistically. Don't synthesize a test PR just for this. - Phase 4b
release.ymlpermissions refactor — workflow-levelcontents: write→ per-job grants oncreate-github-releaseonly. Deliberately scoped out of PR #47 per its scope note; track as a future focused PR with a release dry-run.
Resolved:
- Phase 5 wrap-up + full green end-to-end bake — ✅ 2026-04-24,
on PR #55 preview run 24889490616. All 6 preview jobs succeeded
for the first time ever;
manifest.jsonemitted with realfiles[].sha256integrity data. §10.3 items #2, #3, #5 all ticked with real evidence (not out-of-band substitutes). No separate wrap-up PR needed — PR #55 carried both the test fixes AND the final evidence update in one atomic change. - Issue #53 (uffs-security fnv1a) — ✅ 2026-04-25, closed by PR #55. Root cause was NOT a Windows platform dependency (as the original issue theorized) but a stale hardcoded test vector that had never executed in CI. See §10.5 bug #5/#6 root-cause update entry.
- Issue #54 (uffs-mft pipelined buffer size) — ✅ 2026-04-25,
closed by PR #55. Same root cause as #53: stale hardcoded
expected value. Fix derives the expected value from
DriveType::optimal_chunk_size()itself to eliminate the drift vector. - Broken-classify simulation — ✅ 2026-04-23, on PR #45.
requiredcorrectly propagated failure through 8 skipped downstream jobs. - Ruleset context-string gotcha — ✅ 2026-04-23, same day. See §4.4 for the write-up.
- 7-day parallel-window bake — ✅ 2026-04-23. Compressed to ~2h35m same-day on maintainer direction; all four classification paths validated with zero lane disagreements. See §10.5.
- Phase 4b actions-hardening retrofit — ✅ 2026-04-23 via PR #47
(squash
eef3359b2). 7 workflows audited, 4 modified (tier-2.yml,codeql.yml,release.yml,dependabot-review.yml), 3 already conformant. See §10.3 Phase 4b checklist. - Phase 4 branch-protection cutover — ✅ 2026-04-23 14:13 PDT
via PR #48 (squash
6f99b86aa) + ruleset PUT.ci.ymlretired,pr-fast.ymlis sole required lane. See §4.3. - Stale
ci.ymlreferences in comments — ✅ 2026-04-23 via the housekeeping PR that carries this bullet. 8 files updated (README.mdCI badge,CONTRIBUTING.mdPR-CI table + cross- platform paragraph,pr-fast.yml7 comment blocks,release.yml2 comments,dependabot-review.ymlrationale comment, bothpre-pushhook files,just/test.justshift-left diagram + lockstep note). Only remaining live reference is the intentional historical marker at the top ofpr-fast.ymldocumenting the cutover event itself.
If any of the following happens during rollout, stop and reassess before continuing to the next phase:
- A hard gate added in Phases 1–3 blocks a legitimate push more than once in the same session without an obvious fix. Symptom of an over-tightened gate; relax or move to advisory before continuing.
- Phase 4 validation step 5 (broken-classify simulation) unexpectedly
reports
required = success. Symptom of the GitHub Actions skipped-success bug not being closed; theGate on classifystep is broken. - Phase 5 validation step 4 (pre-fast-gate enforcement) lets a preview build proceed against a red PR-fast. Symptom of the check-runs poller returning stale data.
just shiploses time to any newly-introduced gate. Budget was tight before — keep it tight after.
- 2026-04-23 v3.2 — Current. Post-cutover reconciliation
(evening of the same day as v3.1):
- §4.2 re-framed from "current parallel window" to "historical" with actual duration (~2h35m, 11:38 → 14:13 PDT) and in-window PR list (#45, #46, #47).
- §4.3 materially corrected: the v1 "do everything in a single commit" instruction was a category error (ruleset PUT is an API call, not a commit; bundled-atomicity is not available). Correct sequence is PUT-before-merge with a sub-minute window; executed on 2026-04-23 with a 16-second PUT → merge gap. Added evidence block (commit SHAs, PUT timestamp) and a two-step rollback procedure (git revert + ruleset restore).
- §10.2 dashboard: Phase 4 + Phase 4b rows flipped 🟡 → ✅ with
completion dates and both phase-4 commits (
780c1dbb1parallel6f99b86aacutover).
- §10.2 Phase 4 sub-status paragraph rewritten as "fully cutover, end-of-day" narrative with precise timestamps.
- §10.3 Phase 4 checklist: parallel-window and branch-protection cutover rows both ticked with evidence and the wording correction noted.
- §10.5 Deviations log: two new entries (bake-window compressed; plan v1 "single commit" category error + corrected sequence).
- §10.6 Open blockers: bake window, Phase 4b, and branch-
protection cutover all moved from Active to Resolved. Two
new Active items surfaced (stale
ci.ymlreferences in comments; deferredrelease.ymlpermissions refactor).
- 2026-04-23 v3.1 — Post-PR #45 reconciliation:
- Phases 1, 2, 3, 6, 7 marked complete with commit + PR references
(squash
780c1dbb1). - Phase 4 annotated with live-bake progress: broken-classify sim executed and passed; parallel-window opened; cutover scheduled.
- §4 rewritten to reflect reality: ruleset-based protection
(ID
11889528), 6-entry (not 8) Tier 1 baseline, 7-entry parallel-window state, documented cutover procedure, and a required-check context-string gotcha section. - §10.3 Phase 4 checklist: broken-classify and rust-change bakes ticked with evidence; 7-day window dates filled (2026-04-23 → 2026-04-30); infra-only partially credited.
- §10.5 Deviations log: baseline-count correction + ruleset context-string gotcha recorded.
- §10.6 Open blockers: bake-window + 4b retrofit + two real-world bake gaps marked active; broken-classify + context gotcha moved to resolved.
- Phases 1, 2, 3, 6, 7 marked complete with commit + PR references
(squash
- 2026-04-23 v3 — Incorporates second-round external
review. Eleven correctness fixes:
requiredaggregator explicitly depends onclassifyand fails unlessneeds.classify.result == 'success'— closes the "skipped-counts-as-success" green-lighting bug.- Every compile/test/Windows job gates on
code_changed = rust || dep || infra, notrust == 'true'— dep-only and infra-only PRs now run the full matrix. - Explicit
infra_changedfile class defined in §1.3 with concrete glob patterns, wired intoclassifyoutputs. - Pre-push diff logic uses git's stdin protocol
(
<local_ref> <local_oid> <remote_ref> <remote_oid>) instead of@{push}heuristics, with conservative fallback for unparseable cases. preview-artifacts.ymlgainsverify-pr-fast-greenjob querying check-runs API for the same SHA; refuses to build unless PR-fast is green on that commit.- Explicit checkout-ref policy — every job pins
github.event.pull_request.head.sha || github.sha. No reliance onactions/checkout's default PR behaviour. --lockedapplied to every cargo subcommand that supports it (CI and local).cargo fetch --lockedprimes the cache.- New §2.8 Actions hardening policy:
permissions: contents: readdefault, full-SHA action pinning, forbiddenpull_request_target+ self-hosted fork PRs. merge_group:trigger added topr-fast.ymlfor merge-queue compatibility.- Manifest emits
tested_sha,nextest_version, and per-file SHA256 (not just the artifact-level digest). - Local Windows xwin reclassified as advisory (not hard);
PR-fast native
windows-checkis the authoritative gate. Resolves the v2 half-hard/half-soft inconsistency.
- 2026-04-23 v2 — External review of v1. Lane separation, hard-vs-advisory gate split, single-source cache policy, ordered scheduler, PR-fast/preview split, nextest archive for Windows preview.
- 2026-04-23 v1 —
dev-flow.mdinitial draft. Retained as the "explanation" companion doc.