Skip to content
Open
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
12 changes: 9 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -573,15 +573,17 @@ jobs:
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist-crate
path: attested
# Keep the downloaded artifact outside the checkout. `cargo package`
# refuses to run when untracked files dirty the working tree.
path: ${{ runner.temp }}/attested
- name: Re-package the crate (must be byte-identical to the attested .crate)
run: cargo package -p ordvec --locked
- name: Verify byte-identity vs the attested .crate
env:
VERSION: ${{ needs.guard.outputs.version }}
run: |
set -euo pipefail
ATTESTED="attested/ordvec-${VERSION}.crate"
ATTESTED="${RUNNER_TEMP}/attested/ordvec-${VERSION}.crate"
PACKAGED="target/package/ordvec-${VERSION}.crate"
[ -f "$ATTESTED" ] || { echo "::error::attested .crate not found at $ATTESTED"; exit 1; }
[ -f "$PACKAGED" ] || { echo "::error::packaged .crate not found at $PACKAGED"; exit 1; }
Expand Down Expand Up @@ -619,7 +621,7 @@ jobs:
VERSION: ${{ needs.guard.outputs.version }}
run: |
set -euo pipefail
ATTESTED="attested/ordvec-${VERSION}.crate"
ATTESTED="${RUNNER_TEMP}/attested/ordvec-${VERSION}.crate"
[ -f "$ATTESTED" ] || { echo "::error::attested .crate missing at $ATTESTED"; exit 1; }
A_SHA=$(sha256sum "$ATTESTED" | cut -d' ' -f1)
# crates.io's stable download endpoint (follows redirect to the CDN).
Expand Down Expand Up @@ -672,6 +674,10 @@ jobs:
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
packages-dir: dist
# Makes release recovery idempotent if PyPI already accepted this
# version but another registry publish failed. The next step still
# fails closed unless PyPI-served hashes equal the staged dist files.
skip-existing: true
- name: Post-publish PyPI hashes match staged dist
env:
VERSION: ${{ needs.guard.outputs.version }}
Expand Down
60 changes: 59 additions & 1 deletion tests/release_publish_invariants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Structural release publish invariants for the PyPI upload job."""
"""Structural release publish invariants for registry upload jobs."""

from __future__ import annotations

Expand Down Expand Up @@ -122,6 +122,11 @@ def check_publish_pypi(workflow: dict[str, Any], path: str) -> None:
)
if norm_path(publish_with.get("packages-dir")) != "dist":
fail(f"{path}: PyPI publish step must upload packages-dir: dist")
if not boolish_true(publish_with.get("skip-existing")):
fail(
f"{path}: PyPI publish step must set skip-existing: true so a recovery "
"rerun is idempotent after PyPI has already accepted the version"
)

wheels: list[int] = []
sdists: list[int] = []
Expand Down Expand Up @@ -163,8 +168,61 @@ def check_publish_pypi(workflow: dict[str, Any], path: str) -> None:
fail(f"{path}: publish-pypi must download exactly one sdist artifact into dist")


def check_publish_crate(workflow: dict[str, Any], path: str) -> None:
jobs = mapping(workflow.get("jobs"), f"{path}: jobs")
job = mapping(jobs.get("publish-crate"), f"{path}: jobs.publish-crate")
steps = sequence(job.get("steps"), f"{path}: jobs.publish-crate.steps")

crate_downloads: list[tuple[int, dict[str, Any], dict[str, Any]]] = []

for index, raw_step in enumerate(steps):
step = mapping(raw_step, f"{path}: jobs.publish-crate.steps[{index}]")
if action_name(step) != "actions/download-artifact":
continue
with_block = step.get("with", {})
with_map = mapping(with_block, f"{path}: {step_label(index, step)} with")
if with_map.get("name") == "dist-crate":
crate_downloads.append((index, step, with_map))

if len(crate_downloads) != 1:
fail(f"{path}: publish-crate must download exactly one dist-crate artifact")

index, step, with_map = crate_downloads[0]
label = step_label(index, step)
artifact_path = norm_path(with_map.get("path"))
if artifact_path != "${{ runner.temp }}/attested":
fail(
f"{path}: {label} downloads dist-crate to {artifact_path or 'the default path'!r}; "
"it must use ${{ runner.temp }}/attested so cargo package sees a clean checkout"
)

verify_step_names = {
"Verify byte-identity vs the attested .crate",
"Post-publish byte-identity (download from crates.io == attested)",
}
verify_steps: list[dict[str, Any]] = []
for index, raw_step in enumerate(steps):
step = mapping(raw_step, f"{path}: jobs.publish-crate.steps[{index}]")
if step.get("name") in verify_step_names:
verify_steps.append(step)
if len(verify_steps) != 2:
fail(f"{path}: publish-crate must have both attested .crate verification steps")
Comment on lines +203 to +209
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current check only verifies that the total number of matched steps is 2. If one of the steps is duplicated and the other is missing, the check will still pass. To ensure both unique verification steps are present, we should track the unique names found and verify that both expected names are matched.

Suggested change
verify_steps: list[dict[str, Any]] = []
for index, raw_step in enumerate(steps):
step = mapping(raw_step, f"{path}: jobs.publish-crate.steps[{index}]")
if step.get("name") in verify_step_names:
verify_steps.append(step)
if len(verify_steps) != 2:
fail(f"{path}: publish-crate must have both attested .crate verification steps")
verify_steps: list[dict[str, Any]] = []
found_names: set[str] = set()
for index, raw_step in enumerate(steps):
step = mapping(raw_step, f"{path}: jobs.publish-crate.steps[{index}]")
name = step.get("name")
if name in verify_step_names:
verify_steps.append(step)
found_names.add(name)
if len(found_names) != 2:
fail(f"{path}: publish-crate must have both attested .crate verification steps")


for step in verify_steps:
name = step.get("name")
run = step.get("run")
if not isinstance(run, str):
fail(f"{path}: publish-crate step {name!r} must be a run step")
if "${RUNNER_TEMP}/attested/ordvec-${VERSION}.crate" not in run:
fail(
f"{path}: publish-crate step {name!r} must read the attested .crate "
"from ${RUNNER_TEMP}/attested"
)


def main() -> None:
workflow = load_workflow(WORKFLOW_PATH)
check_publish_crate(workflow, WORKFLOW_PATH)
check_publish_pypi(workflow, WORKFLOW_PATH)


Expand Down
Loading