development
claude/smbcloud-github-app-k9641p
development
feature/apps
feature/build-release
feature/ci-docs
feature/ci-ruby-gems
feature/cicd
feature/completion
feature/dist-nuget
feature/fix-ci-sdk-pypi-rubygem
feature/fix-clippy-on-stable-channel
feature/fix-deployment-script
feature/github-email
feature/gresiq-v1
feature/project-new-structure
feature/rsync-deploy
feature/ruby-runner
feature/sdk-npm
feature/sdk-workflows
feature/smbcloud-cli-release-npm
feature/smbcloud-deploy
feature/wasm
main
release/v0.7.4
| 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 |