name: Release
on:
workflow_dispatch:
inputs:
version:
description: "Version to release (e.g. 2.31.0)"
required: true
type: string
dry_run:
description: "Dry run: build only, publish nothing"
type: boolean
default: false
concurrency:
group: release
cancel-in-progress: false
permissions: {}
env:
VERSION: ${{ inputs.version }}
jobs:
prepare:
name: Prepare v${{ inputs.version }}
runs-on: ubuntu-latest
permissions:
contents: write steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with:
persist-credentials: true
- name: Ensure release is dispatched from main
run: |
if [ "$GITHUB_REF" != "refs/heads/main" ]; then
echo "::error::Releases must be dispatched from main (got $GITHUB_REF)"
exit 1
fi
- name: Validate version
run: |
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Invalid version '$VERSION'. Expected X.Y.Z"
exit 1
fi
current=$(grep -m1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
if [ "$VERSION" = "$current" ]; then
echo "::error::Version $VERSION is already the current version"
exit 1
fi
if git ls-remote --exit-code origin "refs/tags/v$VERSION" >/dev/null 2>&1; then
echo "::error::Tag v$VERSION already exists"
exit 1
fi
- name: Bump version in Cargo.toml and Cargo.lock
run: |
sed -i "0,/^version = /s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml
cargo update --workspace
grep -m1 '^version = ' Cargo.toml
grep -A1 '^name = "lumen"$' Cargo.lock
- name: Upload bumped manifests
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with:
name: release-manifests
path: |
Cargo.toml
Cargo.lock
if-no-files-found: error
- name: Commit, tag, and push
if: ${{ !inputs.dry_run }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Cargo.toml Cargo.lock
git commit -m "chore: bump version to $VERSION"
git tag -a "v$VERSION" -m "Release v$VERSION"
git push origin HEAD:main "v$VERSION"
build:
name: Build ${{ matrix.target }}
needs: prepare
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-apple-darwin
os: macos-15-intel
- target: aarch64-apple-darwin
os: macos-15
- target: x86_64-unknown-linux-gnu
os: ubuntu-22.04
- target: aarch64-unknown-linux-gnu
os: ubuntu-22.04-arm
- target: x86_64-unknown-linux-musl
os: ubuntu-22.04
- target: aarch64-unknown-linux-musl
os: ubuntu-22.04-arm
runs-on: ${{ matrix.os }}
permissions:
contents: read id-token: write attestations: write env:
TARGET: ${{ matrix.target }}
CC_x86_64_unknown_linux_musl: musl-gcc
CC_aarch64_unknown_linux_musl: musl-gcc
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with:
persist-credentials: false
- name: Use bumped manifests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with:
name: release-manifests
- name: Install musl toolchain
if: ${{ contains(matrix.target, 'musl') }}
run: |
sudo sed -i 's|http://ports.ubuntu.com|https://ports.ubuntu.com|g' \
/etc/apt/sources.list /etc/apt/sources.list.d/*.list \
/etc/apt/sources.list.d/*.sources 2>/dev/null || true
for attempt in 1 2 3 4 5; do
sudo apt-get update -o Acquire::Retries=3 \
&& sudo apt-get install -y -o Acquire::Retries=3 musl-tools \
&& break
echo "apt attempt $attempt failed; retrying in 45s"
sleep 45
done
command -v musl-gcc
- name: Install Rust
run: |
rustup toolchain install stable --profile minimal --no-self-update
rustup default stable
rustup target add "$TARGET"
- name: Build
run: cargo build --release --locked --target "$TARGET"
- name: Smoke test
run: |
out=$("./target/$TARGET/release/lumen" --version)
echo "$out"
echo "$out" | grep -qF "$VERSION" || {
echo "::error::Binary reports wrong version"
exit 1
}
- name: Package
run: |
tarball="lumen-$TARGET.tar.gz"
tar -czf "$tarball" -C "target/$TARGET/release" lumen
if command -v sha256sum >/dev/null; then
sha256sum "$tarball" > "$tarball.sha256"
else
shasum -a 256 "$tarball" > "$tarball.sha256"
fi
cat "$tarball.sha256"
- name: Attest build provenance
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 with:
subject-path: lumen-${{ matrix.target }}.tar.gz
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with:
name: lumen-${{ matrix.target }}
path: |
lumen-${{ matrix.target }}.tar.gz
lumen-${{ matrix.target }}.tar.gz.sha256
if-no-files-found: error
release:
name: Create GitHub release
if: ${{ !inputs.dry_run }}
needs: [prepare, build]
runs-on: ubuntu-latest
environment: release
permissions:
contents: write steps:
- name: Download all artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with:
pattern: lumen-*
path: dist
merge-multiple: true
- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "v$VERSION" \
--repo "$GITHUB_REPOSITORY" \
--title "v$VERSION" \
--generate-notes \
--verify-tag \
dist/*
publish-crate:
name: Publish to crates.io
if: ${{ !inputs.dry_run }}
needs: [prepare, build]
runs-on: ubuntu-latest
environment: release
permissions:
contents: read id-token: write env:
HAS_CLASSIC_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN != '' }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with:
ref: v${{ inputs.version }}
persist-credentials: false
- name: Install Rust
run: |
rustup toolchain install stable --profile minimal --no-self-update
rustup default stable
- name: Authenticate with crates.io (Trusted Publishing)
if: ${{ env.HAS_CLASSIC_TOKEN != 'true' }}
id: auth
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 - name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || steps.auth.outputs.token }}
run: cargo publish --locked
homebrew:
name: Update Homebrew tap
if: ${{ !inputs.dry_run }}
needs: [prepare, release]
runs-on: ubuntu-latest
environment: release
permissions: {}
env:
HAS_TAP_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN != '' }}
steps:
- name: Skip notice
if: ${{ env.HAS_TAP_TOKEN != 'true' }}
run: echo "::warning::TAP_GITHUB_TOKEN not set — skipping Homebrew tap update"
- name: Checkout tap
if: ${{ env.HAS_TAP_TOKEN == 'true' }}
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with:
repository: jnsahaj/homebrew-lumen
token: ${{ secrets.TAP_GITHUB_TOKEN }}
persist-credentials: true
- name: Download checksums
if: ${{ env.HAS_TAP_TOKEN == 'true' }}
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with:
pattern: lumen-*
path: dist
merge-multiple: true
- name: Update formula
if: ${{ env.HAS_TAP_TOKEN == 'true' }}
run: |
sha() { awk '{print $1}' "dist/lumen-$1.tar.gz.sha256"; }
url() { echo "https://github.com/jnsahaj/lumen/releases/download/v$VERSION/lumen-$1.tar.gz"; }
cat > Formula/lumen.rb << EOF
class Lumen < Formula
desc "lumen is a command-line tool that can show pretty diff, generate commit messages with AI, summarise diffs / commits, and more without requiring an API key."
homepage "https://github.com/jnsahaj/lumen"
version "$VERSION"
on_macos do
on_intel do
url "$(url x86_64-apple-darwin)"
sha256 "$(sha x86_64-apple-darwin)"
end
on_arm do
url "$(url aarch64-apple-darwin)"
sha256 "$(sha aarch64-apple-darwin)"
end
end
on_linux do
on_intel do
url "$(url x86_64-unknown-linux-gnu)"
sha256 "$(sha x86_64-unknown-linux-gnu)"
end
on_arm do
url "$(url aarch64-unknown-linux-gnu)"
sha256 "$(sha aarch64-unknown-linux-gnu)"
end
end
def install
bin.install "lumen"
end
end
EOF
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Formula/lumen.rb
git commit -m "chore: bump lumen to $VERSION"
git push