name: Release
on:
push:
branches:
- main
workflow_dispatch:
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false
permissions:
contents: write
jobs:
prepare-release:
name: Prepare release
runs-on: ubuntu-latest
outputs:
crate_name: ${{ steps.release.outputs.crate_name }}
release_needed: ${{ steps.release.outputs.release_needed }}
release_tag: ${{ steps.release.outputs.release_tag }}
release_version: ${{ steps.release.outputs.release_version }}
tag_exists: ${{ steps.release.outputs.tag_exists }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Determine release state
id: release
env:
EVENT_NAME: ${{ github.event_name }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
crate_name="$(awk -F' *= *' '/^name = /{gsub(/"/,"",$2); print $2; exit}' Cargo.toml)"
release_version="$(awk -F' *= *' '/^version = /{gsub(/"/,"",$2); print $2; exit}' Cargo.toml)"
release_tag="v${release_version}"
head_commit="$(git rev-parse HEAD)"
release_exists=false
release_is_draft=false
if gh release view "$release_tag" --json isDraft >/tmp/release.json 2>/dev/null; then
release_exists=true
release_is_draft="$(jq -r '.isDraft' /tmp/release.json)"
fi
tag_exists=false
tag_commit=""
if git rev-parse "$release_tag" >/dev/null 2>&1; then
tag_exists=true
tag_commit="$(git rev-list -n 1 "$release_tag")"
fi
release_needed=true
if [ "$EVENT_NAME" != "workflow_dispatch" ] && [ "$release_exists" = true ] && [ "$release_is_draft" = false ]; then
echo "::notice::Release ${release_tag} is already published; skipping automatic rebuild so release assets and Homebrew checksums stay stable."
release_needed=false
elif [ "$EVENT_NAME" != "workflow_dispatch" ] && [ "$tag_exists" = true ] && [ "$tag_commit" != "$head_commit" ]; then
echo "::notice::Version ${release_version} is already tagged on a different commit. Bump Cargo.toml before the next automatic release."
release_needed=false
fi
{
echo "crate_name=${crate_name}"
echo "release_needed=${release_needed}"
echo "release_tag=${release_tag}"
echo "release_version=${release_version}"
echo "tag_exists=${tag_exists}"
} >> "$GITHUB_OUTPUT"
- name: Create and push release tag
if: steps.release.outputs.release_needed == 'true' && steps.release.outputs.tag_exists != 'true'
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag "${{ steps.release.outputs.release_tag }}"
git push origin "refs/tags/${{ steps.release.outputs.release_tag }}"
build-local:
name: Build release asset (${{ matrix.target }})
runs-on: ${{ matrix.runner }}
needs: prepare-release
if: needs.prepare-release.outputs.release_needed == 'true'
strategy:
matrix:
include:
- target: x86_64-apple-darwin
runner: macos-15-intel
- target: aarch64-apple-darwin
runner: macos-14
- target: x86_64-unknown-linux-gnu
runner: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-${{ matrix.target }}-cargo-
- name: cargo build --release
run: cargo build --release
- name: Package release artifact
run: |
set -euo pipefail
mkdir -p target/distrib target/release-package
rm -rf target/release-package/*
cp target/release/magellan target/release-package/
cp README.md LICENSE target/release-package/
archive="target/distrib/magellan-${{ matrix.target }}.tar.xz"
tar -C target/release-package -cJf "${archive}" .
shasum -a 256 "${archive}" > "${archive}.sha256"
- name: Smoke test packaged artifact
run: ./scripts/smoke-test-installed-magellan.sh target/distrib/magellan-${{ matrix.target }}.tar.xz
- name: Upload local artifacts to GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
tag="${{ needs.prepare-release.outputs.release_tag }}"
if ! gh release view "$tag" >/dev/null 2>&1; then
gh release create "$tag" --verify-tag --generate-notes --draft
fi
gh release upload "$tag" \
"target/distrib/magellan-${{ matrix.target }}.tar.xz" \
"target/distrib/magellan-${{ matrix.target }}.tar.xz.sha256" \
--clobber
- name: Upload local artifacts for global packaging
uses: actions/upload-artifact@v6
with:
name: local-artifacts-${{ matrix.target }}
path: |
target/distrib/*.tar.xz
target/distrib/*.tar.xz.sha256
if-no-files-found: error
build-global:
name: Build global artifacts
runs-on: ubuntu-latest
needs: [prepare-release, build-local]
if: needs.prepare-release.outputs.release_needed == 'true'
steps:
- uses: actions/checkout@v5
- name: Download local artifacts
uses: actions/download-artifact@v7
with:
pattern: local-artifacts-*
path: target/distrib
merge-multiple: true
- name: Generate Homebrew formula
run: ./scripts/generate-homebrew-formula.sh target/distrib "${{ needs.prepare-release.outputs.release_version }}" target/distrib/magellan.rb
- name: Upload Homebrew formula artifact
uses: actions/upload-artifact@v6
with:
name: homebrew-formula
path: target/distrib/magellan.rb
if-no-files-found: error
publish-homebrew:
name: Publish Homebrew formula
runs-on: ubuntu-latest
needs: [prepare-release, build-global]
if: needs.prepare-release.outputs.release_needed == 'true'
steps:
- name: Ensure Homebrew tap token is configured
env:
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
if [ -z "${HOMEBREW_TAP_TOKEN}" ]; then
echo "::error::Missing HOMEBREW_TAP_TOKEN secret."
exit 1
fi
- uses: actions/checkout@v5
with:
persist-credentials: true
repository: nclandrei/homebrew-tap
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: homebrew-tap
- name: Download Homebrew formula
uses: actions/download-artifact@v7
with:
name: homebrew-formula
path: homebrew-tap/Formula
- name: Commit and push formula
env:
GITHUB_EMAIL: actions@users.noreply.github.com
GITHUB_USER: magellan bot
working-directory: homebrew-tap
run: |
set -euo pipefail
git config user.name "${GITHUB_USER}"
git config user.email "${GITHUB_EMAIL}"
git add Formula/magellan.rb
if git diff --cached --quiet; then
echo "No Homebrew formula changes to publish."
exit 0
fi
git commit -m "magellan ${{ needs.prepare-release.outputs.release_version }}"
git push
publish-crates:
name: Publish crate (crates.io)
runs-on: ubuntu-latest
needs: [prepare-release, build-global]
if: needs.prepare-release.outputs.release_needed == 'true'
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-publish-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-publish-
- name: Ensure crates.io token is configured
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
if [ -z "${CARGO_REGISTRY_TOKEN}" ]; then
echo "::error::Missing CARGO_REGISTRY_TOKEN secret."
exit 1
fi
- name: Check if crate version is already published
id: crate_version
run: |
set -euo pipefail
if curl -fsS "https://crates.io/api/v1/crates/${{ needs.prepare-release.outputs.crate_name }}/${{ needs.prepare-release.outputs.release_version }}" >/dev/null; then
echo "already_published=true" >> "$GITHUB_OUTPUT"
else
echo "already_published=false" >> "$GITHUB_OUTPUT"
fi
- name: cargo package
if: steps.crate_version.outputs.already_published != 'true'
run: cargo package --locked
- name: Publish crate
if: steps.crate_version.outputs.already_published != 'true'
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish --locked
- name: Skip publish (already exists)
if: steps.crate_version.outputs.already_published == 'true'
run: echo "Crate version already published on crates.io; skipping."
publish-github-release:
name: Publish GitHub release
runs-on: ubuntu-latest
needs: [prepare-release, publish-homebrew, publish-crates]
if: needs.prepare-release.outputs.release_needed == 'true'
steps:
- uses: actions/checkout@v5
- name: Publish the GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release edit "${{ needs.prepare-release.outputs.release_tag }}" --draft=false