1 name: Tests
2
3 # Runs on every branch push (not on tag pushes: a release tag re-tests a SHA the
4 # branch push already covered, and the release workflows handle tags).
5 # Covers:
6 # - All Rust workspace members (fmt, clippy, tests)
7 # - smbcloud-cli + xcrs clippy on Windows and macOS (cfg-gated code)
8 # - smbcloud-auth-sdk-wasm (wasm32-unknown-unknown cargo check)
9 # - sdk/python → maturin wheel build (smbcloud-sdk-auth)
10 # - sdk/npm → wasm-pack + prepare-package.mjs (@smbcloud/sdk-auth)
11 # - sdk/gems → smbcloud-auth, smbcloud-model and smbcloud-email, cross-built for
12 # x86_64-linux, aarch64-linux, arm64-darwin and x86_64-darwin
13 # against the workspace crates (oldest and newest shipped
14 # Ruby), then installed and required on native runners; the
15 # source gem is built, its file list checked and installed
16 # - nuget → .NET tool launcher build (SmbCloud.Cli)
17
18 on:
19 push:
20 branches: ["**"] # tag pushes (v*) re-test a SHA the branch push already covers
21
22 # Superseded pushes to a feature branch are cancelled. development and main are
23 # never cancelled, so every commit there keeps a result.
24 concurrency:
25 group: ${{ github.workflow }}-${{ github.ref }}
26 cancel-in-progress: ${{ github.ref != 'refs/heads/development' && github.ref != 'refs/heads/main' }}
27
28 env:
29 CLI_CLIENT_SECRET: ${{ secrets.CLI_CLIENT_SECRET }}
30 CARGO_TERM_COLOR: always
31
32 jobs:
33 # ── Rust workspace ──────────────────────────────────────────────────────────
34 test-rust:
35 name: Rust — fmt / clippy / test (workspace)
36 runs-on: ubuntu-latest
37 steps:
38 - name: Checkout
39 uses: actions/checkout@v6
40
41 - name: Read Rust toolchain
42 shell: bash
43 run: |
44 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
45 if [ -z "$rust_toolchain" ]; then
46 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
47 exit 1
48 fi
49 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
50
51 - name: Install toolkit
52 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
53 with:
54 toolchain: ${{ env.RUST_TOOLCHAIN }}
55 components: rustfmt, clippy
56
57 - name: Setup Rust cache
58 uses: Swatinem/rust-cache@v2
59 with:
60 key: test-rust
61
62 - name: Check formatting
63 run: cargo fmt --all -- --check
64
65 # smbcloud-auth-sdk-wasm targets wasm32-unknown-unknown and cannot be
66 # compiled or tested on the host. It is checked in its own job below.
67 - name: Clippy — all workspace members (host targets)
68 run: cargo clippy --workspace --exclude smbcloud-auth-sdk-wasm --tests -- -D warnings
69
70 - name: Test — all workspace members (host targets)
71 run: cargo test --workspace --exclude smbcloud-auth-sdk-wasm
72
73 # ── Cross-platform build ────────────────────────────────────────────────────
74 # A #[cfg(unix)] mismatch type-checks on Linux and only fails on Windows, so
75 # compile the packages the release workflows build on those OSes.
76 check-cross-platform:
77 name: Rust — clippy (${{ matrix.os }})
78 runs-on: ${{ matrix.os }}
79 strategy:
80 fail-fast: false
81 matrix:
82 os: [windows-latest, macos-latest]
83 steps:
84 - name: Checkout
85 uses: actions/checkout@v6
86
87 - name: Read Rust toolchain
88 shell: bash
89 run: |
90 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
91 if [ -z "$rust_toolchain" ]; then
92 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
93 exit 1
94 fi
95 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
96
97 - name: Install toolkit
98 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
99 with:
100 toolchain: ${{ env.RUST_TOOLCHAIN }}
101 components: clippy
102
103 - name: Setup Rust cache
104 uses: Swatinem/rust-cache@v2
105 with:
106 key: check-cross-platform-${{ matrix.os }}
107
108 - name: Clippy — smbcloud-cli and xcrs
109 run: cargo clippy --locked --package smbcloud-cli --package xcrs --tests -- -D warnings
110
111 # ── WASM crate ──────────────────────────────────────────────────────────────
112 check-wasm:
113 name: Rust — smbcloud-auth-sdk-wasm (wasm32-unknown-unknown)
114 runs-on: ubuntu-latest
115 steps:
116 - name: Checkout
117 uses: actions/checkout@v6
118
119 - name: Read Rust toolchain
120 shell: bash
121 run: |
122 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
123 if [ -z "$rust_toolchain" ]; then
124 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
125 exit 1
126 fi
127 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
128
129 - name: Install toolkit with wasm32 target
130 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
131 with:
132 toolchain: ${{ env.RUST_TOOLCHAIN }}
133 targets: wasm32-unknown-unknown
134
135 - name: Setup Rust cache
136 uses: Swatinem/rust-cache@v2
137 with:
138 key: check-wasm
139
140 - name: Check — smbcloud-auth-sdk-wasm
141 run: cargo check --package smbcloud-auth-sdk-wasm --target wasm32-unknown-unknown
142
143 # ── Python SDK ──────────────────────────────────────────────────────────────
144 check-python-sdk:
145 name: SDK — Python wheel (smbcloud-sdk-auth)
146 runs-on: ubuntu-latest
147 steps:
148 - name: Checkout
149 uses: actions/checkout@v6
150
151 - name: Read Rust toolchain
152 shell: bash
153 run: |
154 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
155 if [ -z "$rust_toolchain" ]; then
156 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
157 exit 1
158 fi
159 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
160
161 - name: Install toolkit
162 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
163 with:
164 toolchain: ${{ env.RUST_TOOLCHAIN }}
165
166 - name: Setup Rust cache
167 uses: Swatinem/rust-cache@v2
168 with:
169 key: check-python-sdk
170
171 # manylinux: 'off' runs natively on the ubuntu-latest runner without
172 # spinning up a manylinux container, which is sufficient for a CI check.
173 # OpenSSL is vendored in the workspace (features = ["vendored"]) so no
174 # system libssl-dev is required.
175 - name: Build Python wheel
176 uses: PyO3/maturin-action@v1
177 with:
178 working-directory: sdk/python
179 manylinux: "off"
180 args: --release --locked --out dist
181
182 # ── npm / WASM SDK ──────────────────────────────────────────────────────────
183 check-npm-sdk:
184 name: SDK — npm / WASM (@smbcloud/sdk-auth)
185 runs-on: ubuntu-latest
186 steps:
187 - name: Checkout
188 uses: actions/checkout@v6
189
190 - name: Read Rust toolchain
191 shell: bash
192 run: |
193 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
194 if [ -z "$rust_toolchain" ]; then
195 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
196 exit 1
197 fi
198 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
199
200 - name: Install toolkit with wasm32 target
201 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
202 with:
203 toolchain: ${{ env.RUST_TOOLCHAIN }}
204 targets: wasm32-unknown-unknown
205
206 - name: Setup Rust cache
207 uses: Swatinem/rust-cache@v2
208 with:
209 key: check-npm-sdk
210
211 # wasm-pack orchestrates the wasm32 build and wasm-bindgen code generation.
212 - name: Install wasm-pack
213 run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
214
215 - name: Install Node.js
216 uses: actions/setup-node@v6
217 with:
218 node-version-file: .nvmrc
219
220 # prepare-package.mjs runs wasm-pack internally and then assembles the
221 # dist/ directory — this validates the full npm package pipeline.
222 - name: Build and prepare package
223 working-directory: sdk/npm/smbcloud-auth
224 run: node ./prepare-package.mjs
225
226 # ── .NET tool ───────────────────────────────────────────────────────────────
227 check-nuget-tool:
228 name: NuGet — .NET tool launcher (SmbCloud.Cli)
229 runs-on: ubuntu-latest
230 steps:
231 - name: Checkout
232 uses: actions/checkout@v6
233
234 - name: Setup .NET SDK
235 uses: actions/setup-dotnet@v4
236 with:
237 dotnet-version: 8.0.x
238
239 - name: Build .NET tool launcher
240 run: dotnet build nuget/smbcloud-cli/SmbCloud.Cli.csproj --configuration Release
241
242 # ── Ruby Gems ───────────────────────────────────────────────────────────────
243 # Skip the gem jobs when nothing they build from changed.
244 changes:
245 name: Changes — detect gem inputs
246 runs-on: ubuntu-latest
247 outputs:
248 gems: ${{ steps.filter.outputs.gems }}
249 steps:
250 - name: Checkout
251 uses: actions/checkout@v6
252
253 - name: Filter changed paths
254 id: filter
255 uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
256 with:
257 filters: |
258 gems:
259 - "sdk/gems/auth/**"
260 - "sdk/gems/model/**"
261 - "sdk/gems/email/**"
262 - "crates/**"
263 - "Cargo.toml"
264 - "Cargo.lock"
265 - "rust-toolchain.toml"
266 - ".ruby-version"
267 - ".github/workflows/ci.yml"
268
269 # Cross-builds each gem like release-sdk-gem.yml, but against the workspace
270 # crates (not the published ones) and only for the oldest and newest Ruby the
271 # release ships. Those bracket the C-API differences.
272 check-ruby-gems:
273 name: SDK — ${{ matrix.gem }} gem (${{ matrix.platform }})
274 needs: changes
275 if: needs.changes.outputs.gems == 'true'
276 runs-on: ubuntu-latest
277 strategy:
278 fail-fast: false
279 matrix:
280 gem: [auth, model, email]
281 platform: [x86_64-linux, aarch64-linux, arm64-darwin, x86_64-darwin]
282 steps:
283 - name: Checkout
284 uses: actions/checkout@v6
285
286 - name: Read Rust toolchain
287 shell: bash
288 run: |
289 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
290 if [ -z "$rust_toolchain" ]; then
291 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
292 exit 1
293 fi
294 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
295
296 - name: Install toolkit
297 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
298 with:
299 toolchain: ${{ env.RUST_TOOLCHAIN }}
300
301 # CI validates the unreleased workspace version. Derive the local crate
302 # patches from the extension manifest; release builds deliberately
303 # resolve published crates after the release workflow has run. The
304 # whole checkout is mounted into the rb-sys-dock container, so the
305 # relative paths resolve there too.
306 - name: Use workspace Rust crates for the gem check
307 shell: bash
308 run: |
309 mkdir -p sdk/gems/${{ matrix.gem }}/.cargo
310 {
311 echo "[patch.crates-io]"
312 awk -F '=' '/^smbcloud-[[:alnum:]-]+[[:space:]]*=/ {
313 name = $1
314 gsub(/[[:space:]]/, "", name)
315 print name " = { path = \"../../../crates/" name "\" }"
316 }' sdk/gems/${{ matrix.gem }}/ext/${{ matrix.gem }}/Cargo.toml
317 } > sdk/gems/${{ matrix.gem }}/.cargo/config.toml
318 cargo generate-lockfile --manifest-path sdk/gems/${{ matrix.gem }}/Cargo.toml
319
320 - name: Setup Ruby
321 uses: ruby/setup-ruby@v1
322 with:
323 ruby-version: .ruby-version
324 bundler-cache: true
325 working-directory: sdk/gems/${{ matrix.gem }}
326
327 - name: Cross-compile native gem
328 id: cross-gem
329 uses: oxidize-rb/actions/cross-gem@v1
330 with:
331 platform: ${{ matrix.platform }}
332 ruby-versions: "3.1,4.0"
333 working-directory: sdk/gems/${{ matrix.gem }}
334 cache-version: ci-${{ matrix.gem }}
335
336 - name: Upload gem artifact
337 uses: actions/upload-artifact@v7
338 with:
339 name: ci-gem-${{ matrix.gem }}-${{ matrix.platform }}
340 path: ${{ steps.cross-gem.outputs.gem-path }}
341 retention-days: 1
342
343 # Install each prebuilt gem on a native runner and require it, which takes
344 # the real load path (the per-Ruby lib/<gem>/<x.y>/ bundle first). Catches
345 # load-time breakage such as a dyld-rejected install name.
346 smoke-test-ruby-gems:
347 name: Smoke — ${{ matrix.gem }} ${{ matrix.platform }} (Ruby ${{ matrix.ruby }})
348 needs: check-ruby-gems
349 runs-on: ${{ matrix.runner }}
350 strategy:
351 fail-fast: false
352 matrix:
353 gem: [auth, model, email]
354 ruby: ["3.1", "4.0"]
355 platform: [x86_64-linux, aarch64-linux, arm64-darwin]
356 include:
357 - platform: x86_64-linux
358 runner: ubuntu-latest
359 - platform: aarch64-linux
360 runner: ubuntu-24.04-arm
361 - platform: arm64-darwin
362 runner: macos-latest
363 - gem: auth
364 constant: SmbCloud::Auth
365 - gem: model
366 constant: Model
367 - gem: email
368 constant: SmbCloud::Email
369 steps:
370 - name: Setup Ruby
371 uses: ruby/setup-ruby@v1
372 with:
373 ruby-version: ${{ matrix.ruby }}
374
375 - name: Download gem artifact
376 uses: actions/download-artifact@v7
377 with:
378 name: ci-gem-${{ matrix.gem }}-${{ matrix.platform }}
379 path: gem
380
381 - name: Install the gem
382 shell: bash
383 run: gem install gem/*.gem
384
385 # No checkout in this job, so only the installed gem can satisfy the require.
386 - name: Require the gem
387 shell: bash
388 run: ruby -e 'require "${{ matrix.gem }}"; puts ${{ matrix.constant }}'
389
390 # Builds the source gem from git ls-files and installs it, compiling from
391 # source once. Catches spec.files problems (symlinks, missing Cargo files)
392 # that only surface on a user's machine.
393 check-ruby-source-gems:
394 name: SDK — ${{ matrix.gem }} source gem
395 needs: changes
396 if: needs.changes.outputs.gems == 'true'
397 runs-on: ubuntu-latest
398 strategy:
399 fail-fast: false
400 matrix:
401 gem: [auth, model, email]
402 steps:
403 - name: Checkout
404 uses: actions/checkout@v6
405
406 - name: Read Rust toolchain
407 shell: bash
408 run: |
409 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
410 if [ -z "$rust_toolchain" ]; then
411 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
412 exit 1
413 fi
414 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
415
416 - name: Install toolkit
417 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
418 with:
419 toolchain: ${{ env.RUST_TOOLCHAIN }}
420
421 - name: Use workspace Rust crates for the gem check
422 shell: bash
423 run: |
424 mkdir -p sdk/gems/${{ matrix.gem }}/.cargo
425 {
426 echo "[patch.crates-io]"
427 awk -F '=' '/^smbcloud-[[:alnum:]-]+[[:space:]]*=/ {
428 name = $1
429 gsub(/[[:space:]]/, "", name)
430 print name " = { path = \"../../../crates/" name "\" }"
431 }' sdk/gems/${{ matrix.gem }}/ext/${{ matrix.gem }}/Cargo.toml
432 } > sdk/gems/${{ matrix.gem }}/.cargo/config.toml
433
434 - name: Setup Ruby
435 uses: ruby/setup-ruby@v1
436 with:
437 ruby-version: .ruby-version
438 bundler-cache: true
439 working-directory: sdk/gems/${{ matrix.gem }}
440
441 # `gem build`, not `rake build`: the latter depends on :compile.
442 - name: Build the source gem
443 working-directory: sdk/gems/${{ matrix.gem }}
444 run: |
445 mkdir -p pkg
446 gem build ${{ matrix.gem }}.gemspec --output pkg/${{ matrix.gem }}-source.gem
447
448 - name: Check the source gem file list
449 working-directory: sdk/gems/${{ matrix.gem }}
450 shell: bash
451 run: |
452 files="$(gem spec pkg/${{ matrix.gem }}-source.gem files | sed -n 's/^- //p')"
453 echo "$files"
454 fail=0
455 require() {
456 if ! grep -Eq "$1" <<<"$files"; then
457 echo "::error::source gem is missing $2" >&2
458 fail=1
459 fi
460 }
461 require "^ext/${{ matrix.gem }}/extconf\.rb$" "ext/${{ matrix.gem }}/extconf.rb"
462 require '^ext/.*\.rs$' "ext/**/*.rs"
463 require '(^|/)Cargo\.toml$' "Cargo.toml"
464 require '(^|/)Cargo\.lock$' "Cargo.lock"
465 if grep -Eq '(^|/)(target|tmp)/' <<<"$files"; then
466 echo "::error::source gem contains target/ or tmp/ paths" >&2
467 fail=1
468 fi
469 while IFS= read -r f; do
470 if [ -L "$f" ]; then
471 echo "::error::source gem contains the symlink $f" >&2
472 fail=1
473 fi
474 done <<<"$files"
475 exit "$fail"
476
477 - name: Install the source gem
478 working-directory: sdk/gems/${{ matrix.gem }}
479 run: gem install pkg/${{ matrix.gem }}-source.gem