diff --git a/.github/workflows/package-macos.yml b/.github/workflows/package-macos.yml index 60ad1ef..f5bb05f 100644 --- a/.github/workflows/package-macos.yml +++ b/.github/workflows/package-macos.yml @@ -100,14 +100,22 @@ jobs: shell: bash run: | : ${GITHUB_OUTPUT:=/tmp/github_output} + set -euo pipefail - PKG_FILE=$(find deploy -maxdepth 1 -type f -name "fips-*-macos-*.pkg" | sort | head -n 1) - if [[ -z "$PKG_FILE" ]]; then - echo "Missing macOS package" >&2 + # build-pkg.sh names the package from the build target, so each + # matrix leg produces a distinctly named, arch-correct asset. + # Assert that here: a regression in that naming then fails loudly + # at the build stage instead of as a silent collision when the + # release job merges both artifacts into one directory. + EXPECTED="deploy/fips-${{ needs.determine-versioning.outputs.macos_package_version }}-macos-${{ matrix.arch }}.pkg" + if [[ ! -f "$EXPECTED" ]]; then + echo "Expected package $EXPECTED was not produced" >&2 + echo "deploy/ contains:" >&2 + ls -la deploy >&2 || true exit 1 fi - echo "pkg=$PKG_FILE" >> "$GITHUB_OUTPUT" + echo "pkg=$EXPECTED" >> "$GITHUB_OUTPUT" - name: Verify .pkg structural correctness shell: bash @@ -185,16 +193,27 @@ jobs: fi echo "==> .pkg verification PASSED" - - name: SHA-256 hash + - name: SHA-256 hash and sidecar + shell: bash run: | + set -euo pipefail + PKG="${{ steps.macos-assets.outputs.pkg }}" echo "==> macOS release asset:" - shasum -a 256 "${{ steps.macos-assets.outputs.pkg }}" + # Capture the SHA-256 of the verified .pkg on the macOS runner and + # write it to a sidecar file next to the .pkg, in the standard + # ` ` shasum format. The verify-handoff and release + # jobs re-check the downloaded bytes against this value, so any + # corruption introduced after this point is detected before + # publication. + ( cd "$(dirname "$PKG")" && shasum -a 256 "$(basename "$PKG")" | tee "$(basename "$PKG").sha256" ) - name: Upload artifact uses: actions/upload-artifact@v4 with: name: fips_${{ needs.determine-versioning.outputs.macos_package_version }}_${{ matrix.arch }}_macos - path: ${{ steps.macos-assets.outputs.pkg }} + path: | + ${{ steps.macos-assets.outputs.pkg }} + ${{ steps.macos-assets.outputs.pkg }}.sha256 retention-days: 30 - name: Build summary @@ -202,10 +221,61 @@ jobs: echo "Build Summary for macOS/${{ matrix.arch }}:" echo " Package: ${{ steps.macos-assets.outputs.pkg }}" + verify-handoff: + name: Verify macOS package handoff integrity + runs-on: ubuntu-latest + needs: build + + steps: + - name: Download macOS artifacts + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Verify .pkg integrity across the handoff + shell: bash + run: | + set -euo pipefail + cd dist + + pkgs=$(find . -maxdepth 1 -type f -name '*.pkg' | LC_ALL=C sort) + if [[ -z "$pkgs" ]]; then + echo "FAIL: no .pkg artifacts were downloaded" >&2 + exit 1 + fi + + fail=0 + while IFS= read -r pkg; do + base=$(basename "$pkg") + sidecar="${pkg}.sha256" + if [[ ! -f "$sidecar" ]]; then + echo "FAIL: missing SHA-256 sidecar for $base" >&2 + fail=1 + continue + fi + expected=$(awk '{print $1}' "$sidecar") + actual=$(sha256sum "$pkg" | awk '{print $1}') + if [[ "$expected" != "$actual" ]]; then + echo "FAIL: $base SHA-256 mismatch across the artifact handoff" >&2 + echo " expected (macOS runner): $expected" >&2 + echo " actual (downloaded): $actual" >&2 + fail=1 + continue + fi + echo "PASS: $base matches the macOS-runner SHA-256 ($actual)" + done <<<"$pkgs" + + if [[ "$fail" -ne 0 ]]; then + echo "==> macOS package handoff verification FAILED" >&2 + exit 1 + fi + echo "==> macOS package handoff verification PASSED" + release: name: Publish macOS assets to GitHub Release runs-on: ubuntu-latest - needs: build + needs: [build, verify-handoff] if: startsWith(github.ref, 'refs/tags/') permissions: contents: write @@ -217,6 +287,45 @@ jobs: path: dist merge-multiple: true + - name: Validate .pkg bytes before publishing + shell: bash + run: | + set -euo pipefail + cd dist + + pkgs=$(find . -maxdepth 1 -type f -name '*.pkg' | LC_ALL=C sort) + if [[ -z "$pkgs" ]]; then + echo "FAIL: no .pkg artifacts were downloaded" >&2 + exit 1 + fi + + fail=0 + while IFS= read -r pkg; do + base=$(basename "$pkg") + sidecar="${pkg}.sha256" + if [[ ! -f "$sidecar" ]]; then + echo "FAIL: missing SHA-256 sidecar for $base" >&2 + fail=1 + continue + fi + expected=$(awk '{print $1}' "$sidecar") + actual=$(sha256sum "$pkg" | awk '{print $1}') + if [[ "$expected" != "$actual" ]]; then + echo "FAIL: $base SHA-256 mismatch on the bytes about to be published" >&2 + echo " expected (macOS runner): $expected" >&2 + echo " actual (downloaded): $actual" >&2 + fail=1 + continue + fi + echo "PASS: $base matches the macOS-runner SHA-256 ($actual)" + done <<<"$pkgs" + + if [[ "$fail" -ne 0 ]]; then + echo "==> pre-publish .pkg verification FAILED; not publishing" >&2 + exit 1 + fi + echo "==> pre-publish .pkg verification PASSED" + - name: Generate macOS release checksums run: | cd dist diff --git a/packaging/macos/build-pkg.sh b/packaging/macos/build-pkg.sh index 0974342..37b4531 100755 --- a/packaging/macos/build-pkg.sh +++ b/packaging/macos/build-pkg.sh @@ -55,7 +55,23 @@ while [[ $# -gt 0 ]]; do done VERSION="${VERSION_OVERRIDE:-$(grep '^version' "${PROJECT_ROOT}/Cargo.toml" | head -1 | sed 's/.*"\(.*\)"/\1/')}" -ARCH="$(uname -m)" + +# Derive the package architecture from the build target, not the build +# host. When cross-compiling (for example building the x86_64 package on +# an Apple-silicon machine) `uname -m` reports the host architecture and +# would mislabel the package; the Rust target triple is authoritative. +if [[ -n "${TARGET_TRIPLE}" ]]; then + case "${TARGET_TRIPLE}" in + aarch64-*) ARCH="arm64" ;; + x86_64-*) ARCH="x86_64" ;; + *) + echo "Unsupported target triple: ${TARGET_TRIPLE}" >&2 + exit 1 + ;; + esac +else + ARCH="$(uname -m)" +fi PKG_NAME="fips-${VERSION}-macos-${ARCH}" DEPLOY_DIR="${PROJECT_ROOT}/deploy" STAGING_DIR="$(mktemp -d)"