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