name: Release
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
tag:
description: 'Tag to release (e.g., v1.2.0)'
required: true
env:
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
permissions:
contents: write
concurrency:
group: release-${{ github.repository }}
cancel-in-progress: false
jobs:
verify:
runs-on: [self-hosted, clean-room]
outputs:
crate_name: ${{ steps.parse.outputs.crate_name }}
version: ${{ steps.parse.outputs.version }}
has_binaries: ${{ steps.bincheck.outputs.has_binaries }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Checkout provable-contracts (path dep)
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with:
repository: paiml/provable-contracts
path: provable-contracts
- name: Symlink provable-contracts for Cargo path deps
run: ln -sf "$GITHUB_WORKSPACE/provable-contracts" "$GITHUB_WORKSPACE/../provable-contracts"
- name: Generate contract assertions — pv codegen
run: |
if ! command -v pv >/dev/null 2>&1; then
cargo install provable-contracts-cli --locked || true
fi
PV=$(command -v pv 2>/dev/null || true)
if [ -z "$PV" ]; then
echo "::warning::pv not found — skipping contract generation"
else
PC_CONTRACTS="$GITHUB_WORKSPACE/../provable-contracts/contracts"
if [ -f src/lib.rs ] && grep -q 'mod generated_contracts' src/lib.rs && [ ! -f src/generated_contracts.rs ]; then
if [ -d "$PC_CONTRACTS" ]; then
"$PV" codegen "$PC_CONTRACTS" -o src/generated_contracts.rs || true
elif [ -d contracts ]; then
"$PV" codegen contracts/ -o src/generated_contracts.rs || true
fi
fi
fi
- name: Parse tag and verify version
id: parse
run: |
TAG="${RELEASE_TAG}"
# Parse tag format: v1.0.0 or v-cratename-1.0.0
if [[ "$TAG" =~ ^v-([a-z][a-z0-9_-]*)-([0-9]+\..+)$ ]]; then
CRATE_NAME="${BASH_REMATCH[1]}"
TAG_VER="${BASH_REMATCH[2]}"
echo "Workspace release: crate=$CRATE_NAME version=$TAG_VER"
elif [[ "$TAG" =~ ^v([0-9]+\..+)$ ]]; then
CRATE_NAME=""
TAG_VER="${BASH_REMATCH[1]}"
echo "Single-crate release: version=$TAG_VER"
else
echo "::error::Tag '$TAG' does not match expected format (v1.0.0 or v-crate-1.0.0)"
exit 1
fi
# Use cargo metadata for reliable version extraction
if [ -n "$CRATE_NAME" ]; then
CARGO_VER=$(cargo metadata --format-version 1 --no-deps \
| jq -r ".packages[] | select(.name == \"$CRATE_NAME\") | .version")
if [ -z "$CARGO_VER" ] || [ "$CARGO_VER" = "null" ]; then
echo "::error::Crate '$CRATE_NAME' not found in workspace"
exit 1
fi
else
CARGO_VER=$(cargo metadata --format-version 1 --no-deps \
| jq -r '.packages[0].version')
fi
if [ "$TAG_VER" != "$CARGO_VER" ]; then
echo "::error::Tag version $TAG_VER != Cargo.toml version $CARGO_VER"
exit 1
fi
echo "crate_name=$CRATE_NAME" >> "$GITHUB_OUTPUT"
echo "version=$TAG_VER" >> "$GITHUB_OUTPUT"
echo "Version verified: $TAG_VER"
- name: Detect binary targets
id: bincheck
run: |
HAS_BINS=$(cargo metadata --format-version 1 --no-deps \
| jq '[.packages[].targets[] | select(.kind[] == "bin")] | length')
echo "has_binaries=$( [ "$HAS_BINS" -gt 0 ] && echo true || echo false )" >> "$GITHUB_OUTPUT"
echo "Binary targets found: $HAS_BINS"
- name: Verify package tarball
run: |
CRATE="${{ steps.parse.outputs.crate_name }}"
if [ -n "$CRATE" ]; then
cargo package -p "$CRATE"
else
cargo package
fi
create-release:
needs: verify
runs-on: [self-hosted, clean-room]
steps:
- name: Create or update GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Idempotent: create if missing, otherwise leave existing release
# (and its hand-written notes) intact so re-runs don't clobber them.
if gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "Release $RELEASE_TAG already exists — leaving notes intact, downstream jobs will upload assets."
else
gh release create "$RELEASE_TAG" \
--title "$RELEASE_TAG" \
--generate-notes \
--repo "${{ github.repository }}"
fi
build-binaries:
needs: [verify, create-release]
if: needs.verify.outputs.has_binaries == 'true'
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
runner: [self-hosted, clean-room]
- target: x86_64-unknown-linux-musl
runner: [self-hosted, clean-room]
- target: aarch64-unknown-linux-gnu
runner: [self-hosted, clean-room]
- target: aarch64-unknown-linux-musl
runner: [self-hosted, clean-room]
- target: x86_64-apple-darwin
runner: macos-latest
- target: aarch64-apple-darwin
runner: macos-latest
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Checkout provable-contracts (path dep)
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with:
repository: paiml/provable-contracts
path: provable-contracts
- name: Symlink provable-contracts for Cargo path deps
run: ln -sf "$GITHUB_WORKSPACE/provable-contracts" "$GITHUB_WORKSPACE/../provable-contracts"
- name: Generate contract assertions — pv codegen
run: |
if ! command -v pv >/dev/null 2>&1; then
cargo install provable-contracts-cli --locked || true
fi
PV=$(command -v pv 2>/dev/null || true)
if [ -z "$PV" ]; then
echo "::warning::pv not found — skipping contract generation"
else
PC_CONTRACTS="$GITHUB_WORKSPACE/../provable-contracts/contracts"
if [ -f src/lib.rs ] && grep -q 'mod generated_contracts' src/lib.rs && [ ! -f src/generated_contracts.rs ]; then
if [ -d "$PC_CONTRACTS" ]; then
"$PV" codegen "$PC_CONTRACTS" -o src/generated_contracts.rs || true
elif [ -d contracts ]; then
"$PV" codegen contracts/ -o src/generated_contracts.rs || true
fi
fi
fi
- name: Install Rust toolchain (macOS)
if: contains(matrix.target, 'darwin')
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Install target prerequisites (Linux)
if: contains(matrix.target, 'linux')
run: |
case "${{ matrix.target }}" in
*-musl)
if command -v apt-get &>/dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq musl-tools >/dev/null
fi
rustup target add "${{ matrix.target }}" || true
;;
esac
case "${{ matrix.target }}" in
aarch64-*)
if ! command -v cross &>/dev/null; then
cargo install cross --locked
fi
;;
esac
- name: Discover binary names
id: bins
run: |
BINS=$(cargo metadata --format-version 1 --no-deps \
| jq -r '[.packages[].targets[] | select(.kind[] == "bin") | .name] | join(" ")')
echo "names=$BINS" >> "$GITHUB_OUTPUT"
echo "Binaries to build: $BINS"
- name: Build release binaries
run: |
case "${{ matrix.target }}" in
aarch64-unknown-linux-*)
cross build --release --features vendored-openssl --target "${{ matrix.target }}"
;;
*)
cargo build --release --features vendored-openssl --target "${{ matrix.target }}"
;;
esac
- name: Package binaries
run: |
VERSION="${{ needs.verify.outputs.version }}"
TARGET="${{ matrix.target }}"
STAGING="/tmp/release-staging"
mkdir -p "$STAGING"
for BIN in ${{ steps.bins.outputs.names }}; do
ARCHIVE="${BIN}-${VERSION}-${TARGET}.tar.gz"
tar -czf "${STAGING}/${ARCHIVE}" \
-C "target/${TARGET}/release" "$BIN"
echo "Packaged: $ARCHIVE"
done
- name: Upload artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with:
name: binaries-${{ matrix.target }}
path: /tmp/release-staging/*.tar.gz
retention-days: 5
checksums:
needs: [verify, create-release, build-binaries]
if: needs.verify.outputs.has_binaries == 'true'
runs-on: [self-hosted, clean-room]
steps:
- name: Download all binary artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with:
pattern: binaries-*
merge-multiple: true
path: /tmp/release-assets
- name: Generate SHA256SUMS
working-directory: /tmp/release-assets
run: |
sha256sum *.tar.gz > SHA256SUMS
echo "=== SHA256SUMS ==="
cat SHA256SUMS
- name: Upload to GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
working-directory: /tmp/release-assets
run: |
gh release upload "$RELEASE_TAG" \
*.tar.gz SHA256SUMS \
--repo "${{ github.repository }}" \
--clobber
dist-artifacts:
needs: [verify, checksums]
if: needs.verify.outputs.has_binaries == 'true'
runs-on: [self-hosted, clean-room]
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Checkout provable-contracts (path dep)
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with:
repository: paiml/provable-contracts
path: provable-contracts
- name: Symlink provable-contracts for Cargo path deps
run: ln -sf "$GITHUB_WORKSPACE/provable-contracts" "$GITHUB_WORKSPACE/../provable-contracts"
- name: Generate contract assertions — pv codegen
run: |
if ! command -v pv >/dev/null 2>&1; then
cargo install provable-contracts-cli --locked || true
fi
PV=$(command -v pv 2>/dev/null || true)
if [ -z "$PV" ]; then
echo "::warning::pv not found — skipping contract generation"
else
PC_CONTRACTS="$GITHUB_WORKSPACE/../provable-contracts/contracts"
if [ -f src/lib.rs ] && grep -q 'mod generated_contracts' src/lib.rs && [ ! -f src/generated_contracts.rs ]; then
if [ -d "$PC_CONTRACTS" ]; then
"$PV" codegen "$PC_CONTRACTS" -o src/generated_contracts.rs || true
elif [ -d contracts ]; then
"$PV" codegen contracts/ -o src/generated_contracts.rs || true
fi
fi
fi
- name: Generate distribution artifacts
run: |
cargo run --release -- dist \
-f dist-forjar.yaml \
--all \
--output-dir /tmp/dist-output
- name: Upload installer to GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "$RELEASE_TAG" \
/tmp/dist-output/install.sh \
--repo "${{ github.repository }}" \
--clobber
- name: Upload dist artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with:
name: dist-artifacts
path: /tmp/dist-output/
retention-days: 30
homebrew:
needs: [verify, dist-artifacts]
if: needs.verify.outputs.has_binaries == 'true'
runs-on: [self-hosted, clean-room]
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Download dist artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with:
name: dist-artifacts
path: /tmp/dist-output
- name: Download release checksums
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release download "$RELEASE_TAG" \
--pattern "SHA256SUMS" \
--dir /tmp/checksums \
--repo "${{ github.repository }}"
- name: Patch Homebrew formula with real checksums and version
run: |
VERSION="${{ needs.verify.outputs.version }}"
FORMULA="/tmp/dist-output/homebrew.rb"
# Replace VERSION placeholder
sed -i "s/version \"VERSION\"/version \"${VERSION}\"/" "$FORMULA"
# Replace PLACEHOLDER_CHECKSUM for each target
for TARGET_ASSET in \
"forjar-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" \
"forjar-${VERSION}-aarch64-unknown-linux-gnu.tar.gz" \
"forjar-${VERSION}-x86_64-apple-darwin.tar.gz" \
"forjar-${VERSION}-aarch64-apple-darwin.tar.gz"; do
CHECKSUM=$(grep "$TARGET_ASSET" /tmp/checksums/SHA256SUMS | awk '{print $1}' || echo "")
if [ -n "$CHECKSUM" ]; then
# Replace first occurrence of PLACEHOLDER_CHECKSUM
sed -i "0,/PLACEHOLDER_CHECKSUM/s/PLACEHOLDER_CHECKSUM/${CHECKSUM}/" "$FORMULA"
fi
done
echo "=== Patched formula ==="
cat "$FORMULA"
- name: Publish to Homebrew tap
env:
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
VERSION="${{ needs.verify.outputs.version }}"
FORMULA="/tmp/dist-output/homebrew.rb"
# Clone the tap repo
git clone "https://x-access-token:${GH_TOKEN}@github.com/paiml/homebrew-tap.git" /tmp/tap
cp "$FORMULA" /tmp/tap/Formula/forjar.rb
cd /tmp/tap
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/forjar.rb
git commit -m "forjar ${VERSION}" || echo "No changes to commit"
git push