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
22 changes: 20 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,15 @@ jobs:
needs:
- release-target
- validate-artifacts
if: ${{ needs.release-target.outputs.publish_assets == 'true' }}
# Artifact promotion intentionally skips rebuild-artifacts. Override that skipped
# ancestor only after every direct publication prerequisite succeeds.
if: >-
${{
always()
&& needs.release-target.result == 'success'
&& needs.validate-artifacts.result == 'success'
&& needs.release-target.outputs.publish_assets == 'true'
}}
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
Expand All @@ -470,7 +478,17 @@ jobs:
- release-please
- release-target
- publish-release-assets
if: ${{ needs.release-please.outputs.release-created == 'true' && needs.release-target.outputs.publish_assets == 'true' }}
# Keep the explicit status override through the final job so the intentionally
# skipped rebuild ancestor cannot suppress crate publication either.
if: >-
${{
always()
&& needs.release-please.result == 'success'
&& needs.release-target.result == 'success'
&& needs.publish-release-assets.result == 'success'
&& needs.release-please.outputs.release-created == 'true'
&& needs.release-target.outputs.publish_assets == 'true'
}}
runs-on: ubuntu-latest
timeout-minutes: 20
environment:
Expand Down
66 changes: 54 additions & 12 deletions crates/xtask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1303,26 +1303,36 @@ fn check_workflow_policy(
}
let publish_assets =
yaml_path(release, &["jobs", "publish-release-assets"]).unwrap_or(&YamlValue::Null);
if yaml_path(publish_assets, &["if"]).and_then(YamlValue::as_str)
!= Some("${{ needs.release-target.outputs.publish_assets == 'true' }}")
{
if !yaml_path(publish_assets, &["if"]).is_some_and(|condition| {
[
"always()",
"needs.release-target.result == 'success'",
"needs.validate-artifacts.result == 'success'",
"needs.release-target.outputs.publish_assets == 'true'",
]
.iter()
.all(|marker| yaml_contains_string(condition, marker))
}) {
issues.push(
"FAIL: release-publication-condition-invalid: asset publication must require affirmative publish_assets authorization."
"FAIL: release-publication-condition-invalid: asset publication must override skipped ancestors while requiring successful validation and affirmative publish_assets authorization."
.to_string(),
);
}
let publish_crate = yaml_path(release, &["jobs", "publish-crate"]).unwrap_or(&YamlValue::Null);
if !yaml_path(publish_crate, &["if"]).is_some_and(|condition| {
yaml_contains_string(
condition,
[
"always()",
"needs.release-please.result == 'success'",
"needs.release-target.result == 'success'",
"needs.publish-release-assets.result == 'success'",
"needs.release-please.outputs.release-created == 'true'",
) && yaml_contains_string(
condition,
"needs.release-target.outputs.publish_assets == 'true'",
)
]
.iter()
.all(|marker| yaml_contains_string(condition, marker))
}) {
issues.push(
"FAIL: release-crate-publication-condition-invalid: crate publication must require a created release and affirmative publish_assets authorization."
"FAIL: release-crate-publication-condition-invalid: crate publication must override skipped ancestors while requiring successful publication prerequisites, a created release, and affirmative publish_assets authorization."
.to_string(),
);
}
Expand Down Expand Up @@ -2416,12 +2426,14 @@ jobs:
fi
rebuild-artifacts: {uses: './.github/workflows/native.yml'}
publish-release-assets:
if: ${{ needs.release-target.outputs.publish_assets == 'true' }}
needs: [release-target, validate-artifacts]
if: ${{ always() && needs.release-target.result == 'success' && needs.validate-artifacts.result == 'success' && needs.release-target.outputs.publish_assets == 'true' }}
permissions: {contents: write}
environment: {name: cargo}
steps: [{run: 'gh release upload'}]
publish-crate:
if: ${{ needs.release-please.outputs.release-created == 'true' && needs.release-target.outputs.publish_assets == 'true' }}
needs: [release-please, release-target, publish-release-assets]
if: ${{ always() && needs.release-please.result == 'success' && needs.release-target.result == 'success' && needs.publish-release-assets.result == 'success' && needs.release-please.outputs.release-created == 'true' && needs.release-target.outputs.publish_assets == 'true' }}
steps:
- {run: 'cargo publish --dry-run --locked'}
- {run: 'cargo publish --locked'}
Expand Down Expand Up @@ -2604,6 +2616,36 @@ jobs:
);
}

#[test]
fn workflow_policy_rejects_asset_publication_without_skipped_ancestor_override() {
let broken = valid_release_workflow_text().replace(
"${{ always() && needs.release-target.result == 'success' && needs.validate-artifacts.result == 'success' && needs.release-target.outputs.publish_assets == 'true' }}",
"${{ needs.release-target.outputs.publish_assets == 'true' }}",
);
let issues = workflow_policy_issues(&broken);
assert!(
issues
.iter()
.any(|issue| issue.contains("release-publication-condition-invalid")),
"{issues:?}"
);
}

#[test]
fn workflow_policy_rejects_crate_publication_without_skipped_ancestor_override() {
let broken = valid_release_workflow_text().replace(
"${{ always() && needs.release-please.result == 'success' && needs.release-target.result == 'success' && needs.publish-release-assets.result == 'success' && needs.release-please.outputs.release-created == 'true' && needs.release-target.outputs.publish_assets == 'true' }}",
"${{ needs.release-please.outputs.release-created == 'true' && needs.release-target.outputs.publish_assets == 'true' }}",
);
let issues = workflow_policy_issues(&broken);
assert!(
issues
.iter()
.any(|issue| issue.contains("release-crate-publication-condition-invalid")),
"{issues:?}"
);
}

#[test]
fn workflow_policy_rejects_automatic_rebuild_recovery() {
let broken = valid_release_workflow_text()
Expand Down
4 changes: 3 additions & 1 deletion docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ Pull requests targeting `main` and pushes to `main` run:
other merge.
5. The successful CI run for the release pull request merge enables tag creation. The resulting Release run creates the
`vX.Y.Z` tag, proves that the tag resolves to the triggering CI SHA, validates all four
archives/checksums/provenance records from that exact run, and uploads the public assets from one publisher.
archives/checksums/provenance records from that exact run, and uploads the public assets from one publisher. Promotion
intentionally skips the rebuild job; the asset and crate publishers explicitly override that skipped ancestor only
after their direct prerequisites succeed, so GitHub cannot silently suppress publication after validation.
6. `cargo publish --dry-run --locked` runs at the immutable tag, then the crate publishes automatically after native
assets succeed. Manual recovery never publishes the crate.

Expand Down