name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
dry_run:
description: 'cargo publish --dry-run only'
type: boolean
default: true
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
release:
name: verify + publish
runs-on: macos-26
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install the Metal Toolchain
run: |
if xcrun --find metal >/dev/null 2>&1; then
echo "Metal compiler already present: $(xcrun --find metal)"
else
xcodebuild -downloadComponent MetalToolchain
fi
xcrun metal --version
- name: Tag matches Cargo.toml version
run: |
set -euo pipefail
VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)"
if [[ "${{ github.event_name }}" == "push" ]]; then
TAG="${GITHUB_REF_NAME#v}"
if [[ "$TAG" != "$VERSION" ]]; then
echo "error: tag v$TAG does not match Cargo.toml $VERSION" >&2
exit 1
fi
fi
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Release readiness (version triad + package denylist)
run: |
chmod +x scripts/check_release_ready.sh
./scripts/check_release_ready.sh
- name: Publish to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.dry_run }}" == "true" ]]; then
cargo publish --locked --dry-run
exit 0
fi
# Prefer the Actions secret; fall back to a no-op when the crate is
# already on crates.io (local `cargo publish` then tag-push).
if [[ -z "${CARGO_REGISTRY_TOKEN:-}" ]]; then
if curl -fsSLA 'tessl-release/1.0' "https://crates.io/api/v1/crates/tessl/${VERSION}" >/dev/null; then
echo "CARGO_REGISTRY_TOKEN unset, but tessl ${VERSION} is already on crates.io — skipping upload"
exit 0
fi
echo "error: CARGO_REGISTRY_TOKEN secret is not set and ${VERSION} is not on crates.io" >&2
exit 1
fi
if cargo publish --locked 2>&1 | tee /tmp/publish.log; then
exit 0
fi
if rg -q 'already uploaded|crate version .* already exists' /tmp/publish.log; then
echo "version already on crates.io — treating as success"
exit 0
fi
exit 1
- name: Create GitHub Release from CHANGELOG
if: ${{ github.event_name == 'push' || inputs.dry_run == false }}
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
TAG="v${VERSION}"
BODY_FILE="$(mktemp)"
VERSION="$VERSION" python3 - <<'PY' >"$BODY_FILE"
import os, pathlib, re, sys
text = pathlib.Path("CHANGELOG.md").read_text()
version = os.environ["VERSION"]
pat = rf"## \[{re.escape(version)}\][^\n]*\n(.*?)(?=\n## \[|\Z)"
m = re.search(pat, text, re.S)
if not m:
sys.exit(f"CHANGELOG section for {version} not found")
print(m.group(1).strip())
PY
if gh release view "$TAG" >/dev/null 2>&1; then
echo "GitHub Release $TAG already exists; leaving it"
else
gh release create "$TAG" --title "tessl ${VERSION}" --notes-file "$BODY_FILE"
fi