diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3a9f13..1cd2158 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,4 +21,12 @@ jobs: with: ruby-version: "4.0" bundler-cache: true + - name: Verify tag matches gem version + env: + RELEASE_TAG: ${{ github.ref_name }} + run: >- + ruby -Ilib -ropencode/rails_version -e + 'expected = ENV.fetch("RELEASE_TAG").delete_prefix("v"); + abort "tag #{expected.inspect} does not match gem #{Opencode::RAILS_VERSION.inspect}" + unless Opencode::RAILS_VERSION == expected' - uses: rubygems/release-gem@052cc82692552de3ef2b81fd670e41d13cba8092 # v1.4.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index bcde687..018365d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.0.1.alpha8 - 2026-07-20 + +### Bumped + +- Runtime dependency `opencode-ruby` pinned to `= 0.0.1.alpha8`, carrying the + hardened SSE framing parser while retaining the alpha7 subscribe-before- + prompt and at-most-once reconnect contract. + +### Changed + +- Test the supported runtime surface on Ruby 3.2, 3.3, 3.4, and 4.0. +- Pin every third-party CI and release action to an exact reviewed commit and + use Ruby 4.0 for release builds. +- Fail the trusted-publishing job before release when the pushed tag does not + match `Opencode::RAILS_VERSION`. + ## 0.0.1.alpha7 - 2026-07-18 ### Fixed diff --git a/Gemfile b/Gemfile index 95baf27..fb26b46 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" gem "opencode-ruby", git: "https://github.com/ajaynomics/opencode-ruby.git", - ref: "78b6f9c9e9c7d58b699af1c3c17764acd33de798" + ref: "9277646a4bb2cf25a8384ffc140b154f49ea5766" gemspec diff --git a/README.md b/README.md index 78b7acc..9bca90b 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,25 @@ Production-grade [OpenCode](https://opencode.ai) integration for Rails apps. Lay ## Install +After both alpha8 gems are confirmed on RubyGems, pin the lockstep tuple: + ```ruby # Gemfile -gem "opencode-ruby", "= 0.0.1.alpha7" # wire client + Reply state machine +gem "opencode-ruby", "= 0.0.1.alpha8" # wire client + Reply state machine +gem "opencode-rails", "= 0.0.1.alpha8" +``` + +Until publication is verified, validate this candidate checkout against the +exact `opencode-ruby` source it was tested with: + +```ruby +# Gemfile +gem "opencode-ruby", + git: "https://github.com/ajaynomics/opencode-ruby.git", + ref: "9277646a4bb2cf25a8384ffc140b154f49ea5766" -# v0.0.1.alpha7 is a GitHub source release, not a RubyGems release. Pin the -# peeled release commit so a moved tag cannot change the code Bundler installs. gem "opencode-rails", - git: "https://github.com/ajaynomics/opencode-rails.git", - ref: "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb" + path: "../opencode-rails" ``` ```bash @@ -34,16 +44,16 @@ bundle install Runtime deps: `activerecord`, `activestorage`, `activesupport` (>= 7.1). Depends on `opencode-ruby` for the underlying HTTP/SSE primitives. -During the alpha series both gems are pinned in lockstep. Version 0.0.1.alpha7 -uses a subscribe-ready-before-prompt transport contract and reconnects an -accepted turn without posting its prompt again. +During the alpha series both gems are pinned in lockstep. Version 0.0.1.alpha8 +retains the subscribe-ready-before-prompt transport contract and reconnects an +accepted turn without posting its prompt again, while hardening SSE framing. -`opencode-rails` 0.0.1.alpha7 is available as a GitHub source release but has -not been published to RubyGems. The `release.yml` workflow is prepared for -RubyGems trusted publishing, but the gem's trusted publisher still has to be -registered for that workflow and its `release` environment. Until then, pushing -a `v*` tag does not make the gem installable from RubyGems; use the exact Git -commit above. Trusted publishing does not require a long-lived RubyGems API key. +`opencode-rails` 0.0.1.alpha8 is a release candidate and is not yet confirmed +published on RubyGems. The `release.yml` workflow is prepared for RubyGems +trusted publishing, but the gem's trusted publisher must be registered and +verified for that workflow and its `release` environment. Until the registry +result is verified, pushing a `v*` tag does not guarantee publication. Trusted +publishing does not require a long-lived RubyGems API key. ## Quickstart diff --git a/lib/opencode/rails_version.rb b/lib/opencode/rails_version.rb index a7327bd..e39d16b 100644 --- a/lib/opencode/rails_version.rb +++ b/lib/opencode/rails_version.rb @@ -11,5 +11,5 @@ # We can't reuse the same constant from a second gem, so we use a # distinct, non-namespaced constant. module Opencode - RAILS_VERSION = "0.0.1.alpha7" + RAILS_VERSION = "0.0.1.alpha8" end diff --git a/opencode-rails.gemspec b/opencode-rails.gemspec index 3bfa865..8d15e98 100644 --- a/opencode-rails.gemspec +++ b/opencode-rails.gemspec @@ -37,7 +37,7 @@ Gem::Specification.new do |spec| # this gem builds on. During alpha both gems evolve in lockstep — we pin # exactly (= not ~>) so that consumers always pick the version this gem # was tested against. - spec.add_runtime_dependency "opencode-ruby", "= 0.0.1.alpha7" + spec.add_runtime_dependency "opencode-ruby", "= 0.0.1.alpha8" # Rails sub-libraries used at runtime. Depending on these individually # (instead of the `rails` umbrella) avoids forcing host apps to load diff --git a/test/readme_test.rb b/test/readme_test.rb index a039451..a5bfea6 100644 --- a/test/readme_test.rb +++ b/test/readme_test.rb @@ -5,6 +5,7 @@ require "ripper" class ReadmeTest < Minitest::Test README_PATH = File.expand_path("../README.md", __dir__) + GEMFILE_PATH = File.expand_path("../Gemfile", __dir__) def setup @readme = File.read(README_PATH) @@ -36,4 +37,17 @@ class ReadmeTest < Minitest::Test assert Ripper.sexp(source), "README Ruby fence #{index + 1} has invalid syntax" end end + + def test_install_contract_tracks_candidate_versions_and_client_source + gemfile = File.read(GEMFILE_PATH) + client_ref = gemfile[/gem "opencode-ruby".*?ref:\s*"([0-9a-f]{40})"/m, 1] + + refute_nil client_ref, "Gemfile must pin opencode-ruby to an exact commit" + assert_includes @readme, %(gem "opencode-ruby", "= #{Opencode::VERSION}") + assert_includes @readme, %(gem "opencode-rails", "= #{Opencode::RAILS_VERSION}") + assert_includes @readme, %(ref: "#{client_ref}") + assert_includes @readme, + "`opencode-rails` #{Opencode::RAILS_VERSION} is a release candidate" + assert_includes @readme, "pushing a `v*` tag does not guarantee publication" + end end diff --git a/test/release_workflow_test.rb b/test/release_workflow_test.rb index e81664d..98c2466 100644 --- a/test/release_workflow_test.rb +++ b/test/release_workflow_test.rb @@ -33,6 +33,21 @@ class ReleaseWorkflowTest < Minitest::Test assert_equal "4.0", setup_ruby.dig("with", "ruby-version") assert_equal 1, steps.count { |step| step["uses"] == RELEASE_GEM_ACTION } + assert steps.filter_map { |step| step["uses"] }.all? { |uses| uses.match?(/@[0-9a-f]{40}\z/) } refute steps.any? { |step| step.fetch("run", "").match?(/\bgem\s+push\b/) } end + + def test_release_tag_must_match_the_gem_version_before_publish + steps = push_job.fetch("steps") + preflight_index = steps.index { |step| step["name"] == "Verify tag matches gem version" } + publish_index = steps.index { |step| step["uses"] == RELEASE_GEM_ACTION } + + refute_nil preflight_index + refute_nil publish_index + assert_operator preflight_index, :<, publish_index + + preflight = steps.fetch(preflight_index) + assert_equal "${{ github.ref_name }}", preflight.dig("env", "RELEASE_TAG") + assert_includes preflight.fetch("run"), "unless Opencode::RAILS_VERSION == expected" + end end diff --git a/test/version_test.rb b/test/version_test.rb new file mode 100644 index 0000000..9026371 --- /dev/null +++ b/test/version_test.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "test_helper" + +class Opencode::VersionTest < Minitest::Test + GEMSPEC_PATH = File.expand_path("../opencode-rails.gemspec", __dir__) + + def test_alpha_versions_and_runtime_dependency_stay_in_lockstep + specification = Gem::Specification.load(GEMSPEC_PATH) + dependency = specification.runtime_dependencies.find { |item| item.name == "opencode-ruby" } + + refute_nil dependency + assert_equal Opencode::RAILS_VERSION, Opencode::VERSION + assert_equal "= #{Opencode::RAILS_VERSION}", dependency.requirement.to_s + end +end