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
# Release — on a vX.Y.Z tag push:
# 1. verify the tag matches Cargo.toml's version
# 2. build cross-platform binaries on the self-hosted fleet
# 3. publish-crate to crates.io via Trusted Publishing (OIDC — no stored token)
# 4. release cut a GitHub Release with the binaries attached
#
# Triggered ONLY by tag pushes, which require write access — a fork PR can never
# trigger this, so it is safe to run on the persistent Windows/macOS runners
# (the publish credential is never exposed to untrusted code).
#
# 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`.
# - macOS → register the M2 Max runner (labels: self-hosted,macos,arm64).
# - Runner provisioning: rustup everywhere; MSVC Build Tools on Windows;
# Xcode Command Line Tools on macOS; cc/gcc on Linux.
name: Release
on:
push:
tags:
permissions:
contents: write # create the GitHub Release
id-token: write # OIDC token for crates.io Trusted Publishing
jobs:
# Guard: a pushed tag whose version disagrees with Cargo.toml is a mistake.
verify:
runs-on: homeserver-pool
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: homeserver-pool
container: ghcr.io/twowells/rust-ci:latest
archive: tar
- target: x86_64-pc-windows-msvc
runner:
archive: zip
- target: aarch64-apple-darwin
runner:
archive: tar
runs-on: ${{ matrix.runner }}
# Linux builds inside the rust-ci image (bare ARC pod). Windows/macOS define
# no matrix container, so this evaluates empty and they build natively on the
# VM / M2 Max, 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'
shell: pwsh
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: homeserver-pool
# Bare ARC pod → 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: homeserver-pool
steps:
- uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true
- uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: v${{ needs.verify.outputs.version }}
generate_release_notes: true
files: dist/*