1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
name: Release to crates.io
on:
push:
tags:
jobs:
publish:
name: publish to crates.io
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Extract version from tag
id: version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Validate version matches Cargo.toml
run: |
PACKAGE_VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
if [ "$PACKAGE_VERSION" != "${{ steps.version.outputs.VERSION }}" ]; then
echo "::error::Tag version (${{ steps.version.outputs.VERSION }}) doesn't match Cargo.toml version ($PACKAGE_VERSION)"
exit 1
fi
# Rebuilds and re-runs the offline suite here rather than trusting
# an artifact from ci.yml's own run -- this workflow triggers off a
# tag push, a separate event from the branch push ci.yml already
# validated, so there's no prior run's output to reuse. Deliberately
# NOT --locked anywhere below: this repo doesn't commit Cargo.lock
# (library crate; a committed lock silently caps what a loose
# constraint resolves to on every later run -- see the workspace's
# own lockfile-hygiene convention), so a fresh checkout has no lock
# to be strict against.
- run: cargo build --workspace --all-targets
- run: cargo test --workspace --all-features
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- run: cargo fmt --all -- --check
# Dry-run needs no registry auth -- it only verifies metadata,
# compresses the package, and checks the result, never uploads.
- name: Dry-run publish (catches packaging issues before the real one)
run: cargo publish --dry-run
# CRATES_IO_TOKEN (a plain crates.io API token, cargo login-style) --
# NOT OIDC Trusted Publishing, unlike this org's npm (macula-mcp)
# and NuGet (macula-dotnet) release workflows. crates.io does
# support Trusted Publishing (rust-lang/crates-io-auth-action, same
# shape as those two), but it has no PyPI-style "pending publisher"
# for a crate that doesn't exist yet -- a Trusted Publisher can only
# be configured on crates.io AFTER a crate's first release already
# exists there. A static token is the only mechanism that can
# actually perform that first release from CI at all.
#
# macula-rust v0.2.3 published this way 2026-09-05 -- the migration
# path once someone wants to spend the time on it: add a Trusted
# Publisher for macula-io/macula-rust + this workflow file in the
# crate's crates.io settings, replace this step with
# `rust-lang/crates-io-auth-action@v1` (permissions:
# id-token: write) feeding its `token` output to
# CARGO_REGISTRY_TOKEN the same way, then delete the CRATES_IO_TOKEN
# repo secret -- no code reason to keep a long-lived token around
# once the OIDC path is available.
- name: Publish to crates.io
run: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}