name: Release
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
version:
description: Version to release (for example 0.1.1)
required: true
type: string
skip_crates_publish:
description: Skip cargo publish and only run tap/GitHub release steps
required: false
default: true
type: boolean
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
verify:
name: Verify Release Inputs
runs-on: ubuntu-latest
environment: release
env:
REVIEWLOOP_GMAIL_CLIENT_ID: ${{ secrets.REVIEWLOOP_GMAIL_CLIENT_ID }}
REVIEWLOOP_GMAIL_CLIENT_SECRET: ${{ secrets.REVIEWLOOP_GMAIL_CLIENT_SECRET }}
outputs:
crate_name: ${{ steps.meta.outputs.crate_name }}
version: ${{ steps.meta.outputs.version }}
tag_name: ${{ steps.meta.outputs.tag_name }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache Rust artifacts
uses: Swatinem/rust-cache@v2
- name: Validate tag and collect package metadata
id: meta
shell: bash
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
crate_name=$(sed -nE 's/^name\s*=\s*"([^"]+)".*/\1/p' Cargo.toml | head -n1)
version=$(sed -nE 's/^version\s*=\s*"([^"]+)".*/\1/p' Cargo.toml | head -n1)
if [[ -z "$crate_name" || -z "$version" ]]; then
echo "Failed to parse crate metadata from Cargo.toml" >&2
exit 1
fi
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
if [[ -z "${INPUT_VERSION:-}" ]]; then
echo "workflow_dispatch requires inputs.version" >&2
exit 1
fi
if [[ "${INPUT_VERSION}" != "${version}" ]]; then
echo "Manual release version ${INPUT_VERSION} does not match Cargo.toml version ${version}" >&2
exit 1
fi
tag_name="v${version}"
else
tag_version="${GITHUB_REF_NAME#v}"
if [[ "$tag_version" != "$version" ]]; then
echo "Tag ${GITHUB_REF_NAME} does not match Cargo.toml version ${version}" >&2
exit 1
fi
tag_name="${GITHUB_REF_NAME}"
fi
echo "crate_name=$crate_name" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag_name=$tag_name" >> "$GITHUB_OUTPUT"
- name: Release quality gates
run: ./scripts/quality-gates.sh
publish-crates-io:
name: Publish To crates.io
runs-on: ubuntu-latest
needs: verify
environment: release
env:
REVIEWLOOP_GMAIL_CLIENT_ID: ${{ secrets.REVIEWLOOP_GMAIL_CLIENT_ID }}
REVIEWLOOP_GMAIL_CLIENT_SECRET: ${{ secrets.REVIEWLOOP_GMAIL_CLIENT_SECRET }}
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust artifacts
uses: Swatinem/rust-cache@v2
- name: Publish crate (skip if version already exists)
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
CRATE_NAME: ${{ needs.verify.outputs.crate_name }}
VERSION: ${{ needs.verify.outputs.version }}
SKIP_CRATES_PUBLISH: ${{ github.event_name == 'workflow_dispatch' && inputs.skip_crates_publish && 'true' || 'false' }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${CARGO_REGISTRY_TOKEN:-}" ]]; then
echo "Missing required secret: CARGO_REGISTRY_TOKEN" >&2
exit 1
fi
if [[ "${SKIP_CRATES_PUBLISH}" == "true" ]]; then
echo "Skipping cargo publish by manual request."
exit 0
fi
if curl -fsSL \
-A "reviewloop-release-workflow" \
-H "Accept: application/json" \
"https://crates.io/api/v1/crates/${CRATE_NAME}/${VERSION}" >/dev/null; then
echo "${CRATE_NAME} ${VERSION} already published; skipping cargo publish."
exit 0
fi
publish_log="$(mktemp)"
set +e
cargo publish --locked 2>&1 | tee "${publish_log}"
status=${PIPESTATUS[0]}
set -e
if [[ ${status} -eq 0 ]]; then
exit 0
fi
if grep -q "already exists on crates.io index" "${publish_log}"; then
echo "${CRATE_NAME} ${VERSION} already published during this workflow run; continuing."
exit 0
fi
exit "${status}"
update-homebrew-tap:
name: Update Homebrew Tap
runs-on: ubuntu-latest
needs:
- verify
- publish-crates-io
environment: release
permissions:
contents: read
steps:
- name: Validate tap publishing configuration
env:
HOMEBREW_TAP_REPO: ${{ vars.HOMEBREW_TAP_REPO != '' && vars.HOMEBREW_TAP_REPO || 'Acture/homebrew-ac' }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${HOMEBREW_TAP_GITHUB_TOKEN:-}" ]]; then
echo "Missing required secret: HOMEBREW_TAP_GITHUB_TOKEN" >&2
exit 1
fi
echo "Using tap repository: ${HOMEBREW_TAP_REPO}"
- name: Resolve source archive URL and checksum
id: source
env:
TAG_NAME: ${{ needs.verify.outputs.tag_name }}
shell: bash
run: |
set -euo pipefail
source_url="https://github.com/${GITHUB_REPOSITORY}/archive/refs/tags/${TAG_NAME}.tar.gz"
archive_path="$(mktemp)"
curl -fsSL \
-A "reviewloop-release-workflow" \
"${source_url}" \
-o "${archive_path}"
checksum="$(sha256sum "${archive_path}" | awk '{print $1}')"
echo "source_url=${source_url}" >> "$GITHUB_OUTPUT"
echo "checksum=${checksum}" >> "$GITHUB_OUTPUT"
- name: Checkout source repository
uses: actions/checkout@v6
- name: Checkout tap repository
uses: actions/checkout@v6
with:
repository: ${{ vars.HOMEBREW_TAP_REPO != '' && vars.HOMEBREW_TAP_REPO || 'Acture/homebrew-ac' }}
token: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
path: tap
- name: Render formula
env:
CRATE_NAME: ${{ needs.verify.outputs.crate_name }}
VERSION: ${{ needs.verify.outputs.version }}
TAG_NAME: ${{ needs.verify.outputs.tag_name }}
SOURCE_SHA256: ${{ steps.source.outputs.checksum }}
HOMEBREW_FORMULA_PATH: ${{ vars.HOMEBREW_FORMULA_PATH }}
shell: bash
run: |
set -euo pipefail
formula_rel_path="${HOMEBREW_FORMULA_PATH:-Formula/${CRATE_NAME}.rb}"
formula_file="tap/${formula_rel_path}"
bash tools/render_homebrew_formula.sh "$formula_file"
grep -Fq "url \"https://github.com/${GITHUB_REPOSITORY}/archive/refs/tags/${TAG_NAME}.tar.gz\"" "$formula_file"
grep -Fq "sha256 \"${SOURCE_SHA256}\"" "$formula_file"
grep -Fq 'system "cargo", "install", *std_cargo_args(path: ".")' "$formula_file"
- name: Commit and push tap update
env:
CRATE_NAME: ${{ needs.verify.outputs.crate_name }}
VERSION: ${{ needs.verify.outputs.version }}
HOMEBREW_FORMULA_PATH: ${{ vars.HOMEBREW_FORMULA_PATH }}
shell: bash
run: |
set -euo pipefail
formula_rel_path="${HOMEBREW_FORMULA_PATH:-Formula/${CRATE_NAME}.rb}"
cd tap
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add "$formula_rel_path"
if git diff --cached --quiet -- "$formula_rel_path"; then
echo "No formula changes detected."
exit 0
fi
git commit -m "reviewloop ${VERSION}"
current_branch=$(git branch --show-current)
git push origin "HEAD:${current_branch}"
github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs:
- verify
- publish-crates-io
- update-homebrew-tap
environment: release
steps:
- name: Publish GitHub release notes
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.verify.outputs.tag_name }}
generate_release_notes: true