[fix](release) Upload only regular files so nested supplemental assets attach - #556
dchaudhari7177 wants to merge 2 commits into
Conversation
…s attach `release-supplemental` is packed from two source directories (`release-assets/*` and `packages/*.sigstore.json`), so the artifact keeps those prefixes rather than flattening. Both artifacts download into `release-files/`, and the non-recursive `release-files/*` glob then handed `gh release upload` the `packages/` and `release-assets/` directories, which it refuses -- but only after uploading the assets that sorted ahead of them, leaving the draft partially populated. Enumerate regular files recursively instead, and add three guards the previous one-liner had no room for: - refuse an empty `release-files/`, rather than "succeeding" with no assets - refuse colliding basenames: release assets are a flat namespace keyed by basename while the tree being flattened is not, so two same-named files in different directories would overwrite each other under --clobber and publish a release quietly missing one - verify after upload that every local file is present on the draft, so the publish step cannot promote an incomplete release The verification is a set comparison rather than a hardcoded count, so it does not need updating when the build matrix changes the number of distributions.
kaka11chen
left a comment
There was a problem hiding this comment.
The recursive regular-file upload fixes the root cause, and the empty-set/basename-collision guards are useful. I am requesting changes because the release verification still fails open in several cases and does not enforce the asset-integrity requirements from #550. Please address the inline comments and add a committed regression test covering the nested artifact layout, empty/colliding inputs, extra remote assets, and size/digest mismatches.
| # artifact keeps its packages/ and release-assets/ prefixes. The | ||
| # non-recursive release-files/* used to hand gh those directories, | ||
| # which it rejects only after uploading the assets ahead of them. | ||
| mapfile -t assets < <(find release-files -type f -print | sort) |
There was a problem hiding this comment.
[P2] Please propagate failures from asset discovery. mapfile returns its own status and does not propagate the status of this process substitution, so find or sort can emit a partial list and fail while the step continues with a non-empty assets array. Build the sorted list with a directly checked command (preferably NUL-delimited into a temporary file), then load it with mapfile.
| # being flattened is not. Same-named files in different directories | ||
| # would overwrite each other under --clobber and publish a release | ||
| # quietly missing an asset. | ||
| if duplicates="$(printf '%s\n' "${assets[@]}" | xargs -n1 basename | sort | uniq -d)" \ |
There was a problem hiding this comment.
[P2] Please do not evaluate this assignment as part of the if condition. Commands in an if condition are exempt from set -e; if xargs, basename, sort, or uniq fails, this condition simply evaluates false and the upload continues. Assign duplicates in a separate statement so set -euo pipefail can stop the step, then test whether it is non-empty. The missing="$(comm ...)" check below has the same fail-open pattern.
| find release-files -type f -printf '%f\n' | sort > local-assets.txt | ||
| gh release view "$GITHUB_REF_NAME" --json assets --jq '.assets[].name' | sort > remote-assets.txt | ||
|
|
||
| if missing="$(comm -23 local-assets.txt remote-assets.txt)" && [[ -n "$missing" ]]; then |
There was a problem hiding this comment.
[P1] Please verify the exact asset inventory and contents before publishing. comm -23 only proves that local names are a subset of remote names: it still passes with stale extra remote assets, same-named assets having the wrong size/content, or an incomplete local artifact set. This also falls short of #550's acceptance criterion to verify the expected 15 unique assets by name, size, and SHA-256. Generate a normalized local manifest of basename, size, and sha256:<digest>, obtain the corresponding name, size, and digest fields from gh release view --json assets, validate the expected inventory, and compare the two manifests for exact equality.
Review on AstroVela#556 pointed out that the release verification still failed open in several ways, and did not meet AstroVela#550's acceptance criterion of asserting the expected 15 assets. All three shapes are real; confirmed each under set -euo pipefail: mapfile -t assets < <(find ... | sort) # find fails, mapfile returns 0 if dups="$(... )" && [[ -n "$dups" ]] # assignment in an if is exempt missing="$(comm -23 local remote)" # passes with extra remote assets The first can leave a partial-but-non-empty asset list; the second swallows a failure in the collision guard; the third proves only that local names are a subset of remote ones, so it passes with stale assets from an earlier run, and with assets whose bytes are wrong at the right name. Collection and verification move to scripts/check_release_assets.py, next to check_release_artifacts.py, so they can be tested. It builds a local manifest of (basename, size, sha256), asserts the expected inventory -- one sdist, five wheels, a Sigstore bundle per distribution, SHA256SUMS, the SBOM and the provenance bundle -- and requires exact equality with the name/size/digest triples gh reports for the release. An asset gh reports without a digest is refused rather than skipped, since skipping is the same fail-open again. The workflow now calls the script and reads a NUL-delimited list, so a name containing whitespace survives, and every check runs before the first upload rather than after three wheels are already attached. Inventory is checked by shape rather than pinned filenames, so a version bump does not have to touch the script, but a missing wheel or an unsigned distribution still fails. Tests: 24 in tests/fast/test_release_asset_manifest.py, covering the nested artifact layout from AstroVela#550, empty and colliding inputs, each missing asset class, extra remote assets, and size and digest mismatches.
|
Thanks — all three fail-open patterns were real, and the verification is now inventory- and content-based per #550. Pushed as 3aaeeec. I reproduced each pattern before changing anything, under [P2] Asset discovery. Collection moved into a script that exits non-zero on any problem, so [P2] The [P1] Exact inventory and contents. Two judgement calls worth surfacing:
Regression test: 24 cases in One gap to flag honestly: I can't run the full |
|
All three inline findings are addressed in [P2] [P2] Assignments inside [P1] Exact inventory and contents. I could not run The committed tests cover the same five plus the nested layout, empty input, and basename collisions across One judgement call worth flagging: |
Closes #550.
Root cause
release-supplementalis packed from two source directories:With two roots there is no common prefix to strip, so the artifact keeps its
packages/andrelease-assets/prefixes.release-distributions, by contrast, is packed frompackages/*.tar.gz+packages/*.whl— one common prefix, so it flattens.Both download into
release-files/, which is why the tree is mixed: loose wheels at the top, plus two directories.release-files/*then handedgh release uploadthose directories, and it refuses them — but only after uploading the assets that sorted ahead alphabetically, which is why three wheels landed before the failure.Fix
Enumerate regular files recursively (
find release-files -type f) and pass those explicitly. Plus three guards the previous one-liner had no room for:release-files/fails loudly instead of "succeeding" with nothing attached.SHA256SUMSin bothrelease-assets/andpackages/) would overwrite each other under--clobberand publish a release quietly missing one. This isn't in the current layout, but flattening a tree into a flat namespace is exactly where it would appear later.Publish the complete draft release. Steps fail fast, so the publish can no longer promote an incomplete draft.The verification is a set comparison, not a hardcoded count — the issue mentions six distributions and nine supplemental assets, but pinning
15would need editing every time the build matrix changes. Comparing local filenames againstgh release view --json assetscatches a missing asset regardless of how many there should be.Verification
I can't run the release workflow, so I reproduced the artifact layout locally and exercised the logic directly.
The bug, reproduced — same shape as the issue:
All three guards fire, and the happy path doesn't false-positive:
release.ymlparses as YAML, and the step order isAttach→Verify→Publish.find -printfis GNU find, whichubuntu-24.04has.Note on the alternative fix
This could instead be fixed at the upload end, by flattening
release-supplementalwhen the artifact is built (twoupload-artifactsteps, or staging both sets into one directory first). I went with the download end because it is the smaller change and keeps the artifact contents traceable to their source directories. Happy to switch if you'd rather the artifact were flat.