1 name: Release SDK Swift
2
3 on:
4 push:
5 tags:
6 - "[0-9]+.[0-9]+.[0-9]+"
7 workflow_dispatch:
8 inputs:
9 publish:
10 description: "Publish Swift release assets from the selected ref"
11 type: boolean
12 default: false
13 release_tag:
14 description: "Existing semver tag to publish on manual runs, for example 1.1.1"
15 type: string
16 required: false
17
18 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:
34 GRESIQ_API_KEY_DEV: ${{ secrets.GRESIQ_API_KEY_DEV }}
35 GRESIQ_API_SECRET_DEV: ${{ secrets.GRESIQ_API_SECRET_DEV }}
36 GRESIQ_API_KEY_PRODUCTION: ${{ secrets.GRESIQ_API_KEY_PRODUCTION }}
37 GRESIQ_API_SECRET_PRODUCTION: ${{ secrets.GRESIQ_API_SECRET_PRODUCTION }}
38 HF_TOKEN: ${{ secrets.HF_TOKEN }}
39
40 steps:
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:
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.
63 # The build script uses `cargo +nightly -Z build-std` which compiles std
64 # from source using the rust-src component. Do NOT list tvOS in targets:.
65 - name: Install nightly Rust toolchain for tvOS
66 uses: dtolnay/rust-toolchain@nightly
67 with:
68 components: rust-src
69
70 - name: Verify Xcode
71 run: xcodebuild -version
72
73 - name: Build Swift XCFramework
74 run: .github/scripts/build-swift-xcframework.sh
75
76 # ── Read build outputs ────────────────────────────────────────────────
77
78 - name: Read version and checksum
79 id: meta
80 run: |
81 VERSION=$(cat dist/swift/version.txt)
82 CHECKSUM=$(cat dist/swift/OndeFramework.checksum.txt)
83 echo "version=$VERSION" >> "$GITHUB_OUTPUT"
84 echo "checksum=$CHECKSUM" >> "$GITHUB_OUTPUT"
85
86 - name: Resolve release tag
87 id: release
88 env:
89 INPUT_RELEASE_TAG: ${{ inputs.release_tag }}
90 run: |
91 if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
92 RELEASE_TAG="${GITHUB_REF_NAME}"
93 else
94 RELEASE_TAG="${INPUT_RELEASE_TAG}"
95 fi
96
97 if [ -n "${RELEASE_TAG}" ]; then
98 echo "release_tag=${RELEASE_TAG}" >> "$GITHUB_OUTPUT"
99 fi
100
101 - name: Validate release tag matches Cargo.toml version
102 if: startsWith(github.ref, 'refs/tags/') || inputs.publish
103 env:
104 CARGO_VERSION: ${{ steps.meta.outputs.version }}
105 RELEASE_TAG: ${{ steps.release.outputs.release_tag }}
106 run: |
107 if [ -z "$RELEASE_TAG" ]; then
108 echo "::error::release_tag is required when publish=true on workflow_dispatch runs."
109 exit 1
110 fi
111
112 if [ "$RELEASE_TAG" != "$CARGO_VERSION" ]; then
113 echo "::error::Release tag '$RELEASE_TAG' does not match Cargo.toml version '$CARGO_VERSION'. Update Cargo.toml or publish the matching tag."
114 exit 1
115 fi
116 echo "Version check passed: $CARGO_VERSION"
117
118 # ── Upload CI artifacts (manual runs only) ───────────────────────────
119
120 - name: Upload XCFramework artifacts
121 if: github.event_name == 'workflow_dispatch'
122 uses: actions/upload-artifact@v7
123 with:
124 name: onde-swift-xcframework
125 path: |
126 dist/swift/OndeFramework.xcframework.zip
127 dist/swift/OndeFramework.checksum.txt
128 dist/swift/version.txt
129 dist/swift/Package/Sources/Onde/onde.swift
130 if-no-files-found: error
131 retention-days: 14
132
133 # ── Create GitHub Release and attach assets ──────────────────────────
134
135 - name: Create GitHub Release
136 if: startsWith(github.ref, 'refs/tags/') || inputs.publish
137 uses: softprops/action-gh-release@v3
138 with:
139 tag_name: ${{ steps.release.outputs.release_tag }}
140 name: ${{ steps.release.outputs.release_tag }}
141 generate_release_notes: true
142 files: |
143 dist/swift/OndeFramework.xcframework.zip
144 dist/swift/OndeFramework.checksum.txt
145
146 # ── Update onde-swift ────────────────────────────────────────────────
147
148 - name: Checkout onde-swift
149 if: startsWith(github.ref, 'refs/tags/') || inputs.publish
150 uses: actions/checkout@v6
151 with:
152 repository: ondeinference/onde-swift
153 token: ${{ secrets.ONDE_SWIFT_PAT }}
154 path: onde-swift
155
156 - name: Rewrite Package.swift release constants
157 if: startsWith(github.ref, 'refs/tags/') || inputs.publish
158 env:
159 CHECKSUM: ${{ steps.meta.outputs.checksum }}
160 RELEASE_URL: https://github.com/${{ github.repository }}/releases/download/${{ steps.release.outputs.release_tag }}/OndeFramework.xcframework.zip
161 run: |
162 python3 - <<'PY'
163 import os, re, pathlib
164
165 pkg = pathlib.Path("onde-swift/Package.swift")
166 src = pkg.read_text()
167
168 url = os.environ["RELEASE_URL"]
169 checksum = os.environ["CHECKSUM"].strip()
170
171 url_pattern = re.compile(
172 r'(let releaseFrameworkURL =\s*\n\s*")([^"]+)(")',
173 re.MULTILINE,
174 )
175 checksum_pattern = re.compile(
176 r'(let releaseFrameworkChecksum =\s*\n\s*")([^"]+)(")',
177 re.MULTILINE,
178 )
179
180 def replace_constant(pattern, value, text, label):
181 updated_text, count = pattern.subn(
182 lambda match: f"{match.group(1)}{value}{match.group(3)}",
183 text,
184 count=1,
185 )
186 if count != 1:
187 raise RuntimeError(f"{label} pattern did not match - check Package.swift format")
188 return updated_text
189
190 updated = replace_constant(url_pattern, url, src, "releaseFrameworkURL")
191 updated = replace_constant(
192 checksum_pattern,
193 checksum,
194 updated,
195 "releaseFrameworkChecksum",
196 )
197
198 pkg.write_text(updated)
199 print("Package.swift updated:")
200 print(updated)
201 PY
202
203 - name: Copy generated onde.swift into onde-swift
204 if: startsWith(github.ref, 'refs/tags/') || inputs.publish
205 run: |
206 cp dist/swift/Package/Sources/Onde/onde.swift \
207 onde-swift/Sources/Onde/onde.swift
208
209 - name: Commit and tag onde-swift
210 if: startsWith(github.ref, 'refs/tags/') || inputs.publish
211 env:
212 VERSION: ${{ steps.release.outputs.release_tag }}
213 run: |
214 cd onde-swift
215 git config user.name "github-actions[bot]"
216 git config user.email "github-actions[bot]@users.noreply.github.com"
217 git add Package.swift Sources/Onde/onde.swift
218
219 if git diff --cached --quiet; then
220 echo "No changes to commit — Package.swift and onde.swift are already up to date."
221 else
222 git commit -m "Release $VERSION"
223 fi
224
225 # Push the commit to main first so the tag points to the latest HEAD
226 git push origin HEAD:main
227
228 # Create an annotated tag only if it doesn't already exist on the remote.
229 # Never force-overwrite — SPM caches the resolved commit SHA per tag,
230 # so moving a tag silently breaks every consumer's Package.resolved.
231 if git ls-remote --tags origin "refs/tags/$VERSION" | grep -q "refs/tags/$VERSION"; then
232 echo "::warning::Tag $VERSION already exists on onde-swift — skipping tag push."
233 echo "To re-release this version, delete the remote tag first:"
234 echo " git push origin :refs/tags/$VERSION"
235 else
236 git tag -a "$VERSION" -m "Release $VERSION"
237 git push origin "refs/tags/$VERSION"
238 fi