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 }}"
dist-local:
name: Build local artifacts (${{ 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
with:
targets: ${{ matrix.target }}
- 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: Install cargo-dist
run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh
- name: Build local dist artifacts
run: $HOME/.cargo/bin/dist build --tag "${{ needs.prepare-release.outputs.release_tag }}" --target "${{ matrix.target }}" --artifacts=local --output-format=json --allow-dirty > dist-local.json
- name: Smoke test local dist artifact
run: |
artifact="$(jq -r '.artifacts | to_entries[] | select(.value.kind == "executable-zip") | .value.path' dist-local.json | head -n 1)"
if [ -z "$artifact" ] || [ "$artifact" = "null" ] || [ ! -f "$artifact" ]; then
echo "::error::No dist tarball found for smoke test."
exit 1
fi
./scripts/smoke-test-installed-planwarden.sh "$artifact"
- 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
files=()
while IFS= read -r file; do
files+=("$file")
done < <(jq -r '.upload_files[]' dist-local.json)
if [ "${#files[@]}" -eq 0 ]; then
echo "::error::No release files produced by dist-local."
exit 1
fi
gh release upload "$tag" "${files[@]}" --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
dist-global:
name: Build global artifacts
runs-on: ubuntu-latest
needs: [prepare-release, dist-local]
if: needs.prepare-release.outputs.release_needed == 'true'
steps:
- uses: actions/checkout@v5
- name: Install cargo-dist
run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh
- name: Download local artifacts
uses: actions/download-artifact@v7
with:
pattern: local-artifacts-*
path: target/distrib
merge-multiple: true
- name: Build global dist artifacts
run: $HOME/.cargo/bin/dist build --tag "${{ needs.prepare-release.outputs.release_tag }}" --artifacts=global --output-format=json --allow-dirty > dist-global.json
- name: Patch Homebrew formula checksums
run: ./scripts/patch-homebrew-formula-checksums.sh target/distrib
- name: Upload global artifacts to GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
tag="${{ needs.prepare-release.outputs.release_tag }}"
files=()
while IFS= read -r file; do
files+=("$file")
done < <(jq -r '.upload_files[]' dist-global.json)
if [ "${#files[@]}" -eq 0 ]; then
echo "::error::No release files produced by dist-global."
exit 1
fi
gh release upload "$tag" "${files[@]}" --clobber
- name: Upload Homebrew formula artifact
uses: actions/upload-artifact@v6
with:
name: homebrew-formula
path: target/distrib/*.rb
if-no-files-found: error
publish-homebrew:
name: Publish Homebrew formula
runs-on: ubuntu-latest
needs: [prepare-release, dist-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: planwarden bot
working-directory: homebrew-tap
run: |
set -euo pipefail
git config user.name "${GITHUB_USER}"
git config user.email "${GITHUB_EMAIL}"
git add Formula/*.rb
if git diff --cached --quiet; then
echo "No Homebrew formula changes to publish."
exit 0
fi
git commit -m "planwarden ${{ needs.prepare-release.outputs.release_version }}"
git push
publish-crates:
name: Publish crate (crates.io)
runs-on: ubuntu-latest
needs: [prepare-release, dist-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: Detect if crate version already exists
id: crate_version
run: |
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: Package crate
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:
- name: Publish the GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
gh release edit "${{ needs.prepare-release.outputs.release_tag }}" --draft=false