From 31a87fe7496ec810587b63fdb397b316c7a4baa9 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 17 Aug 2026 15:47:32 +0930 Subject: [PATCH] fix(release): bind asset upload repository --- .github/workflows/release.yml | 2 +- crates/xtask/src/main.rs | 27 ++++++++++++++++++++++++++- docs/release.md | 4 +++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1af1306..47ce385 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -470,7 +470,7 @@ jobs: shell: bash env: GH_TOKEN: ${{ github.token }} - run: gh release upload "${{ needs.release-target.outputs.tag-name }}" dist/release-assets/codebase-graph-*.tar.gz dist/release-assets/codebase-graph-*.tar.gz.sha256 --clobber + run: gh release upload "${{ needs.release-target.outputs.tag-name }}" dist/release-assets/codebase-graph-*.tar.gz dist/release-assets/codebase-graph-*.tar.gz.sha256 --clobber --repo "$GITHUB_REPOSITORY" publish-crate: name: publish crates.io package diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 0acc111..9914520 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -1303,6 +1303,17 @@ fn check_workflow_policy( } let publish_assets = yaml_path(release, &["jobs", "publish-release-assets"]).unwrap_or(&YamlValue::Null); + let release_upload_step = yaml_step_by_name(publish_assets, "Upload complete native asset set") + .unwrap_or(&YamlValue::Null); + if !yaml_path(release_upload_step, &["run"]).is_some_and(|run| { + yaml_contains_string(run, "gh release upload") + && yaml_contains_string(run, "--repo \"$GITHUB_REPOSITORY\"") + }) { + issues.push( + "FAIL: release-publisher-repository-missing: the checkout-free release upload must explicitly select GITHUB_REPOSITORY." + .to_string(), + ); + } if !yaml_path(publish_assets, &["if"]).is_some_and(|condition| { [ "always()", @@ -2430,7 +2441,9 @@ jobs: 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'}] + steps: + - name: Upload complete native asset set + run: 'gh release upload v1.2.3 artifact --repo "$GITHUB_REPOSITORY"' publish-crate: 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' }} @@ -2631,6 +2644,18 @@ jobs: ); } + #[test] + fn workflow_policy_rejects_repository_implicit_release_upload() { + let broken = valid_release_workflow_text().replace(" --repo \"$GITHUB_REPOSITORY\"", ""); + let issues = workflow_policy_issues(&broken); + assert!( + issues + .iter() + .any(|issue| issue.contains("release-publisher-repository-missing")), + "{issues:?}" + ); + } + #[test] fn workflow_policy_rejects_crate_publication_without_skipped_ancestor_override() { let broken = valid_release_workflow_text().replace( diff --git a/docs/release.md b/docs/release.md index 3616405..cc95d0e 100644 --- a/docs/release.md +++ b/docs/release.md @@ -55,7 +55,9 @@ Pull requests targeting `main` and pushes to `main` run: `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. 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. + after their direct prerequisites succeed, so GitHub cannot silently suppress publication after validation. The + checkout-free asset publisher explicitly selects `GITHUB_REPOSITORY`, so GitHub CLI never depends on local Git + metadata to locate the release. 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.