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 force_push_existing:
17 description: "Attempt the push even if this version is already on RubyGems. Credential test only — RubyGems rejects re-push, so no new release is created."
18 type: boolean
19 default: false
20 required: false
21
22 permissions:
23 contents: read
24
25 env:
26 CARGO_TERM_COLOR: always
27
28 jobs:
29 # ── Build native gems ──────────────────────────────────────────────────────
30 build-gems:
31 name: Build gem (${{ matrix.name }})
32 runs-on: ${{ matrix.runner }}
33 strategy:
34 fail-fast: false
35 matrix:
36 include:
37 - { name: linux-x64, runner: ubuntu-latest, platform: x86_64-linux }
38 - {
39 name: linux-arm64,
40 runner: ubuntu-24.04-arm,
41 platform: aarch64-linux,
42 }
43 - { name: darwin-arm64, runner: macos-latest, platform: arm64-darwin }
44 - {
45 name: darwin-x64,
46 runner: macos-15-intel,
47 platform: x86_64-darwin,
48 }
49 # Windows is excluded: rb-sys bindgen fails because RubyInstaller is
50 # MinGW-based and its rbconfig emits GCC flags that are incompatible
51 # with the MSVC clang used by bindgen. The source gem provides a
52 # fallback for Windows users (compiled at install time).
53
54 steps:
55 - name: Checkout
56 uses: actions/checkout@v6
57 with:
58 ref: ${{ github.event.inputs.tag || github.ref }}
59
60 - name: Set the release version
61 shell: bash
62 run: |
63 if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
64 release_version="${GITHUB_REF_NAME#v}"
65 else
66 release_version="${{ github.event.inputs.tag }}"
67 release_version="${release_version#v}"
68 fi
69 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
70
71 - name: Wait for Rust crate dependencies on crates.io
72 shell: bash
73 run: |
74 for crate_name in smbcloud-auth-sdk smbcloud-model smbcloud-network; do
75 for attempt in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
76 if curl -fsS -H "User-Agent: smbcloud-cli-release" "https://crates.io/api/v1/crates/${crate_name}/${RELEASE_VERSION}" >/dev/null 2>&1; then
77 echo "${crate_name} ${RELEASE_VERSION} is available on crates.io"
78 break
79 fi
80
81 if [ "$attempt" -eq 20 ]; then
82 echo "Timed out waiting for ${crate_name} ${RELEASE_VERSION} on crates.io" >&2
83 exit 1
84 fi
85
86 echo "Waiting for ${crate_name} ${RELEASE_VERSION} to appear on crates.io..."
87 sleep 15
88 done
89 done
90
91 - name: Install system dependencies (Linux)
92 if: runner.os == 'Linux'
93 run: sudo apt-get update -q && sudo apt-get install -y libclang-dev
94
95 - name: Read Rust toolchain
96 shell: bash
97 run: |
98 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
99 if [ -z "$rust_toolchain" ]; then
100 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
101 exit 1
102 fi
103 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
104
105 - name: Install toolkit
106 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
107 with:
108 toolchain: ${{ env.RUST_TOOLCHAIN }}
109
110 # Scope the Rust cache to the gem's own Cargo workspace so its registry
111 # deps are cached independently from the main workspace.
112 - name: Setup Rust cache
113 uses: Swatinem/rust-cache@v2
114 with:
115 key: sdk-gem-${{ matrix.name }}
116 workspaces: sdk/gems/auth
117
118 - name: Setup Ruby
119 uses: ruby/setup-ruby@v1
120 with:
121 ruby-version: "3.4.2"
122 bundler-cache: true
123 working-directory: sdk/gems/auth
124
125 - name: Build native gem
126 working-directory: sdk/gems/auth
127 shell: bash
128 run: bundle exec rake native gem
129
130 - name: Upload gem artifact
131 uses: actions/upload-artifact@v7
132 with:
133 name: gem-${{ matrix.name }}
134 path: sdk/gems/auth/pkg/*.gem
135
136 # ── Build source gem (fallback for unsupported platforms) ──────────────────
137 build-source-gem:
138 name: Build source gem
139 runs-on: ubuntu-latest
140 steps:
141 - name: Checkout
142 uses: actions/checkout@v6
143 with:
144 ref: ${{ github.event.inputs.tag || github.ref }}
145
146 - name: Set the release version
147 shell: bash
148 run: |
149 if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
150 release_version="${GITHUB_REF_NAME#v}"
151 else
152 release_version="${{ github.event.inputs.tag }}"
153 release_version="${release_version#v}"
154 fi
155 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
156
157 - name: Verify gem version matches tag
158 shell: bash
159 run: |
160 GEM_VERSION=$(sed -n "s/.*VERSION = '\(.*\)'/\1/p" sdk/gems/auth/lib/auth/version.rb)
161
162 echo "Gem version : ${GEM_VERSION}"
163 echo "Tag version : ${RELEASE_VERSION}"
164
165 if [[ "${GEM_VERSION}" != "${RELEASE_VERSION}" ]]; then
166 echo "::error::Version mismatch — bump Auth::VERSION in sdk/gems/auth/lib/auth/version.rb before releasing."
167 exit 1
168 fi
169
170 - name: Setup Ruby
171 uses: ruby/setup-ruby@v1
172 with:
173 ruby-version: "3.4.2"
174 bundler-cache: true
175 working-directory: sdk/gems/auth
176
177 - name: Build source gem
178 working-directory: sdk/gems/auth
179 run: bundle exec rake build
180
181 - name: Upload source gem artifact
182 uses: actions/upload-artifact@v7
183 with:
184 name: gem-source
185 path: sdk/gems/auth/pkg/*.gem
186
187 # ── Push all gems to RubyGems ──────────────────────────────────────────────
188 push-gems:
189 name: Push to RubyGems
190 needs: [build-gems, build-source-gem]
191 runs-on: ubuntu-latest
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 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
209
210 - name: Check whether release already exists on RubyGems
211 id: rubygems-check
212 shell: bash
213 run: |
214 if [[ "${{ github.event.inputs.force_push_existing }}" == "true" ]]; then
215 echo "force_push_existing is set — running the push even if the version exists (credential test)."
216 echo "exists=false" >> "$GITHUB_OUTPUT"
217 elif curl -fsS \
218 "https://rubygems.org/api/v2/rubygems/smbcloud-auth/versions/${RELEASE_VERSION}.json" \
219 >/dev/null 2>&1; then
220 echo "smbcloud-auth ${RELEASE_VERSION} already exists on RubyGems, skipping publish"
221 echo "exists=true" >> "$GITHUB_OUTPUT"
222 else
223 echo "exists=false" >> "$GITHUB_OUTPUT"
224 fi
225
226 - name: Download all gem artifacts
227 if: steps.rubygems-check.outputs.exists != 'true'
228 uses: actions/download-artifact@v7
229 with:
230 pattern: "gem-*"
231 path: gems
232 merge-multiple: true
233
234 - name: Setup Ruby
235 if: steps.rubygems-check.outputs.exists != 'true'
236 uses: ruby/setup-ruby@v1
237 with:
238 ruby-version: "3.4.2"
239
240 - name: Configure RubyGems credentials
241 if: steps.rubygems-check.outputs.exists != 'true'
242 shell: bash
243 env:
244 RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
245 run: |
246 # Fail fast and loudly if the secret is missing or empty. Without this
247 # guard, gem push gets blank credentials, returns 401, and the push
248 # step used to disguise that as "already exists" — silently shipping
249 # nothing while the run stayed green.
250 if [ -z "${RUBYGEMS_API_KEY}" ]; then
251 echo "::error::RUBYGEMS_API_KEY secret is empty or unset. Add a RubyGems API key with push scope as the RUBYGEMS_API_KEY repository secret."
252 exit 1
253 fi
254 mkdir -p "${HOME}/.gem"
255 printf ':rubygems_api_key: %s\n' "${RUBYGEMS_API_KEY}" > "${HOME}/.gem/credentials"
256 chmod 0600 "${HOME}/.gem/credentials"
257
258 # Push each .gem file individually. A version that is genuinely already
259 # published (e.g. a partial re-run) is skipped; any other failure —
260 # bad credentials, validation errors, server errors — fails the job.
261 - name: Push gems to RubyGems
262 if: steps.rubygems-check.outputs.exists != 'true'
263 shell: bash
264 run: |
265 set -uo pipefail
266 push_failed=0
267 for gem_file in gems/*.gem; do
268 name="$(basename "${gem_file}")"
269 echo "Pushing ${name}…"
270 if push_output="$(gem push "${gem_file}" 2>&1)"; then
271 echo "${push_output}"
272 echo "Pushed ${name}."
273 continue
274 fi
275 echo "${push_output}"
276 # The only acceptable failure is the version already existing on
277 # RubyGems; repushing is rejected with this specific message.
278 if echo "${push_output}" | grep -qiE "Repushing of gem versions is not allowed|has already been pushed"; then
279 echo "::notice::${name} is already published for this version — skipping."
280 continue
281 fi
282 echo "::error::Failed to push ${name}."
283 push_failed=1
284 done
285 if [ "${push_failed}" -ne 0 ]; then
286 echo "::error::One or more gems failed to push (see logs above). The release did not fully publish."
287 exit 1
288 fi