diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65293c8..0664b6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ on: - ".github/ISSUE_TEMPLATE/**" - ".github/pull_request_template.md" - ".github/workflows/ci.yml" + - ".github/workflows/downstream.yml" - "CHANGELOG.md" - "Cargo.lock" - "Cargo.toml" @@ -35,6 +36,7 @@ on: - ".github/ISSUE_TEMPLATE/**" - ".github/pull_request_template.md" - ".github/workflows/ci.yml" + - ".github/workflows/downstream.yml" - "CHANGELOG.md" - "Cargo.lock" - "Cargo.toml" diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml new file mode 100644 index 0000000..0811e29 --- /dev/null +++ b/.github/workflows/downstream.yml @@ -0,0 +1,46 @@ +name: Pinned downstream builds + +on: + schedule: + - cron: "17 4 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: downstream-builds-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + downstream: + name: ${{ matrix.trial }} pinned downstream build + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + max-parallel: 1 + matrix: + trial: + - rust-i18n + - cfn-guard + - navi + - stackable-operator + - pingora + - figment + - uaparser + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install stable Rust + run: | + rustup toolchain install stable --profile minimal + rustup default stable + + - name: Run pinned downstream trial + run: scripts/downstream-build-trials.sh "${{ matrix.trial }}" diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 6dd2e71..33478e0 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -145,13 +145,30 @@ The migration claims are executable, not aspirational: `tests/external_downstream_migration.rs` — pinned real-world configs and reduced fixtures from real `serde_yaml` users (Pingora, rust-i18n, cfn-guard, navi, Stackable). -- `scripts/downstream-build-trials.sh` — packages this crate and builds those - downstreams with their `serde_yaml` dependency rewritten to it. +- `scripts/downstream-build-trials.sh` — packages this crate and provides two + separate downstream checks: + - `smoke-only` runs deterministic package-alias and fixture smokes without + cloning or building an upstream repository; this is the bounded smoke gate + used by the normal CI and release-evidence workflows. + - The named trial modes clone pinned upstream commits and build the real + downstream manifests with their `serde_yaml` dependency rewritten to this + crate. The separate [`Pinned downstream builds`](https://github.com/jskoiz/saneyaml/blob/main/.github/workflows/downstream.yml) + workflow runs all named modes on its weekly schedule or by manual dispatch. ```sh cargo test --test serde_yaml_swap_harness --test downstream_migration_harness cargo test --test external_downstream_migration +# Deterministic fixture/package-alias smoke used by normal CI. scripts/downstream-build-trials.sh smoke-only + +# Real pinned downstream build trials (also run by the separate CI workflow). +scripts/downstream-build-trials.sh rust-i18n +scripts/downstream-build-trials.sh cfn-guard +scripts/downstream-build-trials.sh navi +scripts/downstream-build-trials.sh stackable-operator +scripts/downstream-build-trials.sh pingora +scripts/downstream-build-trials.sh figment +scripts/downstream-build-trials.sh uaparser ``` Real-world gates currently cover 33 files / 39 documents across GitHub Actions, diff --git a/tests/trust_metadata.rs b/tests/trust_metadata.rs index 6ee1312..374b3a9 100644 --- a/tests/trust_metadata.rs +++ b/tests/trust_metadata.rs @@ -5,6 +5,8 @@ use saneyaml::Value; const CARGO_TOML: &str = include_str!("../Cargo.toml"); const FUZZ_CARGO_TOML: &str = include_str!("../fuzz/Cargo.toml"); const CI_WORKFLOW: &str = include_str!("../.github/workflows/ci.yml"); +const DOWNSTREAM_WORKFLOW: &str = include_str!("../.github/workflows/downstream.yml"); +const DOWNSTREAM_TRIAL_SCRIPT: &str = include_str!("../scripts/downstream-build-trials.sh"); const README: &str = include_str!("../README.md"); const ARCHITECTURE: &str = include_str!("../docs/ARCHITECTURE.md"); const BENCHMARKS: &str = include_str!("../docs/BENCHMARKS.md"); @@ -131,6 +133,85 @@ fn migration_release_wording_tracks_manifest_metadata() { ); } +#[test] +fn downstream_workflow_keeps_real_trials_separate_and_bounded() { + let workflow = saneyaml::parse_str(DOWNSTREAM_WORKFLOW) + .unwrap_or_else(|err| panic!(".github/workflows/downstream.yml parses as YAML: {err}")) + .into_value(); + let triggers = workflow["on"] + .as_mapping() + .expect("downstream workflow triggers"); + + assert!(triggers.contains_key("schedule")); + assert!(triggers.contains_key("workflow_dispatch")); + assert!(!triggers.contains_key("push")); + assert!(!triggers.contains_key("pull_request")); + + let job = &workflow["jobs"]["downstream"]; + assert_eq!(job["runs-on"].as_str(), Some("ubuntu-latest")); + assert_eq!(job["timeout-minutes"].as_u64(), Some(30)); + assert_eq!(job["strategy"]["fail-fast"].as_bool(), Some(false)); + assert_eq!(job["strategy"]["max-parallel"].as_u64(), Some(1)); + + let actual_trials = job["strategy"]["matrix"]["trial"] + .as_sequence() + .expect("downstream trial matrix") + .iter() + .map(|trial| { + trial + .as_str() + .expect("downstream matrix trial is a string") + .to_owned() + }) + .collect::>(); + let expected_trials = [ + "cfn-guard", + "figment", + "navi", + "pingora", + "rust-i18n", + "stackable-operator", + "uaparser", + ] + .into_iter() + .map(str::to_owned) + .collect::>(); + assert_eq!(actual_trials, expected_trials); + + let run_steps = job["steps"] + .as_sequence() + .expect("downstream workflow steps") + .iter() + .filter_map(|step| step.get("run").and_then(Value::as_str)) + .collect::>(); + assert!( + run_steps.iter().any(|run| { + run.contains("scripts/downstream-build-trials.sh \"${{ matrix.trial }}\"") + }) + ); + assert!( + !DOWNSTREAM_WORKFLOW.contains("smoke-only"), + "real downstream workflow must not silently fall back to fixture smoke" + ); + assert_contains(CI_WORKFLOW, "scripts/downstream-build-trials.sh smoke-only"); + for trial in &expected_trials { + assert_contains(DOWNSTREAM_TRIAL_SCRIPT, &format!(" {trial})")); + } +} + +#[test] +fn migration_proof_distinguishes_fixture_smoke_from_real_trials() { + for term in [ + "separate downstream checks", + "`smoke-only` runs deterministic package-alias and fixture smokes without", + "The named trial modes clone pinned upstream commits and build the real", + "`Pinned downstream builds`](https://github.com/jskoiz/saneyaml/blob/main/.github/workflows/downstream.yml)", + "scripts/downstream-build-trials.sh uaparser", + ] { + assert_contains(MIGRATION, term); + } +} + #[test] fn public_dependency_snippets_track_manifest_version() { let manifest = package_manifest(); @@ -326,6 +407,7 @@ fn trust_metadata_input_filters() -> BTreeSet { ".github/ISSUE_TEMPLATE/**".to_owned(), ".github/pull_request_template.md".to_owned(), ".github/workflows/ci.yml".to_owned(), + ".github/workflows/downstream.yml".to_owned(), ]) }