Add release workflow improvements
Seto Elkahfi committed
Jul 25, 2026 at 19:10 UTC
d7b764882a123062f1e0ecaf5065c68f16ff26cb
11 files changed
+224
-18
.github/scripts/build-swift-xcframework.sh
+4
-4
@@ -22,12 +22,12 @@ WATCHOS_DEPLOYMENT_TARGET="${WATCHOS_DEPLOYMENT_TARGET:-9.0}"
22
rm -rf "$FRAMEWORK_DIR" "$ZIP_PATH" "$CHECKSUM_PATH" "$VERSION_PATH"
23
mkdir -p "$DIST_DIR" "$PACKAGE_DIR/Sources/Onde" "$HEADERS_DIR"
24
25
-cargo +1.94.0 build --manifest-path uniffi-bindgen/Cargo.toml --release
25
+cargo build --manifest-path uniffi-bindgen/Cargo.toml --release
26
27
# Build staticlibs only. Avoid the cdylib link step; the XCFramework consumes .a slices.
28
-IPHONEOS_DEPLOYMENT_TARGET="$IOS_DEPLOYMENT_TARGET" cargo +1.94.0 rustc --target aarch64-apple-ios --release --lib --crate-type staticlib
29
-IPHONEOS_DEPLOYMENT_TARGET="$IOS_DEPLOYMENT_TARGET" cargo +1.94.0 rustc --target aarch64-apple-ios-sim --release --lib --crate-type staticlib
30
-MACOSX_DEPLOYMENT_TARGET="$MACOS_DEPLOYMENT_TARGET" cargo +1.94.0 rustc --target aarch64-apple-darwin --release --lib --crate-type staticlib
28
+IPHONEOS_DEPLOYMENT_TARGET="$IOS_DEPLOYMENT_TARGET" cargo rustc --target aarch64-apple-ios --release --lib --crate-type staticlib
29
+IPHONEOS_DEPLOYMENT_TARGET="$IOS_DEPLOYMENT_TARGET" cargo rustc --target aarch64-apple-ios-sim --release --lib --crate-type staticlib
30
+MACOSX_DEPLOYMENT_TARGET="$MACOS_DEPLOYMENT_TARGET" cargo rustc --target aarch64-apple-darwin --release --lib --crate-type staticlib
31
TVOS_DEPLOYMENT_TARGET="$TVOS_DEPLOYMENT_TARGET" cargo +nightly rustc -Z build-std --target aarch64-apple-tvos --release --lib --crate-type staticlib
32
TVOS_DEPLOYMENT_TARGET="$TVOS_DEPLOYMENT_TARGET" cargo +nightly rustc -Z build-std --target aarch64-apple-tvos-sim --release --lib --crate-type staticlib
33
XROS_DEPLOYMENT_TARGET="$VISIONOS_DEPLOYMENT_TARGET" cargo +nightly rustc -Z build-std --target aarch64-apple-visionos --release --lib --crate-type staticlib
.github/workflows/ci.yml
+7
@@ -18,6 +18,13 @@ permissions:
18
contents: read
19
20
jobs:
21
+ # ── Release metadata ─────────────────────────────────────────────────────
22
+ # Same gate the release workflows use, minus the tag check. Running it on
23
+ # ordinary pushes means version drift surfaces while you are still on a
24
+ # branch, instead of on release day.
25
+ preflight:
26
+ uses: ./.github/workflows/release-preflight.yml
27
+
28
# ── Dart / Flutter SDK validation ────────────────────────────────────────
29
dart-sdk:
30
name: Dart SDK (Flutter)
.github/workflows/release-preflight.yml
new
+59
@@ -0,0 +1,59 @@
1
+name: Release Preflight
2
+
3
+# Shared release gate, called by every release-sdk-*.yml before it allocates an
4
+# expensive runner, and by ci.yml on ordinary pushes.
5
+#
6
+# It replaces five partial copies of "does the tag match my version file?" —
7
+# each release workflow used to check only its own manifest, which is why
8
+# sdk/react-native/package-lock.json drifted four releases (stuck at 1.1.2 while
9
+# package.json read 1.2.1) without a single job failing.
10
+#
11
+# Runs on ubuntu in well under a minute. The 1.2.0 crates.io failure took 13m12s
12
+# of queued macOS runner to surface a manifest error that `make lint-publish`
13
+# proves instantly.
14
+
15
+on:
16
+ workflow_call:
17
+ inputs:
18
+ tag:
19
+ description: "Tag to validate against the release version. Empty skips that check."
20
+ type: string
21
+ required: false
22
+ default: ""
23
+
24
+permissions:
25
+ contents: read
26
+
27
+jobs:
28
+ preflight:
29
+ name: Validate release metadata
30
+ runs-on: ubuntu-latest
31
+
32
+ steps:
33
+ - name: Checkout
34
+ uses: actions/checkout@v6
35
+
36
+ # Every version source (5 manifests, 2 npm JSON files, 3 Cargo.lock files)
37
+ # must agree with Cargo.toml, and both CHANGELOGs must have a real section.
38
+ - name: Version sources agree
39
+ run: make verify
40
+
41
+ # cargo publish rejects git dependencies outright. Cargo.toml documents
42
+ # pointing the mistralrs deps back at the fork for local work, so this is
43
+ # checked at release time rather than on every edit.
44
+ - name: Cargo.toml is publishable to crates.io
45
+ run: make lint-publish
46
+
47
+ - name: Tag matches the release version
48
+ if: inputs.tag != ''
49
+ env:
50
+ TAG: ${{ inputs.tag }}
51
+ run: |
52
+ VERSION="$(python3 scripts/sync-release-version.py --print)"
53
+ echo "Tag: $TAG"
54
+ echo "Version: $VERSION"
55
+ if [ "$TAG" != "$VERSION" ]; then
56
+ echo "::error::Tag '$TAG' does not match the release version '$VERSION'."
57
+ echo "::error::Tags are bare semver — a 'v' prefix will not match the release trigger."
58
+ exit 1
59
+ fi
.github/workflows/release-sdk-dart.yml
+8
@@ -10,8 +10,16 @@ permissions:
10
contents: write
11
12
jobs:
13
+ # Cheap ubuntu gate: version agreement, crates.io publishability, tag match.
14
+ # Blocks the expensive job below so a metadata fault never costs runner time.
15
+ preflight:
16
+ uses: ./.github/workflows/release-preflight.yml
17
+ with:
18
+ tag: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
19
+
20
# ── Validate (every run) ──────────────────────────────────────────────────
21
validate:
22
+ needs: preflight
23
name: Validate
24
runs-on: ubuntu-latest
25
.github/workflows/release-sdk-kotlin.yml
+8
@@ -15,7 +15,15 @@ permissions:
15
contents: write
16
17
jobs:
18
+ # Cheap ubuntu gate: version agreement, crates.io publishability, tag match.
19
+ # Blocks the expensive job below so a metadata fault never costs runner time.
20
+ preflight:
21
+ uses: ./.github/workflows/release-preflight.yml
22
+ with:
23
+ tag: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
24
+
25
publish:
26
+ needs: preflight
27
name: Build and publish KMP library to Maven Central
28
runs-on: macos-26
29
.github/workflows/release-sdk-npm.yml
+8
@@ -10,8 +10,16 @@ permissions:
10
contents: read
11
12
jobs:
13
+ # Cheap ubuntu gate: version agreement, crates.io publishability, tag match.
14
+ # Blocks the expensive job below so a metadata fault never costs runner time.
15
+ preflight:
16
+ uses: ./.github/workflows/release-preflight.yml
17
+ with:
18
+ tag: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
19
+
20
# ── Validate (every run) ──────────────────────────────────────────────────
21
validate:
22
+ needs: preflight
23
name: Validate
24
runs-on: ubuntu-latest
25
.github/workflows/release-sdk-rust.yml
+36
-12
@@ -10,7 +10,15 @@ permissions:
10
contents: read
11
12
jobs:
13
+ # Cheap ubuntu gate: version agreement, crates.io publishability, tag match.
14
+ # Blocks the expensive job below so a metadata fault never costs runner time.
15
+ preflight:
16
+ uses: ./.github/workflows/release-preflight.yml
17
+ with:
18
+ tag: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
19
+
20
publish:
21
+ needs: preflight
22
name: cargo publish
23
runs-on: macos-26
24
@@ -25,26 +33,42 @@ jobs:
33
- name: Checkout
34
uses: actions/checkout@v6
35
36
+ - name: Read Rust toolchain
37
+ run: |
38
+ rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
39
+ if [ -z "$rust_toolchain" ]; then
40
+ echo "::error::Failed to read channel from rust-toolchain.toml"
41
+ exit 1
42
+ fi
43
+ echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
44
+
45
- name: Install stable Rust toolchain
46
uses: dtolnay/rust-toolchain@stable
47
+ with:
48
+ toolchain: ${{ env.RUST_TOOLCHAIN }}
49
31
- - name: Validate tag matches Cargo.toml version
32
- if: startsWith(github.ref, 'refs/tags/')
50
+ # Skip a version already on crates.io instead of hard-failing. crates.io is
51
+ # immutable, so a re-run of a partially-failed release would otherwise die
52
+ # here on "already uploaded" — the npm workflow has had this guard all along.
53
+ - name: Check whether this version is already published
54
+ id: published
55
run: |
34
- TAG="${GITHUB_REF#refs/tags/}"
35
- CRATE_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/version = "//;s/"//')
36
- echo "Tag: $TAG"
37
- echo "Crate: $CRATE_VERSION"
38
- if [ "$TAG" != "$CRATE_VERSION" ]; then
39
- echo "::error::Tag '$TAG' does not match Cargo.toml version '$CRATE_VERSION'."
40
- exit 1
56
+ VERSION="$(python3 scripts/sync-release-version.py --print)"
57
+ if cargo info "onde@${VERSION}" >/dev/null 2>&1; then
58
+ echo "onde ${VERSION} is already on crates.io — skipping publish."
59
+ echo "skip=true" >> "$GITHUB_OUTPUT"
60
+ else
61
+ echo "skip=false" >> "$GITHUB_OUTPUT"
62
fi
63
64
+ # --locked forbids resolving anything other than the committed Cargo.lock,
65
+ # so the crate published is built from the same graph CI tested.
66
- name: cargo publish --dry-run
44
- run: cargo publish --dry-run
67
+ if: steps.published.outputs.skip != 'true'
68
+ run: cargo publish --dry-run --locked
69
70
- name: cargo publish
47
- if: startsWith(github.ref, 'refs/tags/')
71
+ if: startsWith(github.ref, 'refs/tags/') && steps.published.outputs.skip != 'true'
72
env:
73
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
50
- run: cargo publish
74
+ run: cargo publish --locked
.github/workflows/release-sdk-swift.yml
+21
-1
@@ -19,7 +19,15 @@ permissions:
19
contents: write
20
21
jobs:
22
+ # Cheap ubuntu gate: version agreement, crates.io publishability, tag match.
23
+ # Blocks the expensive job below so a metadata fault never costs runner time.
24
+ preflight:
25
+ uses: ./.github/workflows/release-preflight.yml
26
+ with:
27
+ tag: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
28
+
29
build-and-release:
30
+ needs: preflight
31
runs-on: macos-26
32
33
env:
@@ -33,10 +41,22 @@ jobs:
41
- name: Checkout onde
42
uses: actions/checkout@v6
43
44
+ # Read the channel from rust-toolchain.toml so it is pinned in exactly one
45
+ # place. Hardcoding it here is what let CI drift onto stable 1.92 while the
46
+ # dependency tree required 1.94, failing the 1.2.0 release.
47
+ - name: Read Rust toolchain
48
+ run: |
49
+ rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
50
+ if [ -z "$rust_toolchain" ]; then
51
+ echo "::error::Failed to read channel from rust-toolchain.toml"
52
+ exit 1
53
+ fi
54
+ echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
55
+
56
- name: Install stable Rust toolchain
57
uses: dtolnay/rust-toolchain@stable
58
with:
39
- toolchain: 1.94.0
59
+ toolchain: ${{ env.RUST_TOOLCHAIN }}
60
targets: aarch64-apple-ios,aarch64-apple-ios-sim,aarch64-apple-darwin
61
62
# tvOS targets are tier-3 — no prebuilt std libs available via rustup.
Makefile
+5
-1
@@ -1,4 +1,4 @@
1
-.PHONY: help patch minor major custom release release-commit tag sync-version relock verify
1
+.PHONY: help patch minor major custom release release-commit tag sync-version relock verify lint-publish
2
3
SYNC := python3 scripts/sync-release-version.py
4
VERSION = $(shell $(SYNC) --print)
@@ -21,6 +21,7 @@ help:
21
@echo ""
22
@echo "Utilities:"
23
@echo " make verify # check every version source agrees"
24
+ @echo " make lint-publish # check Cargo.toml has no git deps"
25
@echo " make sync-version # re-propagate the current version"
26
@echo " make relock # re-resolve lockfiles (only when deps change)"
27
@@ -88,6 +89,9 @@ sync-version:
89
verify:
90
@$(SYNC) --check
91
92
+lint-publish:
93
+ @$(SYNC) --lint-publish
94
+
95
# A version bump patches the lockfile version strings in-process, so it needs
96
# no cargo and no network. Use this only when dependencies actually changed:
97
# cargo metadata re-resolves (no compile, unlike `cargo check`), but each
rust-toolchain.toml
new
+14
@@ -0,0 +1,14 @@
1
+# Single source of truth for the stable toolchain used by every build and
2
+# workflow. Bare `cargo` in this repo resolves to this channel, so the version
3
+# must NOT be duplicated in workflow files or build scripts — CI reads it from
4
+# here (see .github/workflows/release-preflight.yml and release-sdk-*.yml).
5
+#
6
+# Why pinned rather than "stable": onde-mistralrs raises its MSRV independently
7
+# of us. The 1.2.0 Swift release failed because CI installed stable (1.92.0)
8
+# while the dependency tree required 1.94. A pin turns that into a one-line,
9
+# reviewed change instead of a release-day surprise.
10
+#
11
+# tvOS/visionOS/watchOS are tier-3 and still need `cargo +nightly -Z build-std`
12
+# with rust-src; that override is explicit in build-swift-xcframework.sh.
13
+[toolchain]
14
+channel = "1.94.0"
scripts/sync-release-version.py
+54
@@ -294,6 +294,49 @@ def check(version):
294
return problems
295
296
297
+def lint_publishable():
298
+ """Find git dependencies, which `cargo publish` rejects outright.
299
+
300
+ This is the 1.2.0 crates.io failure: the mistralrs deps were pointed at the
301
+ fork's git branch, and the release burned 13 minutes of macOS runner time to
302
+ surface an error the manifest alone proves in milliseconds. Cargo.toml
303
+ documents pointing them back at git for local fork work, so this is a
304
+ release gate rather than part of --check.
305
+
306
+ [patch.crates-io] is exempt: crates.io consumers ignore it entirely.
307
+ """
308
+ problems = []
309
+ section = None
310
+
311
+ for number, line in enumerate(read_text(CARGO_TOML).splitlines(), start=1):
312
+ stripped = line.strip()
313
+ if stripped.startswith("#"):
314
+ continue
315
+ if stripped.startswith("[") and stripped.endswith("]"):
316
+ section = stripped[1:-1]
317
+ continue
318
+ if section is None or section.startswith("patch."):
319
+ continue
320
+
321
+ is_dependency_table = (
322
+ section in ("dependencies", "build-dependencies", "dev-dependencies")
323
+ or section.startswith(
324
+ ("dependencies.", "build-dependencies.", "dev-dependencies.")
325
+ )
326
+ or section.endswith(".dependencies")
327
+ )
328
+ if not is_dependency_table:
329
+ continue
330
+
331
+ if re.search(r"\bgit\s*=", stripped):
332
+ problems.append(
333
+ "Cargo.toml:{}: git dependency in [{}] — cargo publish "
334
+ "rejects it: {}".format(number, section, stripped)
335
+ )
336
+
337
+ return problems
338
+
339
+
340
def bump(version, part):
341
major, minor, patch = (int(piece) for piece in version.split("."))
342
if part == "major":
@@ -310,6 +353,7 @@ def main():
353
group.add_argument("--set", dest="explicit", metavar="X.Y.Z")
354
group.add_argument("--check", action="store_true")
355
group.add_argument("--print", dest="print_only", action="store_true")
356
+ group.add_argument("--lint-publish", dest="lint_publish", action="store_true")
357
args = parser.parse_args()
358
359
try:
@@ -319,6 +363,16 @@ def main():
363
print(current)
364
return 0
365
366
+ if args.lint_publish:
367
+ problems = lint_publishable()
368
+ if problems:
369
+ print("Cargo.toml is not publishable to crates.io:")
370
+ for problem in problems:
371
+ print(" - {}".format(problem))
372
+ return 1
373
+ print("Cargo.toml is publishable: no git dependencies.")
374
+ return 0
375
+
376
if args.check:
377
problems = check(current)
378
if problems: