name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to release (e.g. v0.5.1)'
required: true
permissions:
contents: write
id-token: write
attestations: write
jobs:
build:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
include:
- name: macos-arm64
os: macos-14
target: aarch64-apple-darwin
features: coreml
asset_suffix: aarch64-apple-darwin
cuda: false
experimental: false
- name: linux-x86_64-cpu
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
features: ""
asset_suffix: x86_64-unknown-linux-gnu
cuda: false
experimental: false
- name: linux-aarch64
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
features: ""
asset_suffix: aarch64-unknown-linux-gnu
cuda: false
experimental: true
steps:
- uses: actions/checkout@v6
- name: Resolve tag
id: tag
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
fi
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install cross-compiler (Linux aarch64)
if: matrix.target == 'aarch64-unknown-linux-gnu'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libc6-dev-arm64-cross
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_AR=aarch64-linux-gnu-ar" >> "$GITHUB_ENV"
- name: Install protoc
uses: arduino/setup-protoc@v3
with:
version: '29.x'
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install CUDA toolkit
if: matrix.cuda
uses: Jimver/cuda-toolkit@v0.2.35
with:
cuda: '12.4.1'
method: network
sub-packages: '["nvcc", "cudart", "cublas", "cublas_dev"]'
- uses: Swatinem/rust-cache@v2
with:
key: release-${{ matrix.asset_suffix }}
- name: Build release
shell: bash
env:
FEATURES: ${{ matrix.features }}
run: |
if [ -n "$FEATURES" ]; then
cargo build --release --locked --target ${{ matrix.target }} --features "$FEATURES"
else
cargo build --release --locked --target ${{ matrix.target }}
fi
- name: Package tarball
id: package
shell: bash
env:
TAG: ${{ steps.tag.outputs.tag }}
SUFFIX: ${{ matrix.asset_suffix }}
TARGET: ${{ matrix.target }}
run: |
VERSION="${TAG#v}"
ASSET="phostt-${VERSION}-${SUFFIX}.tar.gz"
STAGE="$(mktemp -d)"
cp "target/${TARGET}/release/phostt" "$STAGE/phostt"
cp LICENSE README.md CHANGELOG.md "$STAGE/" 2>/dev/null || true
tar -C "$STAGE" -czf "$ASSET" .
sha256sum "$ASSET" > "${ASSET}.sha256" 2>/dev/null || shasum -a 256 "$ASSET" > "${ASSET}.sha256"
echo "asset=$ASSET" >> "$GITHUB_OUTPUT"
echo "sha_file=${ASSET}.sha256" >> "$GITHUB_OUTPUT"
ls -la "$ASSET" "${ASSET}.sha256"
- uses: actions/upload-artifact@v7
with:
name: phostt-${{ matrix.asset_suffix }}
path: |
${{ steps.package.outputs.asset }}
${{ steps.package.outputs.sha_file }}
if-no-files-found: error
retention-days: 7
publish:
name: Publish GitHub release
needs: build
runs-on: ubuntu-latest
env:
HAS_MINISIGN_KEY: ${{ secrets.MINISIGN_SECRET_KEY != '' && 'true' || 'false' }}
steps:
- uses: actions/checkout@v6
- name: Resolve tag
id: tag
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
fi
- uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true
- name: Aggregate SHA256SUMS.txt
shell: bash
working-directory: dist
run: |
: > SHA256SUMS.txt
for f in *.tar.gz; do
(sha256sum "$f" 2>/dev/null || shasum -a 256 "$f") >> SHA256SUMS.txt
done
cat SHA256SUMS.txt
- name: Install Rust toolchain for SBOM generation
uses: dtolnay/rust-toolchain@stable
- name: Generate CycloneDX SBOM
shell: bash
env:
TAG: ${{ steps.tag.outputs.tag }}
run: |
set -euo pipefail
cargo install --locked --version 0.5.5 cargo-cyclonedx
VERSION="${TAG#v}"
# cargo-cyclonedx writes `<crate>.cdx.json` next to Cargo.toml.
cargo cyclonedx --format json --spec-version 1.5
mkdir -p dist
# Pin the artifact name to the release tag for reproducibility.
cp phostt.cdx.json "dist/phostt-${VERSION}.cdx.json"
ls -la "dist/phostt-${VERSION}.cdx.json"
- name: Generate SLSA build provenance
uses: actions/attest-build-provenance@v4
with:
subject-path: |
dist/*.tar.gz
dist/SHA256SUMS.txt
dist/*.cdx.json
- name: Sign tarballs + SHA256SUMS with minisign
if: env.HAS_MINISIGN_KEY == 'true'
id: minisign
shell: bash
env:
MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }}
MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }}
working-directory: dist
run: |
set -euo pipefail
# Install system `minisign` (apt-cacher is fast) — it accepts the
# passphrase on stdin when stdout is not a TTY, which is exactly
# what we need in CI. `rsign2` 0.6 does not expose a
# non-interactive password flag, so we avoid it here.
sudo apt-get update
sudo apt-get install -y --no-install-recommends minisign
printf '%s\n' "$MINISIGN_SECRET_KEY" > minisign.key
chmod 600 minisign.key
for f in *.tar.gz SHA256SUMS.txt *.cdx.json; do
[ -f "$f" ] || continue
# `-S` = sign; `-s` = secret key; `-c` = comment; `-t` = trusted
# comment (appears in `-Vm` output). Pipe the password so
# minisign reads it from stdin instead of prompting.
printf '%s\n' "$MINISIGN_PASSWORD" | \
minisign -Sm "$f" -s minisign.key \
-c "phostt release $(basename "$f")" \
-t "phostt $(basename "$f") ${{ steps.tag.outputs.tag }}"
done
shred -u minisign.key 2>/dev/null || rm -f minisign.key
ls -la ./*.minisig
echo "signed=true" >> "$GITHUB_OUTPUT"
- name: Warn on missing minisign secret
if: env.HAS_MINISIGN_KEY != 'true'
shell: bash
run: |
echo "::warning ::MINISIGN_SECRET_KEY not configured — release artefacts will not carry minisign signatures."
- name: Create / update release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: ${{ steps.tag.outputs.tag }}
draft: false
prerelease: false
fail_on_unmatched_files: false
generate_release_notes: true
files: |
dist/*.tar.gz
dist/*.sha256
dist/SHA256SUMS.txt
dist/*.cdx.json
dist/*.minisig