name: Release
on:
push:
tags:
- "v*"
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
publish:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Read crate metadata
id: meta
run: |
python - <<'PY' >> "$GITHUB_OUTPUT"
import json
import subprocess
metadata = json.loads(
subprocess.check_output(
["cargo", "metadata", "--no-deps", "--format-version", "1"],
text=True,
)
)
package = metadata["packages"][0]
print(f"name={package['name']}")
print(f"version={package['version']}")
PY
- name: Verify tag matches crate version
env:
CRATE_VERSION: ${{ steps.meta.outputs.version }}
run: |
tag_version="${GITHUB_REF_NAME#v}"
if [ "$tag_version" != "$CRATE_VERSION" ]; then
echo "tag $GITHUB_REF_NAME does not match Cargo.toml version $CRATE_VERSION" >&2
exit 1
fi
- name: Verify release tag points to main
run: |
git fetch origin main
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "release tags must point to a commit reachable from origin/main" >&2
exit 1
fi
- run: cargo fmt -- --check
- run: cargo clippy --locked --all-targets --all-features -- -D warnings
- run: cargo test --locked
- run: cargo package --locked
- run: cargo publish --dry-run --locked
- name: Check crates.io for existing version
id: crates
env:
CRATE_NAME: ${{ steps.meta.outputs.name }}
CRATE_VERSION: ${{ steps.meta.outputs.version }}
run: |
status_code="$(
curl -s -o /dev/null -w "%{http_code}" \
-H "User-Agent: diskr-release-workflow" \
-H "Accept: application/json" \
"https://crates.io/api/v1/crates/$CRATE_NAME/$CRATE_VERSION"
)"
if [ "$status_code" = "200" ]; then
echo "already_published=true" >> "$GITHUB_OUTPUT"
elif [ "$status_code" = "404" ]; then
echo "already_published=false" >> "$GITHUB_OUTPUT"
else
echo "unexpected crates.io status: $status_code" >&2
exit 1
fi
- name: Publish to crates.io
if: steps.crates.outputs.already_published != 'true'
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
if [ -z "$CARGO_REGISTRY_TOKEN" ]; then
echo "CARGO_REGISTRY_TOKEN secret is not configured" >&2
exit 1
fi
cargo publish --locked
- name: Download published crate
id: published
env:
CRATE_NAME: ${{ steps.meta.outputs.name }}
CRATE_VERSION: ${{ steps.meta.outputs.version }}
run: |
crate_archive="$RUNNER_TEMP/${CRATE_NAME}-${CRATE_VERSION}.crate"
for attempt in $(seq 1 12); do
if curl -fsSL \
-H "User-Agent: diskr-release-workflow" \
-H "Accept: application/octet-stream" \
"https://crates.io/api/v1/crates/$CRATE_NAME/$CRATE_VERSION/download" \
-o "$crate_archive"; then
echo "crate_archive=$crate_archive" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$attempt" -eq 12 ]; then
echo "published crate did not become available on crates.io" >&2
exit 1
fi
sleep 5
done
- name: Verify published crate provenance
env:
EXPECTED_SHA: ${{ github.sha }}
CRATE_ARCHIVE: ${{ steps.published.outputs.crate_archive }}
run: |
python - <<'PY'
import json
import os
import tarfile
archive = os.environ["CRATE_ARCHIVE"]
expected_sha = os.environ["EXPECTED_SHA"]
with tarfile.open(archive, "r:gz") as tf:
member = next(
(item for item in tf.getmembers() if item.name.endswith("/.cargo_vcs_info.json")),
None,
)
if member is None:
raise SystemExit("published crate is missing .cargo_vcs_info.json")
with tf.extractfile(member) as fh:
info = json.load(fh)
actual_sha = (info.get("git") or {}).get("sha1")
if actual_sha != expected_sha:
raise SystemExit(
f"published crate was built from {actual_sha}, expected {expected_sha}"
)
PY
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "GitHub release $GITHUB_REF_NAME already exists; leaving it unchanged."
exit 0
fi
notes_file="$RUNNER_TEMP/release-notes.md"
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
"repos/$GITHUB_REPOSITORY/releases/generate-notes" \
-f tag_name="$GITHUB_REF_NAME" \
-f target_commitish="$GITHUB_SHA" \
--jq .body > "$notes_file"
gh release create "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" \
--title "$GITHUB_REF_NAME" \
--notes-file "$notes_file"