@ondeinference / onde / commits / d7b7648

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: