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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# Release — on a vX.Y.Z tag push:
# 1. verify the tag matches Cargo.toml's version
# 2. build cross-platform binaries on GitHub-hosted runners
# 3. publish-crate to crates.io via Trusted Publishing (OIDC — no stored token)
# 4. release cut a GitHub Release with the binaries attached + attested
#
# Triggered ONLY by tag pushes, which require write access — a fork PR can never
# trigger this, so the publish credential is never exposed to untrusted code.
#
# Runners are GitHub-hosted: Linux builds inside ghcr.io/twowells/rust-ci (which
# carries rustup + the pinned toolchain + cc), Windows on windows-latest (MSVC),
# macOS on macos-14 (Apple Silicon, Xcode CLT). rustup self-installs the pinned
# toolchain from rust-toolchain.toml on each, so nothing is pre-provisioned.
#
# Release archives are attested with build provenance (actions/attest-build-
# provenance), giving each binary a verifiable link back to this workflow run.
#
# This is the first workflow to create GitHub Releases; v0.1.0 was tagged before
# it existed (a pre-history crates.io name-grab), so v0.2.0 is the first Release.
#
# One-time setup before the first successful run:
# - crates.io → crate Settings → Trusted Publishing → add repo TwoWells/Lattice
# and workflow file `release.yml`.
name: Release
on:
push:
tags:
permissions:
contents: write # create the GitHub Release
id-token: write # OIDC token for crates.io Trusted Publishing + provenance signing
attestations: write # write build-provenance attestations for the release archives
jobs:
# Guard: a pushed tag whose version disagrees with Cargo.toml is a mistake.
verify:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.v.outputs.version }}
steps:
- uses: actions/checkout@v6
- id: v
name: Tag must match Cargo.toml version
run: |
tag="${GITHUB_REF_NAME#v}"
manifest="$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')"
echo "tag=$tag manifest=$manifest"
if [ "$tag" != "$manifest" ]; then
echo "::error::tag v$tag does not match Cargo.toml version $manifest"
exit 1
fi
echo "version=$tag" >> "$GITHUB_OUTPUT"
# Native build on each platform's own runner. fail-fast: false so one
# platform's failure doesn't cancel the others.
build:
needs: verify
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
runner: ubuntu-latest
container: ghcr.io/twowells/rust-ci:latest
archive: tar
- target: x86_64-pc-windows-msvc
runner: windows-latest
archive: zip
- target: aarch64-apple-darwin
runner: macos-14
archive: tar
runs-on: ${{ matrix.runner }}
# Linux builds inside the rust-ci image on the hosted runner. Windows/macOS
# define no matrix container, so this evaluates empty and they build natively
# on windows-latest / macos-14, which carry their own MSVC / Xcode toolchains.
container: ${{ matrix.container }}
steps:
- uses: actions/checkout@v6
- name: Materialize Rust toolchain
run: |
rustup show
rustup target add ${{ matrix.target }}
- name: Build release binary
run: cargo build --release --locked --target ${{ matrix.target }}
- name: Package (tar.gz)
if: matrix.archive == 'tar'
shell: bash
run: |
mkdir -p dist
tar -C "target/${{ matrix.target }}/release" -czf \
"dist/lattice-${{ matrix.target }}.tar.gz" lattice
- name: Package (zip)
if: matrix.archive == 'zip'
# windows-latest ships both Windows PowerShell 5.1 (powershell) and
# PowerShell 7 (pwsh); we pin `powershell` (5.1) deliberately for a stable,
# always-present shell. Compress-Archive is built in there, so this works.
shell: powershell
run: |
New-Item -ItemType Directory -Force dist | Out-Null
Compress-Archive -Force `
-Path "target/${{ matrix.target }}/release/lattice.exe" `
-DestinationPath "dist/lattice-${{ matrix.target }}.zip"
- uses: actions/upload-artifact@v7
with:
name: lattice-${{ matrix.target }}
path: dist/*
# Publish to crates.io. Idempotent: skips if this version is already live, so
# a re-run (or a re-pushed tag) is a harmless no-op rather than a hard failure.
publish-crate:
needs: verify
runs-on: ubuntu-latest
# Hosted runner → run in the toolchain image (cargo publish + the curl guard).
container: ghcr.io/twowells/rust-ci:latest
# Container default shell is sh (dash); force bash for the guard/publish steps.
defaults:
run:
shell: bash
# Must match the Environment name registered in crates.io Trusted Publishing.
# Scopes the OIDC token subject to this environment (tighter trust binding)
# and gives you a hook for required-reviewer / tag protection rules later.
# Create it under Settings -> Environments to attach those rules; otherwise
# GitHub auto-creates it (unprotected) on first run.
environment: release
steps:
- uses: actions/checkout@v6
- name: Materialize Rust toolchain
run: rustup show
- name: Skip if version already on crates.io
id: guard
run: |
v="${{ needs.verify.outputs.version }}"
# Query the sparse index, NOT /api/v1 — the API 403s requests without a
# descriptive User-Agent, but the index enforces no such policy.
# Path layout for a name >= 4 chars: <chars 1-2>/<chars 3-4>/<name>.
# `|| true` tolerates the 404 a never-yet-published crate returns.
idx="$(curl -fsS https://index.crates.io/la/tt/lattice || true)"
if printf '%s\n' "$idx" | grep -q "\"vers\":\"$v\""; then
echo "lattice $v is already published — skipping."
echo "already=1" >> "$GITHUB_OUTPUT"
fi
# OIDC: exchanges the GitHub id-token for a short-lived crates.io token.
# Nothing long-lived is stored in secrets or on the runner.
- name: Authenticate to crates.io (Trusted Publishing)
if: steps.guard.outputs.already != '1'
id: auth
uses: rust-lang/crates-io-auth-action@v1
- name: cargo publish
if: steps.guard.outputs.already != '1'
run: cargo publish --locked
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
release:
needs:
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true
# Sign a build-provenance attestation for every release archive, linking
# each binary back to this workflow run. The globs cover the Linux/macOS
# tar.gz and the Windows zip; Lattice ships no .sha256 sidecars, so these
# match exactly the archives and nothing else.
- uses: actions/attest-build-provenance@v4
with:
subject-path: |
dist/lattice-*.tar.gz
dist/lattice-*.zip
- uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: v${{ needs.verify.outputs.version }}
generate_release_notes: true
files: dist/*