name: Binary Release
on:
push:
tags: ['v*']
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to attach binaries to (e.g. v1.3.0)'
required: true
type: string
unlocked:
description: 'Drop --locked (backfill of pre-v1.4.3 tags whose Cargo.lock is stale)'
required: false
type: boolean
default: false
permissions:
contents: write
concurrency:
group: binary-release-${{ github.event.release.tag_name || inputs.tag || github.ref_name }}
cancel-in-progress: false
env:
CROSS_VERSION: v0.2.5
jobs:
ensure-release:
name: Ensure release exists
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- name: Resolve release tag
id: tag
run: |
TAG="${{ github.event.release.tag_name || inputs.tag || github.ref_name }}"
if [ -z "$TAG" ]; then
echo "::error::No tag resolved (release event missing tag_name and no dispatch input)"
exit 1
fi
case "$TAG" in
v*) ;;
*) echo "::error::Resolved ref '$TAG' is not a v* tag"; exit 1 ;;
esac
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Releasing forjar $TAG"
- name: Create GitHub Release if missing (idempotent)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ steps.tag.outputs.tag }}"
# Mirrors release.yml create-release: never clobber an existing
# release or its hand-written notes.
if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "Release $TAG already exists — leaving notes intact, will upload assets."
else
gh release create "$TAG" \
--title "$TAG" \
--generate-notes \
--verify-tag \
--repo "${{ github.repository }}"
fi
build:
name: ${{ matrix.target }}
needs: ensure-release
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-musl
cross: false
- target: x86_64-unknown-linux-gnu
cross: false
- target: aarch64-unknown-linux-musl
cross: true
- target: aarch64-unknown-linux-gnu
cross: true
steps:
- name: Resolve release tag
id: tag
run: |
TAG="${{ needs.ensure-release.outputs.tag }}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Building forjar for $TAG → ${{ matrix.target }}"
- name: Checkout at tag
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with:
ref: ${{ steps.tag.outputs.tag }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install target on workspace-pinned toolchain
if: ${{ !matrix.cross }}
run: rustup target add ${{ matrix.target }}
- name: Install musl tools
if: matrix.target == 'x86_64-unknown-linux-musl'
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends musl-tools
- name: Install cross
if: matrix.cross
run: |
dir="$RUNNER_TEMP/cross"
mkdir -p "$dir"
curl -sL "https://github.com/cross-rs/cross/releases/download/${CROSS_VERSION}/cross-x86_64-unknown-linux-musl.tar.gz" \
| tar xz -C "$dir"
echo "$dir" >> "$GITHUB_PATH"
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: binary-${{ matrix.target }}-u2204
- name: Build forjar
run: |
LOCKED="--locked"
if [ "${{ inputs.unlocked }}" = "true" ]; then
LOCKED=""
echo "::warning::building without --locked (stale-lock backfill mode)"
fi
if [ "${{ matrix.cross }}" = "true" ]; then
cross build --release --features vendored-openssl --bin forjar --target ${{ matrix.target }} $LOCKED
else
cargo build --release --features vendored-openssl --bin forjar --target ${{ matrix.target }} $LOCKED
fi
- name: Strip binary
run: |
TARGET="${{ matrix.target }}"
BIN_PATH="target/${TARGET}/release/forjar"
# Each cross image ships only the matching ${triple}-strip;
# the gnu image lacks musl-strip and vice-versa.
case "$TARGET" in
aarch64-unknown-linux-musl) STRIP=aarch64-linux-musl-strip ;;
aarch64-unknown-linux-gnu) STRIP=aarch64-linux-gnu-strip ;;
*) STRIP="" ;;
esac
if [ -n "$STRIP" ]; then
docker run --rm -v "$PWD/target:/target:Z" \
"ghcr.io/cross-rs/${TARGET}:main" \
"$STRIP" "/$BIN_PATH" || true
else
strip "$BIN_PATH" || true
fi
ls -la "$BIN_PATH"
- name: Package archive
id: package
run: |
TAG="${{ steps.tag.outputs.tag }}"
# Strip the leading 'v': assets are forjar-<x.y.z>-<target>.tar.gz,
# matching release.yml, the homebrew checksum patching, and the
# name install.sh resolves (VERSION_NUM="${TAG#v}").
VERSION="${TAG#v}"
TARGET="${{ matrix.target }}"
ARCHIVE="forjar-${VERSION}-${TARGET}"
mkdir -p "$ARCHIVE"
cp "target/${TARGET}/release/forjar" "$ARCHIVE/"
for f in README.md LICENSE LICENSE-MIT LICENSE-APACHE; do
[ -f "$f" ] && cp "$f" "$ARCHIVE/"
done
tar czf "${ARCHIVE}.tar.gz" "$ARCHIVE"
shasum -a 256 "${ARCHIVE}.tar.gz" > "${ARCHIVE}.tar.gz.sha256"
echo "archive=${ARCHIVE}.tar.gz" >> "$GITHUB_OUTPUT"
echo "sha=${ARCHIVE}.tar.gz.sha256" >> "$GITHUB_OUTPUT"
du -h "${ARCHIVE}.tar.gz"
- name: Upload assets to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "${{ steps.tag.outputs.tag }}" \
"${{ steps.package.outputs.archive }}" \
"${{ steps.package.outputs.sha }}" \
--clobber
checksums:
name: Aggregate SHA256SUMS
needs: [ensure-release, build]
runs-on: ubuntu-latest
steps:
- name: Regenerate and upload SHA256SUMS
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ needs.ensure-release.outputs.tag }}"
mkdir assets && cd assets
gh release download "$TAG" --repo "${{ github.repository }}" --pattern '*.tar.gz'
sha256sum -- *.tar.gz > SHA256SUMS
cat SHA256SUMS
gh release upload "$TAG" SHA256SUMS --clobber --repo "${{ github.repository }}"
summary:
name: Summary
needs: [ensure-release, build]
runs-on: ubuntu-latest
if: always()
steps:
- name: Write summary
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ needs.ensure-release.outputs.tag }}"
STATUS="${{ needs.build.result }}"
{
echo "### Binary Release: $TAG"
echo ""
if [ "$STATUS" = "success" ]; then
echo "- Build matrix: **4/4 targets succeeded**"
else
echo "- Build matrix: **partial** (status=$STATUS)"
fi
echo "- Targets:"
echo " - x86_64-unknown-linux-musl"
echo " - x86_64-unknown-linux-gnu"
echo " - aarch64-unknown-linux-musl"
echo " - aarch64-unknown-linux-gnu"
} >> "$GITHUB_STEP_SUMMARY"