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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 51 additions & 49 deletions demos/opentofu-plan/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1338,7 +1338,7 @@ mod tests {
artifacts: MemoryPlanArtifactStore,
backend: Arc<OpenTofuBackend>,
nonce_byte: u8,
) -> WorkflowOutcome {
) -> Result<WorkflowOutcome, ServiceError> {
let demo = demo_with_nonce(nonce_byte);
let handle = artifacts
.put(SavedPlanArtifact::new(auths_opentofu::test_support::PLAN_BYTES.to_vec()).unwrap())
Expand All @@ -1362,7 +1362,6 @@ mod tests {
proof: demo.auths.proof,
auths_request: demo.auths.request,
})
.unwrap()
}

async fn request(
Expand Down Expand Up @@ -1447,52 +1446,55 @@ mod tests {

#[test]
fn competing_same_scope_actions_execute_one_provider_effect() {
let state = tempfile::tempdir().unwrap();
let demo = demo_with_nonce(0x11);
let lifecycle_store =
fixture_lifecycle_store(&state.path().join("lifecycle"), &demo.product.action);
let artifacts = MemoryPlanArtifactStore::default();
let backend = Arc::new(OpenTofuBackend::fixture(demo.product.evidence));
let barrier = Arc::new(Barrier::new(2));

let handles = [0x11, 0x22]
.into_iter()
.map(|nonce_byte| {
let lifecycle_store = Arc::clone(&lifecycle_store);
let artifacts = artifacts.clone();
let backend = Arc::clone(&backend);
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
barrier.wait();
execute_fixture_workflow(lifecycle_store, artifacts, backend, nonce_byte)
for _ in 0..32 {
let state = tempfile::tempdir().unwrap();
let demo = demo_with_nonce(0x11);
let lifecycle_store =
fixture_lifecycle_store(&state.path().join("lifecycle"), &demo.product.action);
let artifacts = MemoryPlanArtifactStore::default();
let backend = Arc::new(OpenTofuBackend::fixture(demo.product.evidence));
let barrier = Arc::new(Barrier::new(2));

let handles = [0x11, 0x22]
.into_iter()
.map(|nonce_byte| {
let lifecycle_store = Arc::clone(&lifecycle_store);
let artifacts = artifacts.clone();
let backend = Arc::clone(&backend);
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
barrier.wait();
execute_fixture_workflow(lifecycle_store, artifacts, backend, nonce_byte)
})
})
})
.collect::<Vec<_>>();
let outcomes = handles
.into_iter()
.map(|handle| handle.join().unwrap())
.collect::<Vec<_>>();

assert_eq!(backend.apply_calls(), 1);
assert_eq!(
outcomes
.iter()
.filter(|outcome| matches!(outcome, WorkflowOutcome::Executed { .. }))
.count(),
1
);
assert_eq!(
outcomes
.iter()
.filter(|outcome| {
matches!(
outcome,
WorkflowOutcome::Replay { .. } | WorkflowOutcome::Conflict { .. }
)
})
.count(),
1
);
.collect::<Vec<_>>();
let outcomes = handles
.into_iter()
.map(|handle| handle.join().unwrap())
.collect::<Vec<_>>();

assert_eq!(backend.apply_calls(), 1);
assert_eq!(
outcomes
.iter()
.filter(|outcome| matches!(outcome, Ok(WorkflowOutcome::Executed { .. })))
.count(),
1
);
assert_eq!(
outcomes
.iter()
.filter(|outcome| {
matches!(
outcome,
Ok(WorkflowOutcome::Replay { .. } | WorkflowOutcome::Conflict { .. })
| Err(ServiceError::StateChanged)
)
})
.count(),
1
);
}
}

#[test]
Expand All @@ -1511,14 +1513,14 @@ mod tests {
Arc::clone(&backend),
0x11,
);
assert!(matches!(first, WorkflowOutcome::OutcomeUnknown { .. }));
assert!(matches!(first, Ok(WorkflowOutcome::OutcomeUnknown { .. })));
assert_eq!(backend.apply_calls(), 1);
drop(first_store);

let reopened_store = fixture_lifecycle_store(&lifecycle_path, &demo.product.action);
let recovered =
execute_fixture_workflow(reopened_store, artifacts, Arc::clone(&backend), 0x11);
assert!(matches!(recovered, WorkflowOutcome::Executed { .. }));
assert!(matches!(recovered, Ok(WorkflowOutcome::Executed { .. })));
assert_eq!(backend.apply_calls(), 1);
}

