Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: build

# Thin PR-CI caller for the shared scientist-labs/rust-gem-release build workflow
# (pinned @0.10.0). Supersedes the hand-rolled ci.yml: it compiles the native extension
# and runs the spec suite across the supported Ruby versions, with cache + setup-ruby
# centralized in the reusable workflow.

on: pull_request

jobs:
build:
uses: scientist-labs/rust-gem-release/.github/workflows/build.yml@0.10.0
with:
test-command: bundle exec rake spec
26 changes: 0 additions & 26 deletions .github/workflows/ci.yml

This file was deleted.

62 changes: 62 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Release

# Thin caller for the shared scientist-labs/rust-gem-release reusable workflow.
# It owns only what an `on: workflow_call` workflow cannot: the tag trigger, the
# contents:write token grant, and spellkit's per-gem inputs/secrets. Every build
# leg (source + arm64-darwin + x86_64-linux + aarch64-linux), SHA pin, and gotcha
# lives once in the shared workflow (pinned @0.10.0). See red-candle's
# release.yml for the proven template.
#
# Release procedure (the tag drives it; a CI-created tag would not re-trigger):
# 1. bump lib/spellkit/version.rb
# 2. git commit -am "Release vX.Y.Z"
# 3. git tag X.Y.Z && git push origin HEAD --tags
#
# Dry-run any time without a tag:
# gh workflow run release.yml -f publish=false --ref <branch>
#
# Pre-merge full-matrix dry-run: add the `dry-run-release` label to a PR. The
# `labeled` trigger below fires the real release pipeline (publish=false) against
# the PR branch, validating every cross leg before merge. workflow_dispatch can't
# reach a branch-only workflow, so the label is the only pre-merge path.

on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+" # release: 0.1.2
- "[0-9]+.[0-9]+.[0-9]+.*" # prerelease: 0.1.2.pre1, 0.1.2.rc2 (RubyGems dot form)
- "v[0-9]+.[0-9]+.[0-9]+"
- "v[0-9]+.[0-9]+.[0-9]+.*"
workflow_dispatch:
inputs:
publish:
type: boolean
default: false
description: "push to RubyGems (default off = dry-run)"
pull_request:
types: [opened, synchronize, reopened, labeled]

# A reusable workflow can only REDUCE the inherited token scope, never elevate it —
# so the caller MUST grant contents: write here (and the repo's default workflow token
# must not be read-only) or `gh release create` 403s.
permissions:
contents: write

jobs:
release:
# Normal PRs must not run the expensive cross matrix — only a PR explicitly
# labeled `dry-run-release` does. Tag pushes and manual dispatch always run.
if: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'dry-run-release') }}
uses: scientist-labs/rust-gem-release/.github/workflows/release.yml@0.10.0
with:
# Generic (non-aliasing) adopter: gem-name == ext-name == gemspec basename ==
# "spellkit", so ext-name / gemspec / platform-gem-env all resolve to their
# defaults (spellkit, spellkit.gemspec, RUST_GEM_PLATFORM) and are omitted.
gem-name: spellkit
version-command: ruby -r./lib/spellkit/version -e 'print SpellKit::VERSION'
# On a tag push -> publish; on manual dispatch -> the input (default false = dry-run).
# A labeled-PR dry-run is never a publish (the pull_request event has no `inputs`).
publish: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish) }}
secrets:
# required: false in the shared workflow -> an empty token is a clean no-op skip.
rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
23 changes: 20 additions & 3 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
require "bundler/gem_tasks"
require "rspec/core/rake_task"
require "rake/extensiontask"

RSpec::Core::RakeTask.new(:spec)
# rspec is a dev-only dependency. The rb-sys-dock cross container installs the
# RUNTIME bundle only, so this require must not be allowed to break Rakefile load
# during the linux cross build.
begin
require "rspec/core/rake_task"
RSpec::Core::RakeTask.new(:spec)
rescue LoadError
desc "spec (rspec unavailable here)"
task(:spec) { abort "rspec is a dev dependency" }
end

# Pass the gemspec so rake-compiler defines the native:<platform> gem tasks that
# rb-sys-dock invokes for each precompiled leg (without it only compile tasks
# exist and the cross build fails with "Don't know how to build task native:...").
GEMSPEC = Gem::Specification.load("spellkit.gemspec")

Rake::ExtensionTask.new("spellkit") do |ext|
Rake::ExtensionTask.new("spellkit", GEMSPEC) do |ext|
ext.lib_dir = "lib/spellkit"
ext.ext_dir = "ext/spellkit"
ext.source_pattern = "*.{rs,toml}"
# Precompiled cross-gem build (x86_64-linux + aarch64-linux via rb-sys-dock,
# arm64-darwin natively on macOS) for the shared rust-gem-release matrix.
ext.cross_compile = true
ext.cross_platform = %w[x86_64-linux aarch64-linux arm64-darwin]
end

task default: [:compile, :spec]
11 changes: 9 additions & 2 deletions lib/spellkit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@
require "openssl"
require "fileutils"

# Load the compiled Rust extension. Precompiled (platform) gems install it into a
# Ruby-ABI-versioned subdir (lib/spellkit/<major.minor>/spellkit.{so,bundle}) so a
# single fat gem can carry a binary per Ruby version; source/dev builds place it flat
# at lib/spellkit/spellkit.{so,bundle}. Try the versioned path first, fall back to the
# flat one. Resolution goes through $LOAD_PATH (`require`, never `require_relative`)
# because RubyGems installs native extensions outside the gem's lib/ dir.
begin
require "spellkit/spellkit"
RUBY_VERSION =~ /(\d+\.\d+)/
require "spellkit/#{Regexp.last_match(1)}/spellkit"
rescue LoadError
require "spellkit.bundle"
require "spellkit/spellkit"
end

module SpellKit
Expand Down
15 changes: 14 additions & 1 deletion spellkit.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,20 @@ Gem::Specification.new do |spec|
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.extensions = ["ext/spellkit/extconf.rb"]

# Precompiled platform gems (e.g. arm64-darwin, built natively on a macOS runner)
# carry one compiled extension per Ruby ABI under lib/spellkit/<major.minor>/ and must
# NOT declare extensions, or RubyGems would try to recompile from Rust source on
# install — defeating the precompiled gem. The linux platform gems are assembled by
# rake-compiler/rb_sys (via oxidize-rb cross-gem), which clears extensions itself; this
# env gate covers the manually-assembled darwin fat gem. Unset => normal source gem.
if (platform_gem = ENV["RUST_GEM_PLATFORM"])
spec.platform = platform_gem
spec.extensions = []
spec.files += Dir["lib/spellkit/*/spellkit.bundle"] + Dir["lib/spellkit/*/spellkit.so"]
else
spec.extensions = ["ext/spellkit/extconf.rb"]
end

spec.add_dependency "rb_sys", "~> 0.9"

Expand Down
Loading