1 ---
2 name: smbcloud-cli-release
3 description: Use when building, packaging, or releasing smbCloud CLI binaries through npm, PyPI, or GitHub Actions CI/CD. Covers the Rust binary release source of truth, the npm wrapper and platform packages, the PyPI `maturin` package under `pypi/`, local publishing with `uv tool install maturin`, and fixes to `.github/workflows/release-npm.yml` or `.github/workflows/release-pypi.yml`.
4 ---
5
6 # smbcloud CLI Release
7
8 Use this skill when work touches any part of the smbCloud CLI distribution flow:
9
10 - local npm release for `smbcloud-cli`
11 - local PyPI release for `smbcloud-cli`
12 - GitHub Actions npm release automation
13 - GitHub Actions PyPI release automation
14 - Rust binary packaging into npm platform packages or PyPI wheels
15 - release failures caused by macOS cross-compilation, missing package metadata, npm 2FA, or PyPI trusted publishing setup
16
17 ## Source of truth
18
19 Use these files as the release source of truth:
20
21 - Rust crate version and binary name: `crates/cli/Cargo.toml`
22 - Workspace crate dependency versions: `Cargo.toml` under `[workspace.dependencies]`
23 - SDK WASM crate version: `crates/smbcloud-auth-sdk-wasm/Cargo.toml`
24 - SDK npm package version: `sdk/npm/smbcloud-auth/package.json`
25 - SDK npm build script: `sdk/npm/smbcloud-auth/prepare-package.mjs`
26 - npm release workflow: `.github/workflows/release-npm.yml`
27 - PyPI release workflow: `.github/workflows/release-pypi.yml`
28 - npm platform package generator: `npm/scripts/render-platform-package.cjs`
29 - npm wrapper package generator: `npm/scripts/render-main-package.cjs`
30 - npm wrapper launcher: `npm/smbcloud-cli/src/index.ts`
31 - npm wrapper committed package metadata: `npm/smbcloud-cli/package.json`
32 - npm wrapper committed lockfile: `npm/smbcloud-cli/package-lock.json`
33 - PyPI package metadata: `pypi/pyproject.toml`
34 - PyPI package README: `pypi/README.md`
35 - SDK PyPI package metadata: `sdk/python/pyproject.toml`
36 - SDK Ruby gem auth version: `sdk/gems/auth/lib/auth/version.rb`
37 - SDK Ruby gem auth native extension: `sdk/gems/auth/ext/auth/Cargo.toml`
38 - SDK Ruby gem model version: `sdk/gems/model/lib/model/version.rb`
39 - SDK Ruby gem model native extension: `sdk/gems/model/ext/model/Cargo.toml`
40
41 ## Version sync rules
42
43 The SDK npm package `@smbcloud/sdk-auth` must have its version in `sdk/npm/smbcloud-auth/package.json` match the version in `crates/smbcloud-auth-sdk-wasm/Cargo.toml` exactly. The `prepare-package.mjs` script enforces this at build time and will fail CI if they diverge.
44
45 When bumping workspace crate versions for a release, also update the version constraints in the root `Cargo.toml` under `[workspace.dependencies]` so every `smbcloud-*` path dependency points at the same release version.
46
47 When bumping workspace crate versions for a release, always update `sdk/npm/smbcloud-auth/package.json` in the same commit.
48
49 The npm wrapper package is generated, but its checked-in files still need to match the release version before tagging:
50
51 - rerender `npm/smbcloud-cli/package.json` with `node ../scripts/render-main-package.cjs ./package.json <version>`
52 - refresh `npm/smbcloud-cli/package-lock.json` with `npm install`
53 - commit both files so CI and local packaging agree on the wrapper version and optional dependency versions
54
55 The same applies to the Ruby gems in `sdk/gems/`. For each gem (`auth`, `model`):
56
57 - `lib/<gem>/version.rb` — the gem version constant
58 - `ext/<gem>/Cargo.toml` — the native extension crate version AND the `smbcloud-*` dependency version constraints (e.g. `"0.3"` → `"0.4"`)
59 - Regenerate `Cargo.lock` with `cargo generate-lockfile` and `Gemfile.lock` with `bundle lock` inside the gem directory
60
61 ## Release branch convention
62
63 Every release is prepared on a dedicated branch named **`release/v<version>`**, branched
64 off `development`. Never prepare a release directly on `development`, and never tag a
65 release on the `release/*` (or any feature) branch.
66
67 Reasoning:
68
69 - The release-prep commits (version bump, lockfiles, generated metadata) can be reviewed
70 and run through CI in isolation before they touch the mainline.
71 - The release commit history stays clean and linear on the default branch.
72 - `cargo workspaces publish --allow-branch "*"` accepts any branch, but downstream
73 workflows dispatch from the tag ref, so the tagged commit must contain all intended changes.
74 - Tagging on a feature branch leaves `development` without the release commit and makes git
75 history confusing.
76
77 Workflow:
78
79 1. Branch off an up-to-date `development`: `git checkout development && git pull && git checkout -b release/v<version>`.
80 2. Prepare the release on that branch — `make patch|minor|major|custom VERSION=<version>`
81 (see the version-sync rules above; `make` commits `Release <version>` on the branch).
82 3. Push the branch and let CI run: `git push origin release/v<version>`.
83 4. **Only when CI is green**, merge into `development` with `--no-ff`:
84 `git checkout development && git merge --no-ff release/v<version>`.
85 5. Delete the release branch (local and remote):
86 `git branch -d release/v<version> && git push origin --delete release/v<version>`.
87 6. Tag on `development`: `git tag v<version>`.
88 7. Push both: `git push origin development && git push origin v<version>` — the tag push
89 triggers `release-crate.yml` and the full publish chain.
90
91 If a tag was placed on the wrong commit (e.g. before a last-minute fix), move it:
92
93 1. Merge the fix into `development`.
94 2. Recreate the annotated tag on the correct commit: `git tag -fa v<version> -m "v<version>"`.
95 3. Force-push the tag: `git push origin v<version> --force`.
96
97 When verifying the remote tag, remember that annotated tags have two refs. `git ls-remote --tags origin refs/tags/v<version>*` should show:
98
99 - `refs/tags/v<version>` — the tag object
100 - `refs/tags/v<version>^{}` — the peeled commit
101
102 The peeled `^{}` ref is the one that must match the intended release commit.
103
104 The `release-crate.yml` orchestrator triggers on `push.tags: "v*.*.*"`, so the force-push will re-trigger the full release chain.
105
106 ## Release model
107
108 There are two public distribution channels.
109
110 ### 1. npm
111
112 npm has two package layers. Publish them in order.
113
114 Platform binary packages:
115
116 - `@smbcloud/cli-darwin-arm64`
117 - `@smbcloud/cli-darwin-x64`
118 - `@smbcloud/cli-linux-x64`
119 - `@smbcloud/cli-windows-arm64`
120 - `@smbcloud/cli-windows-x64`
121
122 Wrapper package:
123
124 - `@smbcloud/cli`
125
126 The wrapper package resolves the right platform package with `require.resolve(...)`.
127 Do not publish `@smbcloud/cli` before the required platform packages for that version exist.
128
129 ### 2. PyPI
130
131 PyPI uses one package:
132
133 - `smbcloud-cli`
134
135 It is built from `pypi/pyproject.toml` with `maturin` and `bindings = "bin"`, so the published wheel installs the native `smb` executable directly.
136
137 ## Local npm release workflow
138
139 ### Preflight
140
141 1. Confirm npm auth with `npm whoami`.
142 2. Confirm the crate version in `crates/cli/Cargo.toml`.
143 3. Use the same version string for every npm package in that release.
144 4. Rerender `npm/smbcloud-cli/package.json` and refresh `npm/smbcloud-cli/package-lock.json` before tagging if the wrapper version changed.
145
146 ### Build platform binaries
147
148 Build each target first:
149
150 - `cargo build --release --locked --target aarch64-apple-darwin`
151 - `cargo build --release --locked --target x86_64-apple-darwin`
152 - or the matching Linux or Windows target in CI
153
154 ### Generate platform package
155
156 From `npm/`:
157
158 - run `node ./scripts/render-platform-package.cjs <pkg> <version> <os> <arch>`
159 - create `<pkg>/bin`
160 - copy the built `smb` binary into `bin/`
161 - on Windows, use `smb.exe`
162
163 Example:
164
165 - `node ./scripts/render-platform-package.cjs cli-darwin-arm64 0.3.39 darwin arm64`
166
167 ### Publish order
168
169 1. Publish each platform package with `npm publish --access public`
170 2. Generate `npm/smbcloud-cli/package.json` with `render-main-package.cjs`
171 3. Run `npm install`
172 4. Run `npm run build`
173 5. Publish `@smbcloud/cli`
174
175 ## Local PyPI release workflow
176
177 ### Tooling
178
179 Use `uv` for local `maturin` installation in this repo.
180
181 Install:
182
183 - `uv tool install maturin`
184
185 Upgrade later if needed:
186
187 - `uv tool upgrade maturin`
188
189 If the shell cannot find `maturin`, use:
190
191 - `uv tool run maturin --version`
192
193 ### Preflight
194
195 1. Confirm the crate version in `crates/cli/Cargo.toml`.
196 2. Make sure `.env` exists with `CLI_CLIENT_SECRET=...` because the Rust build reads it at compile time.
197 3. If publishing from a local machine, use a PyPI API token. Trusted publishing is for CI.
198
199 ### Build and upload
200
201 From `pypi/`:
202
203 - `maturin build --release --locked --compatibility pypi --out dist`
204 - `maturin upload dist/*`
205
206 Or publish in one step:
207
208 - `maturin publish --release --locked --compatibility pypi`
209
210 For local uploads, export `MATURIN_PYPI_TOKEN` before running `maturin upload` or `maturin publish`.
211
212 ### Publishing the smb stub (one-time)
213
214 The stub lives in `pypi/smb-stub/` and is built with `hatchling`, not `maturin`. It only needs to be published once — it never changes.
215
216 ```sh
217 cd pypi/smb-stub
218 pip install hatchling build twine
219 python -m build
220 twine upload dist/*
221 ```
222
223 Or with uv:
224
225 ```sh
226 cd pypi/smb-stub
227 uv run --with build python -m build
228 uv run --with twine twine upload dist/*
229 ```
230
231 Use a PyPI API token with upload scope for `smb`. Trusted publishing is not required for a static stub.
232
233 Do not republish the stub on every CLI release. It permanently depends on the unpinned `smbcloud-cli`, so uv always resolves the latest version when the tool environment is created or upgraded.
234
235 ### Local publishing constraint
236
237 A local publish normally builds only for the current platform.
238
239 Do not claim a full PyPI release is complete from one machine unless you intentionally published only one platform or separately produced all required wheels.
240
241 ## npm 2FA
242
243 Publishing may fail with `EOTP`.
244
245 When that happens, retry the exact `npm publish` command with:
246
247 - `--otp=<code>`
248
249 Do not assume the first publish failed before the package upload step. Check the exit result or npm registry state before retrying repeatedly.
250
251 ## Trusted publishing
252
253 ### npm
254
255 Prefer npm trusted publishers for CI over `NPM_TOKEN`.
256
257 Official references:
258
259 - https://docs.npmjs.com/trusted-publishers/
260 - https://docs.npmjs.com/cli/v11/commands/npm-trust/
261
262 ### npm-side constraints
263
264 Trusted publishing can only be configured for packages that already exist on npm.
265
266 That means:
267
268 - publish a package once manually if it does not exist yet
269 - then configure trust for that package
270 - each package can have only one trusted publisher configuration at a time
271
272 For `smbcloud-cli`, configure trust per package, not only for `@smbcloud/cli`.
273
274 Example packages:
275
276 - `@smbcloud/cli`
277 - `@smbcloud/cli-darwin-arm64`
278 - later, each additional published `@smbcloud/cli-*` package
279
280 ### GitHub Actions setup
281
282 The workflow must include:
283
284 - `permissions:`
285 - `id-token: write`
286 - `contents: read`
287
288 Do not pass `NODE_AUTH_TOKEN` to `npm publish` when using trusted publishing.
289
290 The npm CLI detects the GitHub OIDC environment automatically and exchanges it for a short-lived publish credential.
291
292 ### Trusted publisher command
293
294 For this repo, the trust relationship should point at:
295
296 - repository: `smbcloudXYZ/smbcloud-cli`
297 - workflow file: `release-npm.yml`
298
299 Example:
300
301 - `npm trust github @smbcloud/cli --repo smbcloudXYZ/smbcloud-cli --file release-npm.yml --yes`
302 - `npm trust github @smbcloud/cli-darwin-arm64 --repo smbcloudXYZ/smbcloud-cli --file release-npm.yml --yes`
303
304 These commands may require npm 2FA and support `--otp=<code>`.
305
306 ### Migration guidance
307
308 Recommended order:
309
310 1. enable trusted publishing for existing packages
311 2. verify a CI publish works through OIDC
312 3. then remove or stop using `NPM_TOKEN`
313 4. after the migration is proven, tighten npm package publishing access if desired
314
315 Do not remove the manual publish path for brand-new package names, because npm trust cannot be configured before the first publish.
316
317 ### PyPI
318
319 Use trusted publishing in GitHub Actions through `pypa/gh-action-pypi-publish@release/v1`.
320 Do not use trusted publishing as the default local-machine publish path.
321
322 ## CI/CD workflow rules
323
324 ### Shared rules
325
326 For both release workflows:
327
328 - `runs-on` must use `matrix.build.OS` when the matrix uses upper-case keys
329 - derive `RELEASE_VERSION` from the git tag when `GITHUB_REF_TYPE=tag`
330 - for `workflow_dispatch`, fall back to the version in `crates/cli/Cargo.toml`
331 - read the Rust toolchain from `rust-toolchain.toml`
332 - pass that exact toolchain into `dtolnay/rust-toolchain`
333 - run `rustup target add <target> --toolchain ${{ env.RUST_TOOLCHAIN }}` before cross-target builds
334
335 ### npm workflow
336
337 The npm workflow lives in `.github/workflows/release-npm.yml`.
338
339 Important rules:
340
341 - avoid `envsubst` for package generation across runners
342 - use the Node generator scripts instead
343 - use `npm install` and `npm run build` for the wrapper package job
344
345 ### PyPI workflow
346
347 The PyPI workflow lives in `.github/workflows/release-pypi.yml`.
348
349 Important rules:
350
351 - build wheels with `PyO3/maturin-action@v1`
352 - use `working-directory: pypi`
353 - pass `target: ${{ matrix.build.TARGET }}`
354 - use `manylinux: ${{ matrix.build.MANYLINUX || 'off' }}`
355 - build the sdist in a separate job
356 - publish only after wheel and sdist artifacts are downloaded into one directory
357
358 ## Rust toolchain consistency
359
360 When cross-compiling in CI, the Rust toolchain used for `rustup target add` must match the toolchain used by `cargo build` or `maturin`.
361
362 For this repo, `rust-toolchain.toml` is the only Rust version source of truth.
363
364 If the workflow installs a target for one toolchain but builds with another, CI can fail with:
365
366 - `error[E0463]: can't find crate for core`
367
368 ## macOS cross-build constraint
369
370 On an Apple Silicon host, `x86_64-apple-darwin` may fail due to `openssl-sys` if only arm64 Homebrew OpenSSL exists under `/opt/homebrew`.
371
372 If that happens:
373
374 - do not claim the x64 package is releasable from that machine
375 - either build on an Intel Mac
376 - or use a Rosetta or x86 Homebrew toolchain with Intel OpenSSL under `/usr/local/opt/openssl@3`
377 - or publish only the arm64 platform package and explicitly avoid calling the release complete
378
379 Do not publish `@smbcloud/cli` for a version unless you accept that missing platform packages for that version can break installs on those platforms.