Repository navigation
406 lines (378 loc) · 17.9 KB
/
Copy pathrelease.yml
File metadata and controls
406 lines (378 loc) · 17.9 KB
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
name: Release (github, dockerhub, crates.io, npm)
# Least privilege per job below; nothing here needs more than `contents`.
# Creating the release and attaching assets need `contents: write`; the
# crates.io publish authenticates with CARGO_REGISTRY_TOKEN, not GITHUB_TOKEN.
permissions: {}
on:
push:
tags:
- v*
workflow_dispatch:
inputs:
tag:
description: Existing release tag whose missing crates should be published
required: true
type: string
jobs:
crates:
name: Publish crates to crates.io
# Deliberately NOT CI_RUNNER. Publishing and deploying must not depend on a
# particular machine being switched on: GitHub does not fall back when a
# self-hosted runner is offline, it queues for 24h and then fails. A tag
# push once left crates.io publishing queued for a day for exactly that
# reason. Self-hosted is for main.yml, where a warm Dagger cache is worth
# having and a stall only costs slow CI.
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
- uses: dtolnay/rust-toolchain@1.98.1
with:
targets: wasm32-unknown-unknown
# `version: 8` used to be pinned here while browser/package.json declares
# `packageManager: pnpm@<version>`, and there was no Node at all — so this
# job could not have run a JS build even though, as the next step
# explains, it has to. There is no root package.json for
# pnpm/action-setup to read the version from, hence the explicit one; keep
# it equal to browser/package.json's `packageManager`.
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 11.10.0
run_install: false
# The published crate has to CONTAIN the built frontend.
#
# `atomic-server` embeds the data-browser, and server/Cargo.toml's
# `include` lists `assets_tmp` for exactly that reason. But assets_tmp is
# gitignored: it is a build output that `server/build.rs` fills by copying
# `browser/data-browser/dist` into it. A fresh CI checkout therefore does
# not have it, and `cargo package` assembles the .crate from what is on
# disk BEFORE it runs any build script — so publishing straight after
# checkout produces a crate with no frontend in it.
#
# That is not a theoretical concern. Someone running `cargo install
# atomic-server` has no `browser/` directory, so build.rs cannot rebuild
# the assets there; it depends on assets_tmp having been shipped. The
# 0.40.1 crate on crates.io does contain it — because that release was
# packaged on a machine that happened to have already built the frontend,
# not because anything here guaranteed it.
#
# Copying dist ourselves rather than letting build.rs do it keeps this to
# a JS build instead of also compiling the whole server (~1460 crates)
# just to run a build script. The `cargo publish` verification build then
# finds assets_tmp already present and skips the JS build entirely — it
# runs from target/package/, where `../browser/` does not exist.
- name: Build the frontend that gets embedded in the crate
run: |
set -euo pipefail
cd browser
pnpm install --frozen-lockfile
pnpm run build
cd ..
rm -rf server/assets_tmp
cp -r browser/data-browser/dist server/assets_tmp
test -f server/assets_tmp/index.html
# crates.io rejects any .crate over 10 MiB, and these assets are most
# of ours — the atomic_wasm and loro_wasm blobs alone are ~9 MiB
# uncompressed. Measured 7.4 MiB at v0.41.0-beta.2, so there is room,
# but not a lot. Fail here with a readable number rather than at the
# upload, which happens AFTER atomic_lib has already gone out and
# taken its version number with it.
#
# Note `dist` never contains the brotli `.br` siblings: build.rs
# writes those into assets_tmp on release builds, downstream of this.
# They would add ~4 MiB and put the crate over the limit, so if this
# ever starts failing, check that something has not started copying
# them in before looking for real asset growth.
size=$(du -sm server/assets_tmp | cut -f1)
echo "Embedded assets: ${size} MiB uncompressed"
if [ "$size" -gt 40 ]; then
echo "::error::Embedded assets are ${size} MiB uncompressed — the .crate is likely to exceed the 10 MiB crates.io limit."
exit 1
fi
# Runs BEFORE publishing, because crates.io is append-only: a version
# pushed with (say) browser/lib/package.json still on the previous
# number cannot be withdrawn, only yanked, and the mismatched pair stays
# visible forever. The 15 version sites share no single source of truth,
# so nothing else in this pipeline compares them to the tag.
- name: Version sites must match the tag
env:
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
run: node scripts/bump-version.mjs --check "${RELEASE_TAG#v}"
# beta.7's immutable tag omitted an existing build-script module from
# the package allowlist. Correct packaging metadata only when recovering
# that tag; the Rust source itself remains exactly the tagged source.
- name: Repair beta.7 package allowlist during recovery
if: github.event_name == 'workflow_dispatch' && inputs.tag == 'v0.41.0-beta.7'
run: |
python3 - <<'PYTHON'
from pathlib import Path
manifest = Path("server/Cargo.toml")
source = manifest.read_text()
if '"build_assets.rs"' not in source:
old = '"build.rs", "wit/**/*"'
assert old in source, "Unexpected beta.7 package allowlist"
source = source.replace(old, '"build.rs", "build_assets.rs", "wit/**/*"', 1)
manifest.write_text(source)
PYTHON
# Publish directly: katyo/publish-crates@v1 runs cargo update between
# crates, replacing the dependency graph that passed CI. Retain the lock
# and skip existing versions so a partial release can safely resume.
- name: Publish missing crates with the validated dependency lock
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
shell: bash
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
for crate in atomic_lib atomic-cli atomic-server; do
curl --fail --silent --show-error --retry 3 \
"https://index.crates.io/at/om/$crate" -o /tmp/release-crate-index
if python3 -c 'import json,sys; sys.exit(not any(json.loads(line)["vers"] == sys.argv[1] for line in open(sys.argv[2])))' "$version" /tmp/release-crate-index; then
echo "$crate $version is already published; skipping"
continue
fi
cargo publish -p "$crate" --locked --allow-dirty
done
# npm was the missing half of a tag release. crates.io has been automated
# here for a while; `@tomic/*` was still `pnpm publish -r` on someone's
# laptop, which is why npm `latest` is 0.40.0 and the `beta` tag stayed on
# 0.41.0-beta.0 (2025-06) while v0.41.0-beta.1 through beta.7 went out to
# crates.io and GitHub. A tag that publishes one ecosystem and forgets the
# other is a broken release: consumers of `@tomic/lib` cannot install the
# version the changelog describes.
#
# Its own job, not a step on `crates`: a 401 from npmjs.org must not
# re-attempt a crates.io publish that already succeeded, and a re-run here
# must be safe against versions that already landed. `pnpm publish -r`
# skips any package+version already on the registry, so a partial publish
# can be retried without `--force`.
npm:
name: Publish packages to npm
# Deliberately NOT CI_RUNNER, same reason as `crates`.
runs-on: ubuntu-latest
# `id-token: write` is what npm Trusted Publishing (OIDC) needs. Each
# `@tomic/*` package is configured on npmjs.com to accept this repository
# and this workflow file; no long-lived token is stored. `contents: read`
# is the minimum for checkout.
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
- uses: actions/setup-node@v4
with:
node-version: 22
# Writes a root `.npmrc` pointing at the public registry. With no
# NODE_AUTH_TOKEN, pnpm falls through to the OIDC exchange.
registry-url: https://registry.npmjs.org
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
# Latest 10.x: Trusted Publishing support landed in pnpm 10.16 and
# the repo pin (browser/package.json) is older. There is no root
# package.json for the action to read a version from, so this is
# explicit, like the crates job.
version: 11.10.0
run_install: false
# Do NOT run the workspace `pnpm run build`: that walks into
# `@tomic/data-browser`, which shells out to wasm-pack, and the npm
# packages do not embed that frontend.
- name: Install the JS workspace
run: |
set -euo pipefail
cd browser
pnpm install --frozen-lockfile
# Same check as the crates job, same reason: npm is append-only for a
# given version. A tag whose browser/lib/package.json still says the
# previous number publishes a stale `@tomic/lib` that can never be
# replaced. The dist-tag cases run here too so a broken tag mapping
# fails before any upload.
- name: Version sites must match the tag
env:
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
run: |
set -euo pipefail
node scripts/bump-version.mjs --check "${RELEASE_TAG#v}"
node scripts/npm-dist-tag.mjs --check
# Build before publish, lib first: cli / react / svelte / plugin /
# edit-mode / create-template all import `@tomic/lib` types, and one
# concurrent `--filter` list fails with TS2307 on a fresh checkout.
- name: Build publishable packages
run: |
set -euo pipefail
cd browser
pnpm --filter "@tomic/lib" run build
pnpm --filter "@tomic/react" \
--filter "@tomic/cli" \
--filter "@tomic/svelte" \
--filter "@tomic/create-template" \
--filter "@tomic/plugin" \
--filter "@tomic/edit-mode" \
run build
- name: Publish to npm
env:
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
run: |
set -euo pipefail
# A pre-release must NOT become `latest`: `pnpm publish` tags
# `latest` unless told otherwise, which would make
# `npm i @tomic/lib` install a beta. scripts/npm-dist-tag.mjs maps
# `v0.41.0-beta.8` to `beta` and `v0.41.0` to `latest`.
version="${RELEASE_TAG#v}"
dist_tag=$(node scripts/npm-dist-tag.mjs "$version")
echo "Publishing ${version} with dist-tag ${dist_tag}"
cd browser
# `--no-git-checks`: a detached tag HEAD is not a publish branch.
# `--access public`: scoped packages default to restricted, and
# `@tomic/plugin` / `@tomic/edit-mode` have never been published
# so they have no registry-side access flag yet.
# `--ignore-scripts`: skip prepublishOnly. We already built
# above, and `@tomic/lib`'s `lint-package` → `attw` currently
# crashes (`Cannot read properties of undefined (reading
# 'filename')`), which would fail every release. Lint and
# packaging checks belong in the main pipeline. The
# `workspace:*` rewrite is done by `pnpm publish` itself, not
# by those scripts.
# Recursive publish skips `private: true` (@tomic/root,
# data-browser, e2e) and any version already on the registry.
# DONT use `pnpm npm publish` — that skips the workspace:*
# rewrite and would publish a package that cannot resolve
# `@tomic/lib` (browser/CONTRIBUTING.md).
pnpm publish -r --no-git-checks --ignore-scripts --access public --tag "$dist_tag" --report-summary
if [ -f pnpm-publish-summary.json ]; then
echo "Publish summary:"
cat pnpm-publish-summary.json
fi
create-release:
if: github.event_name == 'push'
name: Create Release on GitHub
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: taiki-e/create-gh-release-action@v1
with:
# (optional)
changelog: CHANGELOG.md
env:
# (required)
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# The static musl binaries — the ones that run anywhere.
#
# `upload-assets` below builds for each runner's NATIVE target, so its Linux
# leg produces a glibc binary linked against whatever ubuntu-latest ships.
# That is currently glibc 2.38, which will not start on Ubuntu 22.04 (2.35,
# supported into 2027) or Debian 12 (2.36) — including our own staging box,
# where v0.41.0-beta.2 failed with `version 'GLIBC_2.38' not found`.
#
# Cross-compiling them statically is the entire reason the Dagger pipeline
# exists (`releaseAssets` / `TARGET_IMAGE_MAP`), but no workflow ever called
# it: releases up to v0.40.0 carried `x86_64-unknown-linux-musl` and
# `armv7-unknown-linux-musleabihf`, then v0.40.1 through v0.41.0-beta.1
# shipped nothing at all (the build.rs failure noted below), and when that was
# repaired the job had become native-only. The musl targets never came back.
#
# Separate job rather than another matrix leg: one Dagger invocation emits all
# three targets, and it needs no Node/Rust toolchain on the runner.
upload-musl-assets:
if: github.event_name == 'push'
name: Cross-compile and upload the static musl binaries
# Needs the release to exist before anything can be attached to it.
needs: create-release
# Deliberately NOT CI_RUNNER, for the same reason the other release jobs
# avoid it: a release must not wait on one machine being switched on.
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@8.0.0
with:
version: "latest"
# Keep the shared engine up: Mancave runs several jobs at once, and the
# default stop would kill the engine under a job still using it.
engine-stop: "false"
verb: call
args: --cache-namespace ${{ runner.environment == 'self-hosted' && runner.name || 'hosted' }} release-assets export --path ./release-assets
cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }}
- name: Attach them to the release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
# Fail loudly on an empty export. Without this the upload silently
# succeeds with nothing attached, which is how the musl binaries went
# missing for four releases without anyone noticing.
count=$(find ./release-assets -type f | wc -l)
echo "Exported ${count} binaries:"
ls -l ./release-assets
if [ "$count" -eq 0 ]; then
echo "::error::release-assets produced no binaries."
exit 1
fi
gh release upload "${GITHUB_REF_NAME}" ./release-assets/* --clobber
upload-assets:
if: github.event_name == 'push'
strategy:
matrix:
os:
- ubuntu-latest
- macos-latest
# Currently not supported
# - windows-latest
# Deliberately NOT switched to CI_RUNNER: this job exists to build release
# assets per platform, and the self-hosted runner is Linux/X64 only, so it
# cannot serve the macos-latest leg. Pinning the whole matrix to it would
# silently stop producing macOS binaries.
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- uses: actions/checkout@v4
# Unlike the `crates` job, this one builds from the full repo, so
# `server/build.rs` finds `../browser/` and runs the JS build itself
# rather than reusing a packaged assets_tmp. That needs a working Node
# toolchain, and the wasm32 target for data-browser's `build:wasm`.
#
# It previously had none of that -- pnpm 8 against a repo that declares
# pnpm 10, no Node, no wasm target -- which is why every release since
# v0.40.2 died here with `panicked at server/build.rs: js build failed`
# and shipped no binaries. Checkout now comes first so the setup steps
# act on a repo that exists.
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 11.10.0
run_install: false
- uses: dtolnay/rust-toolchain@1.98.1
with:
targets: wasm32-unknown-unknown
- uses: taiki-e/upload-rust-binary-action@v1
with:
# (required)
bin: atomic-server
# (optional) On which platform to distribute the `.tar.gz` file.
# [default value: unix]
# [possible values: all, unix, windows, none]
tar: all
# (optional) On which platform to distribute the `.zip` file.
# [default value: windows]
# [possible values: all, unix, windows, none]
zip: windows
env:
# (required)
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}