Expand Down
49 changes: 49 additions & 0 deletions docs/plans/PHASE_7_RELEASE_READINESS_AUDIT.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,54 @@
# Phase 7 release readiness audit

## Current deterministic preflight audit (2026-08-03)

The current reusable release builder was audited from candidate validation
through artifact upload after a preparation run exposed a nondeterministic
OpenTofu concurrency-test failure. The production runtime had failed closed:
one competing action executed and the other observed changed state. The test
incorrectly assumed the loser could only receive a replay or conflict outcome.
It now accepts the typed `StateChanged` outcome and repeats the synchronized
race 32 times while proving that exactly one provider effect occurs.

The audit also found that `release-check` used to run two deterministic gates
only after the ordinary pull-request suite: workspace tests with default
features disabled and canonical wire verification. Both now run through
`release_preflight()` inside authoritative CI. `release-check` calls that same
path through `ci()` and no longer repeats those gates or the package check.
This makes the pull request, local release check, and hosted release builder
share one implementation rather than three command lists that can drift.

The end-to-end local audit then found a second late-only defect in release
evidence assembly: the deterministic source archive used a GNU tar header whose
single 100-byte name field could not encode a canonical fixture path after the
release prefix was added. The archive now uses the UStar prefix field, and its
determinism test includes the exact formerly failing fixture path. A complete
local `cargo xtask release-check` subsequently passed and generated validated
inputs for 28 crate archives and 33 total release subjects.

### Current gate map

| Builder stage | Pull-request or repository-local prevention | Why it remains in preparation |
| --- | --- | --- |
| Candidate commit, tag shape, repository IDs, clean checkout | Release workflow contract validation and release-control tests | The real commit, ref, and hosted repository context exist only at runtime |
| Environment-bound OIDC subject | Pinned workflow text is checked by `release-contract` | A genuine GitHub OIDC token cannot be reproduced safely in local or pull-request CI |
| Production source closure | Formal-translation CI checks the committed closure before tool setup | Rechecked against the exact candidate before expensive formal work |
| Authoritative Rust checks | Authoritative CI runs format, architecture, semantic freeze, naming, repository policy, all-target/all-feature check, tests, Clippy, Rustdoc, MSRV, platform, and fuzz smoke | The isolated builder must verify the exact candidate independently |
| Release compilation profiles | Authoritative CI now runs all workspace tests with no default features and canonical wire verification through `release_preflight()` | Rechecked through the same function in `release-check` |
| Formal properties and exact translation | Formal-translation CI runs Lean, Kani, source-closure validation, and exact Aeneas/Charon reproduction twice | The builder reproduces the exact translation with its pinned tools and preserves candidate evidence |
| Compliance and package subjects | Compliance CI runs ABI, exchange/product/profile/domain/matrix/binding/package/WASM/live-demo checks and emits its report | Candidate packages and compliance evidence are regenerated from the isolated checkout |
| Dependency and secret policy | Dedicated CI phases run pinned cargo-deny and gitleaks | cargo-deny is rechecked with the builder's pinned release toolchain; secret scanning is source-history policy rather than an artifact-generation step |
| Release-evidence generation | Schema, catalogue, workflow, archive, manifest, checksum, and adversarial validator tests run in ordinary CI | Final evidence assembly consumes formal, compliance, package, platform, and exact-commit outputs produced together in the isolated builder |
| SLSA attestation and verification | Workflow contract tests pin the action, subject set, repository, issuer constraints, signer workflow, and digest checks | Signing, transparency data, and verification require the real protected environment and GitHub identity |
| Manifest finalization and immutable upload | Release-control tests reject subject drift and rebuilding during promotion | Attestation bundle paths, hosted digests, and artifact IDs exist only in the preparation run |

The remaining preparation-only work is deliberately limited to checks that
need the actual protected GitHub identity or the complete set of exact outputs
from one isolated build. A pull request must now fail before merge for either
Rust feature profile, wire drift, the stressed OpenTofu race contract, package
construction, formal drift, documentation warnings, dependency policy, or the
other deterministic gates enumerated above.

## Status and reviewed baseline

