Skip to content

v0.32.1

Choose a tag to compare

@github-actions github-actions released this 18 Sep 22:48
· 204 commits to main since this release
e4777a4

A CI/CD-only release. No library, binding or C API change: every entry below is about how hal is built and published, and this release exists to exercise that chain end to end for the first time.

Changed

  • The release is now three workflows with one action each, and a tag deploys without building. release.yml triggers on a push to release/X.Y.Z and builds every wheel, C-API archive and the SBOM; tag-release.yml creates the annotated vX.Y.Z tag on merge, and refuses to create one unless that build is green for the commit being merged; a new publish.yml triggers on the tag, resolves the run that built the artifacts, verifies its tree matches the tag's, and publishes to PyPI, crates.io and GitHub Releases without compiling anything.

    release.yml previously built and published on the tag and had no pull_request trigger, so its build ran at the one point in the process nothing could test it. Five of the nine release tags to date failed, and every one of those was a build failure at deploy time rather than a publishing failure: v0.32.0 failed three times in a row on a runner image without git-lfs, then the same image without a C compiler, then a rust-toolchain.toml component conflict, and Build wheels (aarch64-linux) had already failed the same way in v0.29.1 and v0.29.2. Each retry required deleting and re-pushing a tag the release ruleset exists to make immutable. Those failures now appear on the release PR.

    Artifacts are bound to the tag by tree SHA rather than commit SHA, since a squash merge changes the commit and not the tree, and are held for 30 days because the release-PR-to-tag gap is human-paced. A missing, expired or mismatched artifact fails the publish; there is no fallback to a build.

  • Build provenance is attested where the artifacts are built rather than where they are uploaded. Attesting a downloaded file describes the download; attesting it at build time describes the build that produced what consumers receive.

  • cargo publish remains on the tag path and is the single exception to "the tag builds nothing", because it has no pre-built input. --no-verify keeps the verification compile off that path, and the release branch has already packaged the same tree.

  • Quick lints the aarch64 target again. skip-cross-clippy was switched on because the shared per-target pass took a fixed argument list and hal's six PyO3 crates cannot cross-compile without an interpreter for the target — so the escape hatch cost hal aarch64 clippy coverage entirely, on a repository whose products are aarch64. The pass now takes the same exclusions the host pass uses, and runs check-only without the opencv feature, since opencv-sys probes for a target-native OpenCV that is not installed.

  • Quick also runs actionlint and the organisation's SHA-pin policy over .github/, which arrives with the shared workflows rather than being configured here. The 21 findings it reported are fixed rather than suppressed: unquoted $GITHUB_STEP_SUMMARY / $GITHUB_ENV / $GITHUB_OUTPUT redirects, export X=$(...) masking the command's exit status, for x in $(find ...) splitting on whitespace in a path, and sha256sum * reading a leading-dash filename as an option. The maturin argument lists became arrays, so the expansion is quoted and the --sdist filtering is an explicit comparison rather than a substring removal, and the matrix value reaches the script through env rather than being interpolated into it. read -ra still splits on whitespace without interpreting quotes, which is what the unquoted expansion did too — every value in that matrix is a bare flag, and the step records what to do if one ever is not. One xargs -0 sh -c '...' keeps its single quotes with a scoped shellcheck disable and the reason, because $1 there belongs to the inner shell.

  • release.yml keeps the packaged .crate files as artifacts. publish.yml runs cargo publish --no-verify, and skipping the verification compile on the tag path is only defensible because packaging was exercised on a branch a human reviewed — these are the record of exactly what that packaging produced. cargo publish packages from source and cannot consume them, so they are provenance rather than an input.

  • The wheel build uses the shared maturin-build action for installing maturin, running the builds and checking each wheel's version, instead of hal's own copy of each. The matrix, the ANGLE fetch, the lld install, the per-package staging and the two post-build assertions stay here, because they are hal's: an action composes into the job hal already has, where a reusable workflow would have needed a hook for each of them. --sdist moved from the lane arguments to tracker's entry in the build list, which is where it belongs — it is per package and per lane, and expressing only half of that in the lane arguments is what made the old loop strip it by substring.

    Provenance is still generated by hal rather than by the action (attest: false), because the action attests as soon as it has built and these wheels are not shippable until the layout and Mach-O checks have passed. Provenance for a wheel an assertion is about to reject is provenance for something nobody can install.

    maturin is no longer named per lane. The action derives it from the runner OS, which is what it is a property of — maturin[patchelf,zig] on the two Linux lanes and plain maturin on macOS and Windows, exactly what the matrix used to spell out.

  • Release notes were never extracted. The awk range /## \[VERSION\]/,/^## \[/ ends on the first line matching its second pattern — and the heading it starts on matches that too, so the range was always a single record, which the body then skipped with next. Every release since that step was written shipped empty notes: v0.32.0's GitHub Release body is zero characters. The || echo "Release $VERSION" fallback never fired, because awk exits 0 when it prints nothing. Replaced with .github/scripts/extract_release_notes.py, which requires a complete version token — a prefix match would take the ## [1.2.30] section for release 1.2.3 — and exits non-zero when the section is absent, so the failure is loud rather than an empty release body.

    Found by the publish.yml rehearsal on this release branch, before any tag existed. It is the first defect the rehearsal has caught, and it had been shipping unnoticed for at least one release.

Operational

  • PyPI and crates.io Trusted Publishers must be re-pointed from release.yml to publish.yml before the next tag. Both match a publisher on the workflow filename, so all five PyPI distributions and all ten crates will fail to upload until their publisher configuration names the new file. A publish.yml rehearsal does not catch this, because a rehearsal skips the upload.