Skip to content

Commit 9f76774

Browse files
docs(release): wire pre-release-check + RELEASING.md + comments to new flow
- pre-release-check: clarify that swift xcframework is built here as a sanity check only; the shipping zip comes from release_swift.yml on PR CI. Same-host determinism check kept (catches stale caches). - RELEASING.md: rewrite for the two-phase version bump. Local prepare-release does versions only (no Xcode required); PR CI's release_swift.yml bakes the Swift xcframework SHA into Package.swift on the release branch and uploads the zip to a draft GitHub release. Tag CI just promotes the draft. New 'Why PR-CI builds the Swift xcframework' section documents the determinism trade-off the old local-baked flow kept hitting. - Package.swift, bump_version.py, zip_xcframework.sh: comments updated to match the new flow (releaseChecksum is written by PR CI, not local; zip_xcframework's determinism is now same-host sanity, not cross-host reproducibility).
1 parent 9062172 commit 9f76774

5 files changed

Lines changed: 104 additions & 73 deletions

File tree

Makefile.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ dependencies = ["format-check", "clippy", "docs", "test-rust"]
579579
# ---------------------------------------------------------------------------
580580

581581
[tasks.pre-release-check]
582-
description = "Local dry-run of every release artifact build (no uploads, no network publishes). Run before tagging."
582+
description = "Local dry-run of every release artifact build (no uploads, no network publishes). Run before opening the release PR. Swift xcframework + zip is exercised here as a sanity check but the published release uses the zip that release_swift.yml builds on PR CI — local SHAs do NOT need to match CI."
583583
script_runner = "@shell"
584584
script = '''
585585
set -eu
@@ -589,16 +589,20 @@ if [ "$(uname -s)" != "Darwin" ]; then
589589
exit 1
590590
fi
591591
592-
echo "==> swift: build + verify layout + iOS consumer build + deterministic zip"
592+
echo "==> swift: build + verify layout + iOS sim test + same-host zip determinism"
593593
cargo make swift-xcframework >/dev/null
594594
cargo make verify-swift-xcframework >/dev/null
595595
cargo make verify-swift-consumer >/dev/null
596596
bash scripts/release/zip_xcframework.sh
597597
H1=$(shasum -a 256 IrohLib.xcframework.zip | awk '{print $1}')
598598
bash scripts/release/zip_xcframework.sh
599599
H2=$(shasum -a 256 IrohLib.xcframework.zip | awk '{print $1}')
600-
[ "$H1" = "$H2" ] || { echo "swift zip non-deterministic: $H1 vs $H2" >&2; exit 1; }
601-
echo " ok (sha256=$H1)"
600+
# Same-host determinism: the build must produce identical bytes when run twice
601+
# on the same machine. Catches stale caches / non-deterministic flags. Doesn't
602+
# attempt cross-host match — release_swift.yml is the only build whose zip
603+
# actually ships.
604+
[ "$H1" = "$H2" ] || { echo "swift zip non-deterministic on this host: $H1 vs $H2" >&2; exit 1; }
605+
echo " ok (sha256=$H1, ships from CI not from here)"
602606
603607
echo "==> python: maturin build + wheel consumer smoke"
604608
PYVENV=$(mktemp -d)/venv

Package.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ import Foundation
1919
// to the release zip. Set IROH_FORCE_REMOTE_XCFRAMEWORK to force the release
2020
// zip even in a built checkout.
2121
//
22-
// The two release literals below are the only things `cargo make
23-
// prepare-release` rewrites (per Phase 6 plan, CI never writes to main).
24-
// Local prepare-release builds a deterministic xcframework zip, shasums it,
25-
// and bakes both values into this manifest in the release commit.
22+
// `releaseTag` is rewritten locally by `cargo make prepare-release <V>`.
23+
// `releaseChecksum` is rewritten on PR CI by `release_swift.yml` (one bot
24+
// commit on the release branch, marked `[skip swift-release]`), so the value
25+
// here always matches the IrohLib.xcframework.zip attached to the GitHub
26+
// release — never a cross-host determinism game.
2627
let releaseTag = "v1.0.0"
2728
let releaseChecksum = "514b147f7965fe17acaece9a1157cf9421463b6c9282224983e871ea868b86ef"
2829

RELEASING.md

Lines changed: 73 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
# Releasing iroh-ffi
22

3-
A release ships a new version of the iroh-ffi crate's surface to every
4-
binding host (npm, PyPI, GitHub Releases for Swift) plus refreshed docs on
5-
GitHub Pages. The tag is the trigger — pushing `v<VERSION>` fans out to all
6-
four publish workflows independently.
3+
A release ships a new version of the iroh-ffi crate's surface to every binding
4+
host (npm, PyPI, Maven Central, GitHub Releases for Swift) plus refreshed docs
5+
on GitHub Pages. The tag is the trigger — pushing `v<VERSION>` fans out to all
6+
publish workflows independently.
77

8-
The repo manifest stays **self-consistent at every commit**: the version
9-
literals in `Cargo.toml`, `iroh-js/package.json`, and `Package.swift` (tag +
10-
checksum) are written by a local script before the release PR is opened. CI
11-
never writes to `main`.
8+
The repo manifest stays self-consistent at every commit. Version literals in
9+
`Cargo.toml`, `iroh-js/package.json`, etc. are written by a local script
10+
before the release PR is opened; the Swift xcframework SHA-256 in
11+
`Package.swift` is written by PR CI (one bot commit on the release branch).
12+
CI never writes to `main`.
1213

1314
## Prerequisites
1415

15-
- macOS workstation with Xcode (the prepare-release script builds the Apple
16-
xcframework to compute the SwiftPM checksum).
16+
- Local shell with `cargo`, `yarn`, and Python 3 (no Xcode needed —
17+
prepare-release no longer builds the xcframework).
1718
- Push rights on the repo + the relevant publisher accounts (npm
1819
`@number0/iroh`; PyPI trusted publisher for `iroh`; Sonatype Central for
19-
`computer.iroh` once Maven publishing is wired).
20+
`computer.iroh`).
2021
- `main` is green at the commit you want to release from.
2122

2223
## Steps
2324

24-
0. **Local dry-run** (recommended). Validates every artifact builds without
25-
touching any registry:
25+
0. **Local dry-run** (recommended, macOS only — needs Xcode for the swift slot).
26+
Validates every artifact builds without touching any registry:
2627

2728
```sh
2829
# the armored private key (same contents as the SIGNING_KEY CI secret)
@@ -31,37 +32,47 @@ never writes to `main`.
3132
cargo make pre-release-check
3233
```
3334

34-
Builds the Swift xcframework + deterministic zip, the Python wheel,
35-
the napi addon + `npm publish --dry-run`, and `gradle
36-
publishToMavenLocal`. Catches config typos, signing failures, manifest
37-
errors before the real publish.
35+
Builds the Swift xcframework (sanity check only — the published zip
36+
comes from PR CI, not here), the Python wheel + import smoke, the napi
37+
addon + `npm publish --dry-run`, and `gradle publishToMavenLocal` +
38+
JNA-from-per-platform-staged smoke. Catches config typos, signing
39+
failures, manifest errors before the real publish.
3840

39-
1. **Prepare the release branch + commit.**
41+
1. **Bump versions on a release branch.**
4042

4143
```sh
4244
git checkout main && git pull
4345
cargo make prepare-release 1.0.0-rc.1 # no leading v
44-
```
45-
46-
This rewrites the version literals, refreshes lockfiles, builds a
47-
deterministic `IrohLib.xcframework.zip`, and bakes its SHA-256 into
48-
`Package.swift`. Inspect the diff (`git diff`) — expected files only:
49-
`Cargo.toml`, `Cargo.lock`, `iroh-js/package.json`, `iroh-js/yarn.lock`,
50-
`Package.swift`.
51-
52-
2. **Open the release PR.**
53-
54-
```sh
5546
git checkout -b release/v1.0.0-rc.1
5647
git commit -am "chore(release): v1.0.0-rc.1"
5748
git push -u origin release/v1.0.0-rc.1
5849
gh pr create --base main --fill --title "chore(release): v1.0.0-rc.1"
5950
```
6051

61-
Get all CI green on the PR.
52+
`prepare-release` only rewrites version literals + lockfiles. Inspect
53+
the diff (`git diff`) — expected files only: `Cargo.toml`, `Cargo.lock`,
54+
`iroh-js/package.json` + `iroh-js/Cargo.toml`, `iroh-js/yarn.lock`,
55+
`pyproject.toml`, `kotlin/lib/build.gradle.kts`, `Package.swift`
56+
(releaseTag only — the checksum is filled in by CI in step 2).
6257

63-
3. **Merge to `main`.** Squash or merge-commit — both work; whichever lands
64-
keeps Package.swift consistent.
58+
2. **Wait for PR CI to bake the Swift xcframework SHA.**
59+
60+
`release_swift.yml` fires on push to `release/v*` branches. It builds
61+
the xcframework on a self-hosted macOS runner, runs the verify gates
62+
(layout + iOS sim test), zips deterministically, and:
63+
- commits `chore(release): bake Swift xcframework SHA <hex>
64+
[skip swift-release]` to the release branch (overwrites the
65+
placeholder checksum in `Package.swift`);
66+
- uploads `IrohLib.xcframework.zip` to a draft GitHub release named
67+
`v1.0.0-rc.1`.
68+
69+
Review the bot's commit + the draft release asset (downloadable from
70+
the GitHub Releases page) before merging. The SHA in `Package.swift`
71+
is exactly the SHA of the zip on the draft release — no cross-host
72+
determinism game.
73+
74+
3. **Merge to `main`.** Squash or merge-commit; both keep `Package.swift`
75+
consistent with the draft release.
6576

6677
4. **Tag the merge commit and push.**
6778

@@ -75,32 +86,43 @@ never writes to `main`.
7586

7687
| Workflow | Publishes |
7788
|---|---|
78-
| `release.yml` | GitHub Release + per-OS binary assets + `IrohLib.xcframework.zip` |
79-
| `ci_js.yml` `publish` | `@number0/iroh` to npm (`--provenance`, `NPM_TOKEN`) |
80-
| `wheels.yml` `publish` | `iroh` wheels to PyPI (Trusted Publishing / OIDC — no token) |
89+
| `release.yml` | promotes the draft GH release v<version> to published; uploads per-OS C lib archives |
90+
| `ci_js.yml` `publish` | `@number0/iroh` to npm (`--provenance`, OIDC) |
91+
| `wheels.yml` `publish` | `iroh` wheels to PyPI (OIDC) |
92+
| `release.yml` `build-and-publish-kotlin` | `computer.iroh:iroh` to Maven Central |
8193
| `docs.yml` | GitHub Pages site refresh |
82-
| _(future)_ `release.yml` `build-and-publish-kotlin` | Maven Central `computer.iroh:iroh` (blocked on #147) |
8394

84-
If any publish fails, fix and re-tag with the next patch version (do not
85-
re-use a tag).
95+
The Swift xcframework zip is **not** rebuilt at tag time — `release.yml`
96+
`create-release` just promotes the PR-CI-built draft.
97+
98+
If any publish fails, fix and re-tag with the next patch version (do
99+
not re-use a tag).
86100

87-
## Why local-only prepare
101+
## Why PR-CI builds the Swift xcframework (not local, not tag-CI)
88102

89103
Binary SwiftPM packages need the manifest checksum to match the uploaded
90-
zip byte-for-byte. We use a deterministic zip recipe
91-
(`scripts/release/zip_xcframework.sh`: fixed mtimes + sorted file list + no
92-
zip extra fields) so the local zip and the one rebuilt by `release.yml`
93-
hash identically. This lets `Package.swift` carry the correct checksum
94-
from the release commit forward — no CI write-back to `main`, no
95-
intermediate broken state. Matches the firebase-ios-sdk / Sentry-Cocoa
96-
release pattern for binary SwiftPM packages.
104+
zip byte-for-byte. The previous flow built the xcframework locally during
105+
`prepare-release`, baked the SHA into `Package.swift`, then rebuilt on
106+
tag-CI and expected the SHA to still match — which required cross-host
107+
build determinism (matching Xcode patch version, macOS SDK, rustc, cargo
108+
deps, etc.). That kept failing in practice.
109+
110+
Single source of truth: PR CI builds the zip once, bakes its SHA into the
111+
PR, uploads to a draft release. Tag CI just promotes the draft. The SHA
112+
in `Package.swift` is the SHA of the bytes on the release — they were
113+
literally the same zip. No build-environment skew can break consumers.
114+
115+
The trade-off is that the release branch picks up one bot commit
116+
(`bake Swift xcframework SHA <hex>`), which is reviewable in the PR diff.
117+
`[skip swift-release]` in the commit message prevents the workflow from
118+
re-firing on its own output.
97119

98120
## One-time publisher setup (per host)
99121

100-
- **npm**`NPM_TOKEN` repo secret with publish rights on `@number0/iroh`.
122+
- **npm**Trusted Publishing configured at npmjs.com (no token).
101123
- **PyPI** — register a trusted publisher at
102124
<https://pypi.org/manage/account/publishing/>: project `iroh`, owner
103-
`n0-computer`, repository `iroh-ffi`, workflow `wheels.yml`. No long-lived
104-
token needed. Until registered, the first publish job will fail loudly.
105-
- **Maven Central** _(future, blocked on #147)_ — claim `computer.iroh`
106-
namespace via DNS TXT on `iroh.computer`; GPG signing key; OSSRH creds.
125+
`n0-computer`, repository `iroh-ffi`, workflow `wheels.yml`.
126+
- **Maven Central**`computer.iroh` namespace claimed via DNS TXT on
127+
`iroh.computer`; armored PGP key in `SIGNING_KEY` secret; Sonatype
128+
Central API creds.

scripts/release/bump_version.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
#!/usr/bin/env python3
2-
"""Rewrite the three version literals across the repo as a single source of truth.
3-
4-
Called by `cargo make prepare-release <VERSION>` in two passes:
5-
1. Pass 1 (just VERSION): bumps Cargo.toml [package].version,
6-
iroh-js/package.json version, and Package.swift releaseTag.
7-
2. Pass 2 (--checksum HEX): writes Package.swift releaseChecksum once the
8-
deterministic xcframework zip has been built and shasum'd.
2+
"""Rewrite the version literals across the repo as a single source of truth.
3+
4+
Two entry points:
5+
1. `cargo make prepare-release <V>` calls this with just VERSION to bump
6+
Cargo.toml [package].version, iroh-js/{Cargo.toml,package.json}, all the
7+
iroh-js/npm/*/package.json sub-packages, pyproject.toml,
8+
kotlin/lib/build.gradle.kts coordinates, and Package.swift releaseTag.
9+
2. release_swift.yml (PR CI) calls this with --checksum HEX once it has
10+
built the xcframework and shasum'd the deterministic zip, to write
11+
Package.swift releaseChecksum.
912
1013
Pure deterministic text/JSON transforms — no model, no network (org Rule 5).
1114
"""

scripts/release/zip_xcframework.sh

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
#!/bin/bash
22
set -eu
33

4-
# Deterministic zip of Iroh.xcframework — produces byte-identical output
5-
# regardless of host or wall clock. Used by:
6-
# - `cargo make prepare-release` (local) so it can compute a checksum that
7-
# will match what CI uploads, and bake it into Package.swift in the same
8-
# PR (CI never rewrites main, per the Phase 6 plan).
9-
# - `.github/workflows/release.yml` `build-and-publish-swift` so the
10-
# uploaded asset matches the just-committed Package.swift checksum.
4+
# Same-host deterministic zip of Iroh.xcframework — produces byte-identical
5+
# output across multiple runs on the same machine (catches stale caches and
6+
# non-deterministic flags). Used by:
7+
# - release_swift.yml (PR CI) — builds the zip whose SHA-256 gets baked
8+
# into Package.swift on the release branch and uploaded to a draft GH
9+
# release. That single CI build is the only build whose bytes ship.
10+
# - cargo make pre-release-check (local) — sanity check that the build is
11+
# reproducible-on-this-host; local SHAs don't need to match CI's.
1112
#
1213
# Determinism recipe:
1314
# - normalize all mtimes to 1980-01-01 (the zip-format epoch);

0 commit comments

Comments
 (0)