Historical read-only repository audit for AP-SPEC-032. It records the baseline
Expand Down
6 changes: 3 additions & 3 deletions release/semantic-freeze.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schema": "auths.semantic-freeze/1",
"freezeVersion": 11,
"freezeVersion": 12,
"publicSurface": {
"rustRoots": [
"auths",
Expand Down Expand Up @@ -592,7 +592,7 @@
},
{
"id": "auths.release.public-surface",
"version": 11,
"version": 12,
"classification": "release-metadata",
"categories": [
"package-names",
Expand Down Expand Up @@ -659,7 +659,7 @@
"xtask/src/release_control.rs",
"xtask/src/semantic_freeze.rs"
],
"sha256": "6a3fc5e05f10d97a721ad899045234944d78b3647af66d66869d1683fdfe8530"
"sha256": "018187c9cc5065e2f614d4f2071d9f05ce7ff0160a4f3fe9ede428511da8639d"
}
]
}
13 changes: 13 additions & 0 deletions xtask/src/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub(crate) fn ci_authoritative() -> Result<(), String> {
repository_hygiene()?;
cargo(&["check", "--workspace", "--all-targets", "--all-features"])?;
cargo(&["test", "--workspace", "--all-features"])?;
release_preflight()?;
cargo(&[
"clippy",
"--workspace",
Expand All @@ -33,6 +34,18 @@ pub(crate) fn ci_authoritative() -> Result<(), String> {
fuzz_smoke()
}

/// Runs the deterministic compilation-profile and canonical-byte gates that
/// release preparation depends on in addition to the all-features workspace
/// suite.
///
/// Keep this in authoritative pull-request CI. A release candidate must not be
/// the first place that no-default-features compilation or wire drift is
/// discovered.
pub(crate) fn release_preflight() -> Result<(), String> {
cargo(&["test", "--workspace", "--no-default-features"])?;
wire(false)
}

pub(crate) fn release_documentation() -> Result<(), String> {
let status = Command::new("cargo")
.args(["doc", "--workspace", "--all-features", "--no-deps"])
Expand Down
18 changes: 13 additions & 5 deletions xtask/src/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,6 @@ pub(crate) fn release_check() -> Result<(), String> {
validate_release_tag(&tag, env!("CARGO_PKG_VERSION"))?;
}
ci()?;
cargo(&["test", "--workspace", "--no-default-features"])?;
wire(false)?;
package_check()?;
release_evidence()?;
println!("release checks passed");
Ok(())
Expand Down Expand Up @@ -787,7 +784,11 @@ fn write_deterministic_archive(
for (relative, mode) in files {
let bytes = fs::read(root().join(relative))
.map_err(|error| format!("could not read archive input {relative}: {error}"))?;
let mut header = tar::Header::new_gnu();
// UStar keeps the path prefix in its dedicated 155-byte field. The
// GNU header layout reuses that field and therefore rejects otherwise
// valid repository paths once the release archive prefix pushes them
// beyond the 100-byte name field.
let mut header = tar::Header::new_ustar();
header
.set_path(Path::new(prefix).join(relative))
.map_err(|error| format!("could not encode archive path {relative}: {error}"))?;
Expand Down Expand Up @@ -1760,7 +1761,14 @@ mod tests {
fn release_archive_encoding_is_deterministic() {
let first = root().join("target/release-archive-determinism-a.tar.zst");
let second = root().join("target/release-archive-determinism-b.tar.zst");
let files = BTreeMap::from([("Cargo.toml".to_owned(), 0o644)]);
let files = BTreeMap::from([
("Cargo.toml".to_owned(), 0o644),
(
"core/fixtures/v1/indeterminate/accepted-extension-without-handler.action.cbor"
.to_owned(),
0o644,
),
]);
write_deterministic_archive(&first, "auths-test", &files, 1)
.expect("first deterministic archive");
write_deterministic_archive(&second, "auths-test", &files, 1)
Expand Down
4 changes: 2 additions & 2 deletions xtask/src/semantic_freeze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::*;

const INVENTORY_PATH: &str = "release/semantic-freeze.json";
const INVENTORY_SCHEMA: &str = "auths.semantic-freeze/1";
const FREEZE_VERSION: u64 = 11;
const FREEZE_VERSION: u64 = 12;
const PUBLIC_RUST_ROOTS: [&str; 2] = ["auths", "auths-sdk"];
const PUBLIC_RUST_CLOSURE: [&str; 28] = [
"auths",
Expand Down Expand Up @@ -321,7 +321,7 @@ fn generate_inventory() -> Result<SemanticFreezeInventory, String> {
]);
entries.push(freeze_entry(
"auths.release.public-surface",
11,
12,
FreezeClassification::ReleaseMetadata,
&[
"package-names",
Expand Down
Loading