@smbcloud / cli / commits / 92470d9

Add SDK PyPI and RubyGem release workflows and update CI targets

- Add .github/workflows/release-sdk-pypi.yml for publishing Python SDK to PyPI - Add .github/workflows/release-sdk-gem.yml for publishing Ruby gem to RubyGems - Update .gitignore for .NET/NuGet build outputs - Clarify Windows arm64 handling in SKILL.md and PyPI workflow - Bump github-script action to v8 in release-github.yml

keypair34 committed May 3, 2026 at 18:52 UTC 92470d9c0e46bf6d0290cb63e7416fb8109fd02c
7 files changed +495 -16
.agents/skills/ci-cd/SKILL.md
+12 -9
@@ -218,16 +218,16 @@ The PyPI workflow is exempt because maturin is already pointed at `crates/cli/Ca
218
219 ### Current runners and targets
220
221 -| Platform | Runner | Target | Notes |
222 -| ------------- | ------------------ | --------------------------- | ------------------------- |
223 -| Linux x86_64 | `ubuntu-latest` | `x86_64-unknown-linux-gnu` | Native |
224 -| Linux arm64 | `ubuntu-24.04-arm` | `aarch64-unknown-linux-gnu` | Native — do not use QEMU |
225 -| Windows x64 | `windows-2022` | `x86_64-pc-windows-msvc` | Native |
226 -| Windows arm64 | `windows-2022` | `aarch64-pc-windows-msvc` | Cross-compile on x64 host |
227 -| macOS x64 | `macos-15-intel` | `x86_64-apple-darwin` | Native Intel runner |
228 -| macOS arm64 | `macos-latest` | `aarch64-apple-darwin` | Native Apple Silicon |
221 +| Platform | Runner | Target | Notes |
222 +| ------------- | ------------------ | --------------------------- | -------------------------- |
223 +| Linux x86_64 | `ubuntu-latest` | `x86_64-unknown-linux-gnu` | Native |
224 +| Linux arm64 | `ubuntu-24.04-arm` | `aarch64-unknown-linux-gnu` | Native — do not use QEMU |
225 +| Windows x64 | `windows-2022` | `x86_64-pc-windows-msvc` | Native |
226 +| Windows arm64 | `windows-2022` | `aarch64-pc-windows-msvc` | Do not use for PyPI wheels |
227 +| macOS x64 | `macos-15-intel` | `x86_64-apple-darwin` | Native Intel runner |
228 +| macOS arm64 | `macos-latest` | `aarch64-apple-darwin` | Native Apple Silicon |
229
230 -For npm and GitHub Release, use native runners. For PyPI (maturin), the macOS x64 build uses `macos-latest` with cross-compilation because maturin handles it transparently inside its container.
230 +For npm and GitHub Release, use native runners. For PyPI (maturin), the macOS x64 build uses `macos-latest` with cross-compilation because maturin handles it transparently inside its container. Windows arm64 is the exception: GitHub's hosted Windows runners only provide x64 Python, so maturin will refuse to build an `aarch64-pc-windows-msvc` wheel when the interpreter reports `win-amd64`.
231
232 ### Do not use QEMU for arm64 Linux
233
@@ -607,6 +607,9 @@ This reads whatever version is currently in `Cargo.toml` on the checked-out bran
607 **QEMU for arm64 Linux**
608 Using `ubuntu-latest` (x86_64) to build `aarch64-unknown-linux-gnu` via QEMU inside manylinux is slow and causes container mismatch errors. Use `ubuntu-24.04-arm`.
609
610 +**Windows arm64 PyPI wheels on hosted runners**
611 +Maturin needs a matching Python interpreter for wheel generation. On GitHub-hosted Windows runners, the available interpreter reports `win-amd64`, so an `aarch64-pc-windows-msvc` wheel build gets skipped and then fails. Unless you have an arm64 Windows runner with arm64 Python installed, leave Windows arm64 out of the PyPI wheel matrix.
612 +
613 **`yum` vs `apt` in manylinux**
614 manylinux2014 is CentOS 7. Use `yum`, not `apt`. For manylinux_2_28 (AlmaLinux 8) use `dnf`.
615
.github/workflows/release-github.yml
+1 -1
@@ -175,7 +175,7 @@ jobs:
175 files: release/*
176
177 - name: Trigger Homebrew release
178 - uses: actions/github-script@v7
178 + uses: actions/github-script@v8
179 with:
180 script: |
181 await github.rest.actions.createWorkflowDispatch({
.github/workflows/release-pypi.yml
+6 -6
@@ -1,4 +1,8 @@
1 -name: PyPI Release
1 +name: PyPI CLI Release
2 +
3 +# Windows arm64 wheels are intentionally not built here. GitHub's hosted
4 +# Windows runners provide x64 Python, and maturin refuses to build an arm64
5 +# wheel when the interpreter reports win-amd64.
6
7 on:
8 push:
@@ -39,11 +43,7 @@ jobs:
43 OS: windows-2022,
44 TARGET: x86_64-pc-windows-msvc,
45 }
42 - - {
43 - NAME: win32-arm64-msvc,
44 - OS: windows-2022,
45 - TARGET: aarch64-pc-windows-msvc,
46 - }
46 +
47 - { NAME: darwin-x64, OS: macos-latest, TARGET: x86_64-apple-darwin }
48 - {
49 NAME: darwin-arm64,
.github/workflows/release-sdk-gem.yml new
+235
@@ -0,0 +1,235 @@
1 +name: SDK Ruby Gem Release
2 +
3 +# Publishes the smbcloud-auth Ruby gem (sdk/gems/auth) to RubyGems.
4 +# Builds pre-compiled native gems for the supported Unix platforms, plus a
5 +# source gem as a fallback for unsupported environments.
6 +#
7 +# Required secrets:
8 +# RUBYGEMS_API_KEY — RubyGems.org API key with push access to smbcloud-auth
9 +
10 +on:
11 + workflow_dispatch:
12 + inputs:
13 + tag:
14 + description: "Release tag (e.g. v0.3.35)"
15 + required: true
16 +
17 +permissions:
18 + contents: read
19 +
20 +env:
21 + CARGO_TERM_COLOR: always
22 +
23 +jobs:
24 + build-gems:
25 + name: Build gem (${{ matrix.name }})
26 + runs-on: ${{ matrix.runner }}
27 + strategy:
28 + fail-fast: false
29 + matrix:
30 + include:
31 + - { name: linux-x64, runner: ubuntu-latest, platform: x86_64-linux }
32 + - {
33 + name: linux-arm64,
34 + runner: ubuntu-24.04-arm,
35 + platform: aarch64-linux,
36 + }
37 + - { name: darwin-arm64, runner: macos-latest, platform: arm64-darwin }
38 + - {
39 + name: darwin-x64,
40 + runner: macos-15-intel,
41 + platform: x86_64-darwin,
42 + }
43 +
44 + steps:
45 + - name: Checkout
46 + uses: actions/checkout@v6
47 + with:
48 + ref: ${{ github.event.inputs.tag || github.ref }}
49 +
50 + - name: Set the release version
51 + shell: bash
52 + run: |
53 + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
54 + release_version="${GITHUB_REF_NAME#v}"
55 + else
56 + release_version="${{ github.event.inputs.tag }}"
57 + release_version="${release_version#v}"
58 + fi
59 + echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
60 +
61 + - name: Wait for Rust crate dependencies on crates.io
62 + shell: bash
63 + run: |
64 + for crate_name in smbcloud-auth-sdk smbcloud-model smbcloud-network; do
65 + for attempt in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
66 + if curl -fsS "https://crates.io/api/v1/crates/${crate_name}/${RELEASE_VERSION}" >/dev/null 2>&1; then
67 + echo "${crate_name} ${RELEASE_VERSION} is available on crates.io"
68 + break
69 + fi
70 +
71 + if [ "$attempt" -eq 20 ]; then
72 + echo "Timed out waiting for ${crate_name} ${RELEASE_VERSION} on crates.io" >&2
73 + exit 1
74 + fi
75 +
76 + echo "Waiting for ${crate_name} ${RELEASE_VERSION} to appear on crates.io..."
77 + sleep 15
78 + done
79 + done
80 +
81 + - name: Read Rust toolchain
82 + shell: bash
83 + run: |
84 + rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
85 + if [ -z "$rust_toolchain" ]; then
86 + echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
87 + exit 1
88 + fi
89 + echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
90 +
91 + - name: Install toolkit
92 + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
93 + with:
94 + toolchain: ${{ env.RUST_TOOLCHAIN }}
95 +
96 + - name: Setup Rust cache
97 + uses: Swatinem/rust-cache@v2
98 + with:
99 + key: sdk-gem-${{ matrix.name }}
100 + workspaces: sdk/gems/auth
101 +
102 + - name: Setup Ruby
103 + uses: ruby/setup-ruby@v1
104 + with:
105 + ruby-version: "3.4.2"
106 + bundler-cache: true
107 + working-directory: sdk/gems/auth
108 +
109 + - name: Build native gem
110 + working-directory: sdk/gems/auth
111 + shell: bash
112 + run: bundle exec rake native gem
113 +
114 + - name: Upload gem artifact
115 + uses: actions/upload-artifact@v7
116 + with:
117 + name: gem-${{ matrix.name }}
118 + path: sdk/gems/auth/pkg/*.gem
119 +
120 + build-source-gem:
121 + name: Build source gem
122 + runs-on: ubuntu-latest
123 + steps:
124 + - name: Checkout
125 + uses: actions/checkout@v6
126 + with:
127 + ref: ${{ github.event.inputs.tag || github.ref }}
128 +
129 + - name: Set the release version
130 + shell: bash
131 + run: |
132 + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
133 + release_version="${GITHUB_REF_NAME#v}"
134 + else
135 + release_version="${{ github.event.inputs.tag }}"
136 + release_version="${release_version#v}"
137 + fi
138 + echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
139 +
140 + - name: Verify gem version matches tag
141 + shell: bash
142 + run: |
143 + GEM_VERSION=$(sed -n "s/.*VERSION = '\\(.*\\)'/\\1/p" sdk/gems/auth/lib/auth/version.rb)
144 +
145 + echo "Gem version : ${GEM_VERSION}"
146 + echo "Tag version : ${RELEASE_VERSION}"
147 +
148 + if [[ "${GEM_VERSION}" != "${RELEASE_VERSION}" ]]; then
149 + echo "::error::Version mismatch — bump Auth::VERSION in sdk/gems/auth/lib/auth/version.rb before releasing."
150 + exit 1
151 + fi
152 +
153 + - name: Setup Ruby
154 + uses: ruby/setup-ruby@v1
155 + with:
156 + ruby-version: "3.4.2"
157 + bundler-cache: true
158 + working-directory: sdk/gems/auth
159 +
160 + - name: Build source gem
161 + working-directory: sdk/gems/auth
162 + run: bundle exec rake build
163 +
164 + - name: Upload source gem artifact
165 + uses: actions/upload-artifact@v7
166 + with:
167 + name: gem-source
168 + path: sdk/gems/auth/pkg/*.gem
169 +
170 + push-gems:
171 + name: Push to RubyGems
172 + needs: [build-gems, build-source-gem]
173 + runs-on: ubuntu-latest
174 +
175 + steps:
176 + - name: Checkout
177 + uses: actions/checkout@v6
178 + with:
179 + ref: ${{ github.event.inputs.tag || github.ref }}
180 +
181 + - name: Set the release version
182 + shell: bash
183 + run: |
184 + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
185 + release_version="${GITHUB_REF_NAME#v}"
186 + else
187 + release_version="${{ github.event.inputs.tag }}"
188 + release_version="${release_version#v}"
189 + fi
190 + echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
191 +
192 + - name: Check whether release already exists on RubyGems
193 + id: rubygems-check
194 + shell: bash
195 + run: |
196 + if curl -fsS \
197 + "https://rubygems.org/api/v2/rubygems/smbcloud-auth/versions/${RELEASE_VERSION}.json" \
198 + >/dev/null 2>&1; then
199 + echo "smbcloud-auth ${RELEASE_VERSION} already exists on RubyGems, skipping publish"
200 + echo "exists=true" >> "$GITHUB_OUTPUT"
201 + else
202 + echo "exists=false" >> "$GITHUB_OUTPUT"
203 + fi
204 +
205 + - name: Download all gem artifacts
206 + if: steps.rubygems-check.outputs.exists != 'true'
207 + uses: actions/download-artifact@v7
208 + with:
209 + pattern: "gem-*"
210 + path: gems
211 + merge-multiple: true
212 +
213 + - name: Setup Ruby
214 + if: steps.rubygems-check.outputs.exists != 'true'
215 + uses: ruby/setup-ruby@v1
216 + with:
217 + ruby-version: "3.4.2"
218 +
219 + - name: Configure RubyGems credentials
220 + if: steps.rubygems-check.outputs.exists != 'true'
221 + shell: bash
222 + run: |
223 + mkdir -p "${HOME}/.gem"
224 + echo ":rubygems_api_key: ${{ secrets.RUBYGEMS_API_KEY }}" > "${HOME}/.gem/credentials"
225 + chmod 0600 "${HOME}/.gem/credentials"
226 +
227 + - name: Push gems to RubyGems
228 + if: steps.rubygems-check.outputs.exists != 'true'
229 + shell: bash
230 + run: |
231 + for gem_file in gems/*.gem; do
232 + echo "Pushing $(basename "${gem_file}")…"
233 + gem push "${gem_file}" \
234 + || echo "::warning::$(basename "${gem_file}") may already exist for this version — skipping."
235 + done
.github/workflows/release-sdk-pypi.yml new
+233
@@ -0,0 +1,233 @@
1 +name: SDK PyPI Release
2 +
3 +# Publishes smbcloud-sdk-auth (sdk/python) to PyPI.
4 +# Triggered by the crates release workflow when wrapped Rust crates change,
5 +# or manually via workflow_dispatch.
6 +#
7 +# Windows arm64 wheels are intentionally not built here. GitHub's hosted
8 +# Windows runners provide x64 Python, and maturin refuses to build an arm64
9 +# wheel when the interpreter reports win-amd64.
10 +
11 +on:
12 + workflow_dispatch:
13 + inputs:
14 + tag:
15 + description: "Release tag (e.g. v0.3.35)"
16 + required: true
17 +
18 +permissions:
19 + id-token: write
20 + contents: read
21 +
22 +env:
23 + CARGO_TERM_COLOR: always
24 + CLI_CLIENT_SECRET: ${{ secrets.CLI_CLIENT_SECRET }}
25 +
26 +jobs:
27 + build-wheels:
28 + name: Build PyPI wheels
29 + runs-on: ${{ matrix.build.OS }}
30 + strategy:
31 + fail-fast: false
32 + matrix:
33 + build:
34 + - {
35 + NAME: linux-x64-glibc,
36 + OS: ubuntu-latest,
37 + TARGET: x86_64-unknown-linux-gnu,
38 + MANYLINUX: "2014",
39 + }
40 + - {
41 + NAME: linux-arm64-glibc,
42 + OS: ubuntu-24.04-arm,
43 + TARGET: aarch64-unknown-linux-gnu,
44 + MANYLINUX: "2014",
45 + }
46 + - {
47 + NAME: win32-x64-msvc,
48 + OS: windows-2022,
49 + TARGET: x86_64-pc-windows-msvc,
50 + }
51 + - {
52 + NAME: darwin-x64,
53 + OS: macos-15-intel,
54 + TARGET: x86_64-apple-darwin,
55 + }
56 + - {
57 + NAME: darwin-arm64,
58 + OS: macos-latest,
59 + TARGET: aarch64-apple-darwin,
60 + }
61 + steps:
62 + - name: Checkout
63 + uses: actions/checkout@v6
64 + with:
65 + ref: ${{ github.event.inputs.tag || github.ref }}
66 +
67 + - name: Set the release version
68 + shell: bash
69 + run: |
70 + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
71 + release_version="${GITHUB_REF_NAME#v}"
72 + else
73 + release_version="${{ github.event.inputs.tag }}"
74 + release_version="${release_version#v}"
75 + fi
76 +
77 + echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
78 +
79 + - name: Read Rust toolchain
80 + shell: bash
81 + run: |
82 + rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
83 + if [ -z "$rust_toolchain" ]; then
84 + echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
85 + exit 1
86 + fi
87 +
88 + echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
89 +
90 + - name: Install toolkit
91 + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
92 + with:
93 + toolchain: ${{ env.RUST_TOOLCHAIN }}
94 +
95 + - name: Install Rust target
96 + shell: bash
97 + run: |
98 + rustup target add ${{ matrix.build.TARGET }} --toolchain ${{ env.RUST_TOOLCHAIN }}
99 + rustup target list --installed
100 +
101 + - name: Verify Cargo.toml version matches tag
102 + shell: bash
103 + run: |
104 + CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 \
105 + | jq -r '.packages[] | select(.name == "smbcloud-auth-sdk-py") | .version')
106 +
107 + echo "Cargo.toml version : ${CARGO_VERSION}"
108 + echo "Release tag version: ${RELEASE_VERSION}"
109 +
110 + if [[ "${CARGO_VERSION}" != "${RELEASE_VERSION}" ]]; then
111 + echo "::error::Version mismatch — bump version in crates/smbcloud-auth-sdk-py/Cargo.toml before releasing."
112 + exit 1
113 + fi
114 +
115 + - name: Setup Rust cache
116 + uses: Swatinem/rust-cache@v2
117 + with:
118 + key: sdk-pypi-${{ matrix.build.NAME }}
119 +
120 + - name: Build wheel
121 + uses: PyO3/maturin-action@v1
122 + with:
123 + target: ${{ matrix.build.TARGET }}
124 + manylinux: ${{ matrix.build.MANYLINUX || 'off' }}
125 + working-directory: sdk/python
126 + args: --release --locked --compatibility pypi --out dist
127 + before-script-linux: yum install -y perl-core
128 + docker-options: -e CLI_CLIENT_SECRET
129 +
130 + - name: Upload wheel artifact
131 + uses: actions/upload-artifact@v7
132 + with:
133 + name: wheels-${{ matrix.build.NAME }}
134 + path: sdk/python/dist/*
135 +
136 + build-sdist:
137 + name: Build source distribution
138 + runs-on: ubuntu-latest
139 + steps:
140 + - name: Checkout
141 + uses: actions/checkout@v6
142 + with:
143 + ref: ${{ github.event.inputs.tag || github.ref }}
144 +
145 + - name: Set the release version
146 + shell: bash
147 + run: |
148 + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
149 + release_version="${GITHUB_REF_NAME#v}"
150 + else
151 + release_version="${{ github.event.inputs.tag }}"
152 + release_version="${release_version#v}"
153 + fi
154 +
155 + echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
156 +
157 + - name: Read Rust toolchain
158 + shell: bash
159 + run: |
160 + rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
161 + if [ -z "$rust_toolchain" ]; then
162 + echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
163 + exit 1
164 + fi
165 +
166 + echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
167 +
168 + - name: Install toolkit
169 + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
170 + with:
171 + toolchain: ${{ env.RUST_TOOLCHAIN }}
172 +
173 + - name: Build sdist
174 + uses: PyO3/maturin-action@v1
175 + with:
176 + command: sdist
177 + working-directory: sdk/python
178 + args: --out dist
179 +
180 + - name: Upload sdist artifact
181 + uses: actions/upload-artifact@v7
182 + with:
183 + name: sdist
184 + path: sdk/python/dist/*
185 +
186 + publish-pypi:
187 + name: Publish to PyPI
188 + runs-on: ubuntu-latest
189 + needs:
190 + - build-wheels
191 + - build-sdist
192 +
193 + steps:
194 + - name: Checkout
195 + uses: actions/checkout@v6
196 + with:
197 + ref: ${{ github.event.inputs.tag || github.ref }}
198 +
199 + - name: Set the release version
200 + shell: bash
201 + run: |
202 + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
203 + release_version="${GITHUB_REF_NAME#v}"
204 + else
205 + release_version="${{ github.event.inputs.tag }}"
206 + release_version="${release_version#v}"
207 + fi
208 +
209 + echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
210 +
211 + - name: Download distribution artifacts
212 + uses: actions/download-artifact@v7
213 + with:
214 + pattern: "*"
215 + path: dist
216 + merge-multiple: true
217 +
218 + - name: Check whether release already exists on PyPI
219 + id: pypi-check
220 + shell: bash
221 + run: |
222 + if curl -fsS "https://pypi.org/pypi/smbcloud-sdk-auth/${RELEASE_VERSION}/json" >/dev/null 2>&1; then
223 + echo "smbcloud-sdk-auth ${RELEASE_VERSION} already exists on PyPI, skipping publish"
224 + echo "exists=true" >> "$GITHUB_OUTPUT"
225 + else
226 + echo "exists=false" >> "$GITHUB_OUTPUT"
227 + fi
228 +
229 + - name: Publish distribution
230 + if: steps.pypi-check.outputs.exists != 'true'
231 + uses: pypa/gh-action-pypi-publish@release/v1
232 + with:
233 + skip-existing: true
.gitignore
+8
@@ -12,3 +12,11 @@
12 # Generated PyPI release artifacts
13 /pypi/dist/
14 /pypi/.venv/
15 +
16 +# Local .NET CLI state
17 +/.dotnet/
18 +
19 +# Generated .NET / NuGet build outputs
20 +/nuget/dist/
21 +/nuget/**/bin/
22 +/nuget/**/obj/
sdk/python/dist/smbcloud_auth_sdk_py-0.3.39-cp38-abi3-macosx_11_0_arm64.whl
Binary files /dev/null and b/sdk/python/dist/smbcloud_auth_sdk_py-0.3.39-cp38-abi3-macosx_11_0_arm64.whl differ