1 name: SDK Ruby Gem Release
2
3 # Publishes the smbcloud-auth (sdk/gems/auth), smbcloud-model (sdk/gems/model)
4 # and smbcloud-email (sdk/gems/email) Ruby gems to RubyGems.
5 # Native gems are cross-compiled on Linux with rb-sys-dock, so the darwin gems
6 # get the generic `arm64-darwin` / `x86_64-darwin` platforms (a gem built with a
7 # runner's Ruby gets e.g. `arm64-darwin-23`, which only matches that macOS
8 # release). Each native gem is fat: one extension per Ruby minor >= 3.1. A
9 # source gem covers every other platform and Ruby.
10 #
11 # Pull requests that touch this workflow build everything but publish nothing.
12 # Gem and crate changes are covered by ci.yml, which builds against the
13 # workspace crates.
14 #
15 # Gems are pushed with RubyGems trusted publishing (OIDC), so there is no API
16 # key secret. Each gem needs a trusted publisher on rubygems.org for repository
17 # smbcloudXYZ/smbcloud-cli, workflow release-sdk-gem.yml, with no environment.
18 # A gem that does not exist yet (smbcloud-email) needs a *pending* trusted
19 # publisher, created from the owner's profile; the first push turns it into a
20 # normal one. See https://guides.rubygems.org/trusted-publishing/
21
22 on:
23 workflow_dispatch:
24 inputs:
25 tag:
26 description: "Release version (e.g. v0.3.35)"
27 required: true
28 ref:
29 description: "Git ref to build from (defaults to the tag)"
30 required: false
31 pull_request:
32 paths:
33 - ".github/workflows/release-sdk-gem.yml"
34
35 permissions:
36 contents: read
37
38 # PR dry runs cancel their superseded runs. A release for a tag is serialized
39 # and never cancelled halfway through push-gems.
40 concurrency:
41 group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || inputs.tag }}
42 cancel-in-progress: ${{ github.event_name == 'pull_request' }}
43
44 env:
45 CARGO_TERM_COLOR: always
46
47 jobs:
48 # ── Build native gems ──────────────────────────────────────────────────────
49 build-gems:
50 name: Build ${{ matrix.gem }} gem (${{ matrix.platform }})
51 runs-on: ubuntu-latest
52 strategy:
53 fail-fast: false
54 matrix:
55 gem: [auth, model, email]
56 platform: [x86_64-linux, aarch64-linux, arm64-darwin, x86_64-darwin]
57
58 steps:
59 - name: Checkout
60 uses: actions/checkout@v6
61 with:
62 ref: ${{ inputs.ref || inputs.tag || github.ref }}
63
64 - name: Set the release version
65 shell: bash
66 run: |
67 if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
68 release_version="$(sed -n "s/.*VERSION = '\(.*\)'/\1/p" sdk/gems/${{ matrix.gem }}/lib/${{ matrix.gem }}/version.rb)"
69 elif [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
70 release_version="${GITHUB_REF_NAME#v}"
71 else
72 release_version="${{ github.event.inputs.tag }}"
73 release_version="${release_version#v}"
74 fi
75 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
76
77 - name: Wait for Rust crate dependencies on crates.io
78 if: github.event_name != 'pull_request'
79 shell: bash
80 run: |
81 for crate_name in smbcloud-auth-sdk smbcloud-email-sdk smbcloud-model smbcloud-network; do
82 for attempt in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
83 if curl -fsS -H "User-Agent: smbcloud-cli-release" "https://crates.io/api/v1/crates/${crate_name}/${RELEASE_VERSION}" >/dev/null 2>&1; then
84 echo "${crate_name} ${RELEASE_VERSION} is available on crates.io"
85 break
86 fi
87
88 if [ "$attempt" -eq 20 ]; then
89 echo "Timed out waiting for ${crate_name} ${RELEASE_VERSION} on crates.io" >&2
90 exit 1
91 fi
92
93 echo "Waiting for ${crate_name} ${RELEASE_VERSION} to appear on crates.io..."
94 sleep 15
95 done
96 done
97
98 - name: Read Rust toolchain
99 shell: bash
100 run: |
101 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
102 if [ -z "$rust_toolchain" ]; then
103 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
104 exit 1
105 fi
106 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
107
108 - name: Install toolkit
109 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
110 with:
111 toolchain: ${{ env.RUST_TOOLCHAIN }}
112
113 # The container build uses this lockfile, so resolve the published
114 # smbcloud-* crates for this release on the host first.
115 - name: Resolve the published Rust crate dependencies
116 shell: bash
117 run: cargo generate-lockfile --manifest-path sdk/gems/${{ matrix.gem }}/Cargo.toml
118
119 - name: Setup Ruby
120 uses: ruby/setup-ruby@v1
121 with:
122 ruby-version: .ruby-version
123 bundler-cache: true
124 working-directory: sdk/gems/${{ matrix.gem }}
125
126 - name: Cross-compile native gem
127 id: cross-gem
128 uses: oxidize-rb/actions/cross-gem@v1
129 with:
130 platform: ${{ matrix.platform }}
131 ruby-versions: ">= 3.1"
132 working-directory: sdk/gems/${{ matrix.gem }}
133 cache-version: ${{ matrix.gem }}
134
135 - name: Upload gem artifact
136 uses: actions/upload-artifact@v7
137 with:
138 name: gem-${{ matrix.gem }}-${{ matrix.platform }}
139 path: ${{ steps.cross-gem.outputs.gem-path }}
140
141 # ── Build source gem (fallback for unsupported platforms) ──────────────────
142 build-source-gem:
143 name: Build ${{ matrix.gem }} source gem
144 runs-on: ubuntu-latest
145 strategy:
146 fail-fast: false
147 matrix:
148 gem: [auth, model, email]
149 steps:
150 - name: Checkout
151 uses: actions/checkout@v6
152 with:
153 ref: ${{ inputs.ref || inputs.tag || github.ref }}
154
155 - name: Set the release version
156 shell: bash
157 run: |
158 if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
159 release_version="$(sed -n "s/.*VERSION = '\(.*\)'/\1/p" sdk/gems/${{ matrix.gem }}/lib/${{ matrix.gem }}/version.rb)"
160 elif [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
161 release_version="${GITHUB_REF_NAME#v}"
162 else
163 release_version="${{ github.event.inputs.tag }}"
164 release_version="${release_version#v}"
165 fi
166 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
167
168 - name: Verify gem version matches tag
169 shell: bash
170 run: |
171 GEM_VERSION=$(sed -n "s/.*VERSION = '\(.*\)'/\1/p" sdk/gems/${{ matrix.gem }}/lib/${{ matrix.gem }}/version.rb)
172
173 echo "Gem version : ${GEM_VERSION}"
174 echo "Tag version : ${RELEASE_VERSION}"
175
176 if [[ "${GEM_VERSION}" != "${RELEASE_VERSION}" ]]; then
177 echo "::error::Version mismatch — bump VERSION in sdk/gems/${{ matrix.gem }}/lib/${{ matrix.gem }}/version.rb before releasing."
178 exit 1
179 fi
180
181 # The source gem ships Cargo.lock; resolve it against the release's
182 # published crates rather than shipping the committed lock.
183 - name: Resolve the published Rust crate dependencies
184 shell: bash
185 run: cargo generate-lockfile --manifest-path sdk/gems/${{ matrix.gem }}/Cargo.toml
186
187 - name: Setup Ruby
188 uses: ruby/setup-ruby@v1
189 with:
190 ruby-version: .ruby-version
191 bundler-cache: true
192 working-directory: sdk/gems/${{ matrix.gem }}
193
194 - name: Build source gem
195 working-directory: sdk/gems/${{ matrix.gem }}
196 run: bundle exec rake build
197
198 - name: Upload source gem artifact
199 uses: actions/upload-artifact@v7
200 with:
201 name: gem-${{ matrix.gem }}-source
202 path: sdk/gems/${{ matrix.gem }}/pkg/*.gem
203
204 # ── Push all gems to RubyGems ──────────────────────────────────────────────
205 push-gems:
206 name: Push ${{ matrix.gem }} to RubyGems
207 needs: [build-gems, build-source-gem]
208 if: github.event_name == 'workflow_dispatch'
209 runs-on: ubuntu-latest
210 permissions:
211 contents: read
212 id-token: write
213 strategy:
214 fail-fast: false
215 matrix:
216 gem: [auth, model, email]
217
218 steps:
219 - name: Checkout
220 uses: actions/checkout@v6
221 with:
222 ref: ${{ inputs.ref || inputs.tag || github.ref }}
223
224 - name: Set the release version
225 shell: bash
226 run: |
227 if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
228 release_version="$(sed -n "s/.*VERSION = '\(.*\)'/\1/p" sdk/gems/${{ matrix.gem }}/lib/${{ matrix.gem }}/version.rb)"
229 elif [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
230 release_version="${GITHUB_REF_NAME#v}"
231 else
232 release_version="${{ github.event.inputs.tag }}"
233 release_version="${release_version#v}"
234 fi
235 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
236
237 - name: Download all gem artifacts
238 uses: actions/download-artifact@v7
239 with:
240 pattern: "gem-${{ matrix.gem }}-*"
241 path: gems
242 merge-multiple: true
243
244 - name: Setup Ruby
245 uses: ruby/setup-ruby@v1
246 with:
247 ruby-version: .ruby-version
248
249 # Exchanges this job's OIDC token for a short-lived RubyGems API key and
250 # exports it as GEM_HOST_API_KEY, which `gem push` reads.
251 - name: Configure RubyGems credentials
252 uses: rubygems/configure-rubygems-credentials@v2.1.0
253
254 # Push each .gem file individually. A file that is already published (a
255 # re-run, or completing a partial release) is skipped; any other failure
256 # fails the job.
257 - name: Push gems to RubyGems
258 shell: bash
259 run: |
260 for gem_file in gems/*.gem; do
261 echo "Pushing $(basename "${gem_file}")…"
262 if output=$(gem push "${gem_file}" 2>&1); then
263 echo "${output}"
264 elif grep -q "Repushing of gem versions is not allowed" <<<"${output}"; then
265 echo "::notice::$(basename "${gem_file}") is already on RubyGems, skipping."
266 else
267 echo "${output}" >&2
268 exit 1
269 fi
270 done