Standardize release build matrix #5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release Build Matrix | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: ["main"] | |
| paths: ["Cargo.toml"] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: release-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| build-artifacts: | |
| name: Build (${{ matrix.target }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: x86_64-unknown-linux-gnu | |
| os: ubuntu-latest | |
| archive_ext: tar.gz | |
| bin: decoding | |
| strip: linux | |
| use_cross: false | |
| - target: aarch64-unknown-linux-gnu | |
| os: ubuntu-latest | |
| archive_ext: tar.gz | |
| bin: decoding | |
| strip: linux-cross | |
| use_cross: true | |
| - target: x86_64-apple-darwin | |
| os: macos-15-intel | |
| archive_ext: tar.gz | |
| bin: decoding | |
| strip: macos | |
| use_cross: false | |
| - target: aarch64-apple-darwin | |
| os: macos-14 | |
| archive_ext: tar.gz | |
| bin: decoding | |
| strip: macos | |
| use_cross: false | |
| - target: x86_64-pc-windows-msvc | |
| os: windows-latest | |
| archive_ext: zip | |
| bin: decoding.exe | |
| strip: none | |
| use_cross: false | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Install cross | |
| if: matrix.use_cross | |
| uses: taiki-e/install-action@cross | |
| - name: Read version | |
| id: version | |
| shell: bash | |
| run: | | |
| version=$(python3 - <<'PY' | |
| import tomllib | |
| from pathlib import Path | |
| data = tomllib.loads(Path('Cargo.toml').read_text()) | |
| print(f"v{data['package']['version']}") | |
| PY | |
| ) | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| - name: Build (cross) | |
| if: matrix.use_cross | |
| run: cross build --release --locked --target ${{ matrix.target }} | |
| - name: Build (cargo) | |
| if: matrix.use_cross == false | |
| run: cargo build --release --locked --target ${{ matrix.target }} | |
| - name: Strip binary (linux) | |
| if: matrix.strip == 'linux' | |
| run: | | |
| if command -v strip >/dev/null; then | |
| strip target/${{ matrix.target }}/release/${{ matrix.bin }} | |
| else | |
| echo "strip not available; skipping" | |
| fi | |
| - name: Strip binary (macos) | |
| if: matrix.strip == 'macos' | |
| run: | | |
| if command -v strip >/dev/null; then | |
| strip -x target/${{ matrix.target }}/release/${{ matrix.bin }} | |
| else | |
| echo "strip not available; skipping" | |
| fi | |
| - name: Install cross-strip toolchain | |
| if: matrix.strip == 'linux-cross' | |
| run: sudo apt-get update && sudo apt-get install -y binutils-aarch64-linux-gnu | |
| - name: Strip binary (linux cross) | |
| if: matrix.strip == 'linux-cross' | |
| run: aarch64-linux-gnu-strip target/${{ matrix.target }}/release/${{ matrix.bin }} | |
| - name: Package archive (unix) | |
| if: matrix.archive_ext == 'tar.gz' | |
| shell: bash | |
| run: | | |
| dist_dir="dist/${{ matrix.target }}" | |
| mkdir -p "$dist_dir" | |
| cp "target/${{ matrix.target }}/release/${{ matrix.bin }}" "$dist_dir/decoding" | |
| cp README.md "$dist_dir/" | |
| if [ -f LICENSE ]; then | |
| cp LICENSE "$dist_dir/" | |
| fi | |
| tar -czf "decoding-${{ steps.version.outputs.version }}-${{ matrix.target }}.tar.gz" -C "$dist_dir" . | |
| - name: Package archive (windows) | |
| if: matrix.archive_ext == 'zip' | |
| shell: pwsh | |
| run: | | |
| $dist = "dist/${{ matrix.target }}" | |
| New-Item -ItemType Directory -Force -Path $dist | Out-Null | |
| Copy-Item "target/${{ matrix.target }}/release/${{ matrix.bin }}" "$dist\\decoding.exe" | |
| Copy-Item README.md $dist | |
| if (Test-Path LICENSE) { | |
| Copy-Item LICENSE $dist | |
| } | |
| Compress-Archive -Path "$dist\\*" -DestinationPath "decoding-${{ steps.version.outputs.version }}-${{ matrix.target }}.zip" -Force | |
| - name: Upload archive | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: decoding-${{ steps.version.outputs.version }}-${{ matrix.target }} | |
| path: decoding-${{ steps.version.outputs.version }}-${{ matrix.target }}.${{ matrix.archive_ext }} | |
| if-no-files-found: error | |
| retention-days: 7 | |
| release: | |
| name: Release artifacts | |
| runs-on: ubuntu-latest | |
| needs: build-artifacts | |
| permissions: | |
| contents: write | |
| id-token: write | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Fetch tags | |
| run: git fetch --tags | |
| - name: Read version | |
| id: version | |
| run: | | |
| version=$(python3 - <<'PY' | |
| import tomllib | |
| from pathlib import Path | |
| data = tomllib.loads(Path('Cargo.toml').read_text()) | |
| print(f"v{data['package']['version']}") | |
| PY | |
| ) | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| - name: Create tag if missing | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| version="${{ steps.version.outputs.version }}" | |
| if git rev-parse --verify --quiet "refs/tags/$version" >/dev/null; then | |
| tag_sha="$(git rev-list -n 1 "$version")" | |
| if [ "$tag_sha" != "$GITHUB_SHA" ]; then | |
| echo "Tag $version already exists at $tag_sha, current commit is $GITHUB_SHA." | |
| echo "Refusing to republish artifacts for an existing version tag." | |
| echo "Bump Cargo.toml package.version to cut a new immutable release." | |
| exit 1 | |
| fi | |
| echo "Tag $version already points to current commit; reusing it." | |
| else | |
| git tag -a "$version" -m "Release $version" | |
| git push origin "$version" | |
| fi | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: dist | |
| merge-multiple: true | |
| - name: Generate SHA256SUMS | |
| run: | | |
| cd dist | |
| find . -maxdepth 1 -type f \( -name "decoding-*.tar.gz" -o -name "decoding-*.zip" \) -print0 \ | |
| | sort -z \ | |
| | xargs -0 sha256sum \ | |
| | sed 's| \./| |' > SHA256SUMS | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@v3 | |
| - name: Sign SHA256SUMS | |
| run: | | |
| cosign sign-blob --yes \ | |
| --output-signature dist/SHA256SUMS.sig \ | |
| --output-certificate dist/SHA256SUMS.pem \ | |
| dist/SHA256SUMS | |
| - name: Install cargo-cyclonedx | |
| uses: taiki-e/install-action@cargo-cyclonedx | |
| - name: Generate SBOM | |
| run: | | |
| cargo cyclonedx --format json --override-filename sbom.cdx | |
| mv sbom.cdx.json dist/sbom.cdx.json | |
| - name: Generate provenance | |
| run: | | |
| python3 - <<'PY' | |
| import datetime | |
| import json | |
| import os | |
| import pathlib | |
| dist = pathlib.Path("dist") | |
| sums = (dist / "SHA256SUMS").read_text().strip().splitlines() | |
| subjects = [] | |
| for line in sums: | |
| digest, name = line.split(maxsplit=1) | |
| subjects.append({ | |
| "name": name, | |
| "digest": {"sha256": digest}, | |
| }) | |
| now = datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z" | |
| statement = { | |
| "_type": "https://in-toto.io/Statement/v1", | |
| "subject": subjects, | |
| "predicateType": "https://slsa.dev/provenance/v1", | |
| "predicate": { | |
| "buildDefinition": { | |
| "buildType": "https://github.com/actions/runner", | |
| "externalParameters": { | |
| "workflow": "release.yml", | |
| "ref": os.environ.get("GITHUB_REF"), | |
| "sha": os.environ.get("GITHUB_SHA"), | |
| }, | |
| "internalParameters": {}, | |
| "resolvedDependencies": [], | |
| }, | |
| "runDetails": { | |
| "builder": { | |
| "id": f"https://github.com/{os.environ.get('GITHUB_REPOSITORY')}/actions/runs/{os.environ.get('GITHUB_RUN_ID')}" | |
| }, | |
| "metadata": { | |
| "invocationId": os.environ.get("GITHUB_RUN_ID"), | |
| "startedOn": now, | |
| "finishedOn": now, | |
| }, | |
| }, | |
| }, | |
| } | |
| (dist / "provenance.intoto.jsonl").write_text(json.dumps(statement) + "\n") | |
| PY | |
| - name: Upload release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.version.outputs.version }} | |
| name: ${{ steps.version.outputs.version }} | |
| body: | | |
| This release packages `${{ steps.version.outputs.version }}` for all supported targets. | |
| ## Verification | |
| ```bash | |
| sha256sum -c SHA256SUMS | |
| cosign verify-blob \ | |
| --certificate-identity "https://github.com/cmdrvl/decoding/.github/workflows/release.yml@refs/heads/main" \ | |
| --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ | |
| --certificate SHA256SUMS.pem \ | |
| --signature SHA256SUMS.sig \ | |
| SHA256SUMS | |
| ``` | |
| The macOS and Linux archives use per-target names and checksums suitable for a future Homebrew formula. | |
| generate_release_notes: true | |
| files: | | |
| dist/decoding-*.tar.gz | |
| dist/decoding-*.zip | |
| dist/SHA256SUMS | |
| dist/SHA256SUMS.sig | |
| dist/SHA256SUMS.pem | |
| dist/provenance.intoto.jsonl | |
| dist/sbom.cdx.json |