name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Tag to release (e.g. v0.5.0) — must already exist on the repo"
required: true
default: ""
permissions:
contents: write
packages: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
validate:
name: Validate tag
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.resolve.outputs.tag }}
version: ${{ steps.resolve.outputs.version }}
steps:
- name: Resolve and validate tag
id: resolve
run: |
# For a tag push → github.ref_name is the tag (e.g. "v0.5.0")
# For manual runs → the user supplies it via the `tag` input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG="${{ github.ref_name }}"
fi
echo "Resolved tag: ${TAG}"
if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ Tag '${TAG}' does not match vX.Y.Z — aborting."
exit 1
fi
VERSION="${TAG#v}"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "✅ Tag ${TAG} (version ${VERSION}) is valid."
test:
name: Test & lint
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.validate.outputs.tag }}
- name: Install Rust stable (rustfmt + clippy)
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo fmt -- --check
- name: Run clippy
run: cargo clippy -- -D warnings -A deprecated
- name: Run tests
run: cargo test --locked --all-features --all-targets
build:
name: Build release
needs: [validate, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.validate.outputs.tag }}
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Build release
run: cargo build --release --all-features
release:
name: Create GitHub Release
needs: [validate, build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.validate.outputs.tag }}
fetch-depth: 0
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Install git-cliff
run: cargo install git-cliff --locked
- name: Install Nushell
run: cargo install nu --locked
- name: Prepare release (RELEASE_NOTES.md + CHANGELOG.md)
run: nu scripts/release_prepare.nu ${{ needs.validate.outputs.tag }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.validate.outputs.tag }}
body_path: RELEASE_NOTES.md
files: |
LICENSE
README.md
CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish:
name: Publish to crates.io
needs: [validate, release]
runs-on: ubuntu-latest
env:
HAS_TOKEN: ${{ secrets.CRATES_IO_TOKEN != '' }}
steps:
- name: Skip notice (no token)
if: env.HAS_TOKEN != 'true'
run: echo "⏭️ CRATES_IO_TOKEN is not configured — skipping crates.io publish."
- uses: actions/checkout@v4
if: env.HAS_TOKEN == 'true'
with:
ref: ${{ needs.validate.outputs.tag }}
- name: Install Rust stable
if: env.HAS_TOKEN == 'true'
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo dependencies
if: env.HAS_TOKEN == 'true'
uses: Swatinem/rust-cache@v2
- name: Publish tui-checkbox
if: env.HAS_TOKEN == 'true'
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
run: |
VERSION="${{ needs.validate.outputs.version }}"
if cargo info tui-checkbox@${VERSION} 2>/dev/null | grep -q "^tui-checkbox"; then
echo "⏭️ tui-checkbox@${VERSION} already on crates.io — skipping."
else
echo "📦 Publishing tui-checkbox…"
output=$(cargo publish --allow-dirty 2>&1) && echo "$output" || {
echo "$output"
echo "$output" | grep -q "already exists" && echo "⏭️ Already published — skipping." || exit 1
}
fi
echo "✅ Publish step complete."