From 05ea6c1f694ca0e6e27b58383a31413ddc61e97b Mon Sep 17 00:00:00 2001 From: Nik Samokhvalov Date: Fri, 10 Jul 2026 03:24:22 -0700 Subject: [PATCH 1/6] release(ruby): validate database suite and version availability --- .github/workflows/release-ruby.yml | 74 +++++++++++++++- clients/ruby/RELEASE.md | 38 +++++--- clients/ruby/script/assert_no_test_skips.rb | 34 ++++++++ clients/ruby/script/check_rubygems_version.rb | 57 ++++++++++++ .../fixtures/rubygems_versions/available.json | 14 +++ .../fixtures/rubygems_versions/invalid.json | 1 + .../rubygems_versions/invalid_schema.json | 3 + .../rubygems_versions/missing_number.json | 6 ++ .../fixtures/rubygems_versions/published.json | 8 ++ clients/ruby/test/test_release_checks.rb | 87 +++++++++++++++++++ 10 files changed, 307 insertions(+), 15 deletions(-) create mode 100644 clients/ruby/script/assert_no_test_skips.rb create mode 100644 clients/ruby/script/check_rubygems_version.rb create mode 100644 clients/ruby/test/fixtures/rubygems_versions/available.json create mode 100644 clients/ruby/test/fixtures/rubygems_versions/invalid.json create mode 100644 clients/ruby/test/fixtures/rubygems_versions/invalid_schema.json create mode 100644 clients/ruby/test/fixtures/rubygems_versions/missing_number.json create mode 100644 clients/ruby/test/fixtures/rubygems_versions/published.json create mode 100644 clients/ruby/test/test_release_checks.rb diff --git a/.github/workflows/release-ruby.yml b/.github/workflows/release-ruby.yml index 0e072b7b..86ef24d6 100644 --- a/.github/workflows/release-ruby.yml +++ b/.github/workflows/release-ruby.yml @@ -24,9 +24,23 @@ jobs: build: if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + services: + postgres: + image: postgres:18@sha256:22c89fe0d0f507606260237fd55e51f6137f58b2d5bcf6152242b96d9fe8f9a4 + env: + POSTGRES_PASSWORD: pgque_test + POSTGRES_DB: pgque_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready --username=postgres --dbname=pgque_test" + --health-interval 1s + --health-timeout 5s + --health-retries 30 env: VERSION: ${{ inputs.version }} TAG_NAME: ruby/v${{ inputs.version }} + PGQUE_TEST_DSN: postgresql://postgres:pgque_test@localhost:5432/pgque_test defaults: run: working-directory: clients/ruby @@ -36,6 +50,7 @@ jobs: ref: ${{ github.sha }} fetch-depth: 0 persist-credentials: false + submodules: recursive - uses: ruby/setup-ruby@v1 with: @@ -62,10 +77,54 @@ jobs: fi fi + - name: Verify RubyGems version is available + run: | + set -Eeuo pipefail + response=$(mktemp) + trap 'rm -f "$response"' EXIT + curl --fail-with-body --silent --show-error \ + --retry 3 --retry-all-errors \ + --connect-timeout 10 --max-time 30 \ + -H 'Accept: application/json' \ + https://rubygems.org/api/v1/versions/pgque.json \ + -o "$response" + ruby script/check_rubygems_version.rb "$VERSION" "$response" + + - name: Build and install current PgQue SQL + working-directory: ${{ github.workspace }} + run: | + set -Eeuo pipefail + ready_checks=0 + for _attempt in $(seq 1 30); do + if psql "$PGQUE_TEST_DSN" -v ON_ERROR_STOP=1 -c 'select 1' >/dev/null 2>&1; then + ready_checks=$((ready_checks + 1)) + if [ "$ready_checks" -ge 2 ]; then + break + fi + else + ready_checks=0 + fi + sleep 1 + done + if [ "$ready_checks" -lt 2 ]; then + echo "PostgreSQL did not remain ready for two consecutive checks" + exit 1 + fi + + bash build/transform.sh + if ! git diff --exit-code -- devel/sql/pgque.sql devel/sql/pgque-tle.sql; then + echo "generated PgQue SQL is stale; run build/transform.sh and commit the result" + exit 1 + fi + psql "$PGQUE_TEST_DSN" -v ON_ERROR_STOP=1 -f devel/sql/pgque.sql + - name: Run tests and build gem run: | set -Eeuo pipefail - bundle exec rake test + test_log=$(mktemp) + trap 'rm -f "$test_log"' EXIT + bundle exec rake test 2>&1 | tee "$test_log" + ruby script/assert_no_test_skips.rb "$test_log" gem build pgque.gemspec ruby script/validate_release.rb "$VERSION" "$TAG_NAME" "./pgque-${VERSION}.gem" @@ -151,6 +210,19 @@ jobs: fi fi + - name: Recheck RubyGems version availability + run: | + set -Eeuo pipefail + response=$(mktemp) + trap 'rm -f "$response"' EXIT + curl --fail-with-body --silent --show-error \ + --retry 3 --retry-all-errors \ + --connect-timeout 10 --max-time 30 \ + -H 'Accept: application/json' \ + https://rubygems.org/api/v1/versions/pgque.json \ + -o "$response" + ruby script/check_rubygems_version.rb "$VERSION" "$response" + - name: Configure RubyGems trusted-publishing credentials uses: rubygems/configure-rubygems-credentials@dc5a8d8553e6ee01fc26761a49e99e733d17954a # v2.1.0 diff --git a/clients/ruby/RELEASE.md b/clients/ruby/RELEASE.md index 0417d90c..12448903 100644 --- a/clients/ruby/RELEASE.md +++ b/clients/ruby/RELEASE.md @@ -27,7 +27,7 @@ pre-release; users need `gem install pgque --pre` to receive it. RubyGems' Trusted Publishing requires the gem to **already exist** on the registry before a trusted publisher can be configured. The very -first release is therefore manual: +first release was therefore manual: ```bash cd clients/ruby @@ -36,7 +36,12 @@ gem signin # one-time, prompts for rubygems.org credentials gem push pgque-0.3.0.rc.1.gem ``` -After that, every subsequent release goes through the workflow below. +That bootstrap publish is complete: `pgque 0.3.0.rc.1` already exists on +RubyGems. Published RubyGems versions are immutable, so the workflow must never +be dispatched with `0.3.0.rc.1`, even if its namespaced Git tag does not exist. +The next attempt must first bump `Pgque::VERSION` to a new version (for example, +`0.3.0.rc.2`). Every release after the bootstrap goes through the workflow +below. ## GitHub environment prerequisite @@ -75,26 +80,31 @@ The release workflow is `.github/workflows/release-ruby.yml`. 3. Ensure the `rubygems` GitHub environment exists and is protected. 4. Ensure the gem already exists on RubyGems and Trusted Publishing is configured (bootstrap section above). -5. Run **Release Ruby client** with `dry_run=true` first. Dry runs - validate the clean tree, version and namespaced tag, run the test suite, - build and inspect the `.gem`, smoke-install it, and confirm the tag is - available. They do not create a tag, publish, or require the `rubygems` - environment approval or OIDC permissions. +5. Run **Release Ruby client** with `dry_run=true` first. Dry runs validate the + clean tree, version and namespaced tag, confirm the version is not already on + RubyGems, install the current development SQL into a pinned PostgreSQL 18 + service, and run the full database-backed Ruby suite. Any skipped test fails + the release check. They then build and inspect the `.gem`, smoke-install it, + and confirm the tag is available. Dry runs do not create a tag, publish, or + require the `rubygems` environment approval or OIDC permissions. 6. Run it with `dry_run=false`. Approve the `rubygems` environment when prompted. 7. Verify the published artifact installs in a clean environment: ```bash - gem install pgque --pre # or pin: gem install pgque -v 0.3.0.rc.1 + VERSION=0.3.0.rc.2 # replace with the version just published + gem install pgque -v "$VERSION" ruby -rpgque -e 'puts Pgque::VERSION' ``` -The workflow builds with `gem build`, smoke-installs the resulting `.gem` -against a temporary `GEM_HOME`, and uploads that exact artifact to the publish -job. The publish job revalidates the artifact, obtains short-lived credentials -through RubyGems Trusted Publishing / OIDC, creates the annotated tag -`ruby/v${VERSION}` at the dispatch SHA, pushes the tag, and publishes with -`gem push`. No long-lived `RUBYGEMS_API_KEY` is needed. +The workflow checks RubyGems version availability before uploading the artifact, +then builds with `gem build`, smoke-installs the resulting `.gem` against a +temporary `GEM_HOME`, and uploads that exact artifact to the publish job. The +publish job revalidates both the artifact and RubyGems availability immediately +before tagging, obtains short-lived credentials through RubyGems Trusted +Publishing / OIDC, creates the annotated tag `ruby/v${VERSION}` at the dispatch +SHA, pushes the tag, and publishes with `gem push`. No long-lived +`RUBYGEMS_API_KEY` is needed. Ruby client tags are deliberately namespaced. Never use plain `v${VERSION}` for a gem release: that namespace belongs to PgQue SQL/server releases, whose diff --git a/clients/ruby/script/assert_no_test_skips.rb b/clients/ruby/script/assert_no_test_skips.rb new file mode 100644 index 00000000..aaedf9b7 --- /dev/null +++ b/clients/ruby/script/assert_no_test_skips.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Copyright 2026 Nikolay Samokhvalov. Apache-2.0 license. + +module PgqueRelease + module TestSkipCheck + module_function + + SUMMARY_PATTERN = /^\d+ runs, \d+ assertions, \d+ failures, \d+ errors, (\d+) skips$/ + + def check!(output) + summaries = output.scan(SUMMARY_PATTERN).flatten + raise ArgumentError, "Minitest summary not found" if summaries.empty? + + skip_count = Integer(summaries.last, 10) + if skip_count.positive? + raise ArgumentError, "Ruby release test suite reported #{skip_count} skipped test(s)" + end + + true + end + end +end + +if $PROGRAM_NAME == __FILE__ + abort "usage: assert_no_test_skips.rb TEST_LOG" unless ARGV.length == 1 + + begin + PgqueRelease::TestSkipCheck.check!(File.read(ARGV.fetch(0))) + rescue ArgumentError => e + abort e.message + end + puts "Ruby release test suite reported zero skips" +end diff --git a/clients/ruby/script/check_rubygems_version.rb b/clients/ruby/script/check_rubygems_version.rb new file mode 100644 index 00000000..fc6a26c1 --- /dev/null +++ b/clients/ruby/script/check_rubygems_version.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Copyright 2026 Nikolay Samokhvalov. Apache-2.0 license. + +require "json" +require "rubygems" + +module PgqueRelease + module RubyGemsVersionCheck + module_function + + VERSION_PATTERN = /\A\d+\.\d+\.\d+(?:\.[0-9A-Za-z]+)*\z/ + + def check!(version, response) + unless VERSION_PATTERN.match?(version) && + Gem::Version.correct?(version) && Gem::Version.new(version).to_s == version + raise ArgumentError, "invalid canonical RubyGems version: #{version.inspect}" + end + + versions = JSON.parse(response) + unless versions.is_a?(Array) + raise ArgumentError, "invalid RubyGems versions response: expected a JSON array" + end + + published_versions = versions.each_with_index.map do |entry, index| + number = entry["number"] if entry.is_a?(Hash) + unless number.is_a?(String) && !number.empty? + raise ArgumentError, + "invalid RubyGems versions response: entry #{index} has no string number" + end + number + end + + if published_versions.include?(version) + raise ArgumentError, + "pgque #{version} is already published on RubyGems and immutable; choose a new version" + end + + true + rescue JSON::ParserError => e + raise ArgumentError, "invalid RubyGems versions response: #{e.message}" + end + end +end + +if $PROGRAM_NAME == __FILE__ + abort "usage: check_rubygems_version.rb VERSION [RESPONSE_PATH]" unless [1, 2].include?(ARGV.length) + + version, response_path = ARGV + response = response_path ? File.read(response_path) : $stdin.read + begin + PgqueRelease::RubyGemsVersionCheck.check!(version, response) + rescue ArgumentError => e + abort e.message + end + puts "RubyGems version available: pgque #{version}" +end diff --git a/clients/ruby/test/fixtures/rubygems_versions/available.json b/clients/ruby/test/fixtures/rubygems_versions/available.json new file mode 100644 index 00000000..4a3bd7b6 --- /dev/null +++ b/clients/ruby/test/fixtures/rubygems_versions/available.json @@ -0,0 +1,14 @@ +[ + { + "authors": "PgQue maintainers", + "built_at": "2026-07-09T00:00:00.000Z", + "number": "0.3.0.rc.1", + "prerelease": true + }, + { + "authors": "PgQue maintainers", + "built_at": "2026-07-10T00:00:00.000Z", + "number": "0.3.0.rc.10", + "prerelease": true + } +] diff --git a/clients/ruby/test/fixtures/rubygems_versions/invalid.json b/clients/ruby/test/fixtures/rubygems_versions/invalid.json new file mode 100644 index 00000000..afb37d9f --- /dev/null +++ b/clients/ruby/test/fixtures/rubygems_versions/invalid.json @@ -0,0 +1 @@ +{"number": diff --git a/clients/ruby/test/fixtures/rubygems_versions/invalid_schema.json b/clients/ruby/test/fixtures/rubygems_versions/invalid_schema.json new file mode 100644 index 00000000..c6fd3f3f --- /dev/null +++ b/clients/ruby/test/fixtures/rubygems_versions/invalid_schema.json @@ -0,0 +1,3 @@ +{ + "number": "0.3.0.rc.1" +} diff --git a/clients/ruby/test/fixtures/rubygems_versions/missing_number.json b/clients/ruby/test/fixtures/rubygems_versions/missing_number.json new file mode 100644 index 00000000..1ddab8a1 --- /dev/null +++ b/clients/ruby/test/fixtures/rubygems_versions/missing_number.json @@ -0,0 +1,6 @@ +[ + { + "authors": "PgQue maintainers", + "prerelease": true + } +] diff --git a/clients/ruby/test/fixtures/rubygems_versions/published.json b/clients/ruby/test/fixtures/rubygems_versions/published.json new file mode 100644 index 00000000..010f6659 --- /dev/null +++ b/clients/ruby/test/fixtures/rubygems_versions/published.json @@ -0,0 +1,8 @@ +[ + { + "authors": "PgQue maintainers", + "built_at": "2026-07-09T00:00:00.000Z", + "number": "0.3.0.rc.1", + "prerelease": true + } +] diff --git a/clients/ruby/test/test_release_checks.rb b/clients/ruby/test/test_release_checks.rb new file mode 100644 index 00000000..ff52b4b9 --- /dev/null +++ b/clients/ruby/test/test_release_checks.rb @@ -0,0 +1,87 @@ +# Copyright 2026 Nikolay Samokhvalov. Apache-2.0 license. + +require "minitest/autorun" +require "open3" +require "rbconfig" +require_relative "../script/assert_no_test_skips" +require_relative "../script/check_rubygems_version" + +class TestReleaseChecks < Minitest::Test + FIXTURE_DIR = File.expand_path("fixtures/rubygems_versions", __dir__) + RUBYGEMS_CHECKER = File.expand_path("../script/check_rubygems_version.rb", __dir__) + + def fixture(name) + File.read(File.join(FIXTURE_DIR, name)) + end + + def test_accepts_an_available_exact_version + assert PgqueRelease::RubyGemsVersionCheck.check!("0.3.0.rc.2", fixture("available.json")) + end + + def test_exact_matching_does_not_confuse_rc_1_and_rc_10 + assert PgqueRelease::RubyGemsVersionCheck.check!("0.3.0.rc.1", <<~JSON) + [{"number":"0.3.0.rc.10"}] + JSON + end + + def test_rejects_an_already_published_version + error = assert_raises(ArgumentError) do + PgqueRelease::RubyGemsVersionCheck.check!("0.3.0.rc.1", fixture("published.json")) + end + assert_match(/already published.*immutable.*new version/, error.message) + end + + def test_rejects_invalid_json + error = assert_raises(ArgumentError) do + PgqueRelease::RubyGemsVersionCheck.check!("0.3.0.rc.2", fixture("invalid.json")) + end + assert_match(/invalid RubyGems versions response/, error.message) + end + + def test_rejects_an_invalid_top_level_schema + error = assert_raises(ArgumentError) do + PgqueRelease::RubyGemsVersionCheck.check!("0.3.0.rc.2", fixture("invalid_schema.json")) + end + assert_match(/expected a JSON array/, error.message) + end + + def test_rejects_an_entry_without_a_version_number + error = assert_raises(ArgumentError) do + PgqueRelease::RubyGemsVersionCheck.check!("0.3.0.rc.2", fixture("missing_number.json")) + end + assert_match(/entry 0 has no string number/, error.message) + end + + def test_cli_rejects_a_published_version + _stdout, stderr, status = Open3.capture3( + RbConfig.ruby, + RUBYGEMS_CHECKER, + "0.3.0.rc.1", + File.join(FIXTURE_DIR, "published.json"), + ) + + refute status.success? + assert_match(/already published.*immutable/, stderr) + end + + def test_accepts_a_zero_skip_minitest_summary + output = "10 runs, 20 assertions, 0 failures, 0 errors, 0 skips\n" + assert PgqueRelease::TestSkipCheck.check!(output) + end + + def test_rejects_a_nonzero_skip_minitest_summary + error = assert_raises(ArgumentError) do + PgqueRelease::TestSkipCheck.check!( + "10 runs, 20 assertions, 0 failures, 0 errors, 2 skips\n", + ) + end + assert_match(/reported 2 skipped test/, error.message) + end + + def test_rejects_output_without_a_minitest_summary + error = assert_raises(ArgumentError) do + PgqueRelease::TestSkipCheck.check!("no tests ran\n") + end + assert_match(/summary not found/, error.message) + end +end From c13a9083c90bce0b652f2570fcd259aea68ed722 Mon Sep 17 00:00:00 2001 From: Nik Samokhvalov Date: Sat, 11 Jul 2026 07:06:35 -0700 Subject: [PATCH 2/6] chore(ruby): test branch release dry runs --- clients/ruby/test/test_release_checks.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/clients/ruby/test/test_release_checks.rb b/clients/ruby/test/test_release_checks.rb index ff52b4b9..650dc010 100644 --- a/clients/ruby/test/test_release_checks.rb +++ b/clients/ruby/test/test_release_checks.rb @@ -3,17 +3,26 @@ require "minitest/autorun" require "open3" require "rbconfig" +require "yaml" require_relative "../script/assert_no_test_skips" require_relative "../script/check_rubygems_version" class TestReleaseChecks < Minitest::Test FIXTURE_DIR = File.expand_path("fixtures/rubygems_versions", __dir__) RUBYGEMS_CHECKER = File.expand_path("../script/check_rubygems_version.rb", __dir__) + RELEASE_WORKFLOW = File.expand_path("../../../.github/workflows/release-ruby.yml", __dir__) def fixture(name) File.read(File.join(FIXTURE_DIR, name)) end + def test_branch_dry_run_executes_validation_without_enabling_publish + jobs = YAML.safe_load_file(RELEASE_WORKFLOW).fetch("jobs") + + assert_includes jobs.fetch("build").fetch("if"), "inputs.dry_run" + assert_equal "${{ !inputs.dry_run }}", jobs.fetch("publish-rubygems").fetch("if") + end + def test_accepts_an_available_exact_version assert PgqueRelease::RubyGemsVersionCheck.check!("0.3.0.rc.2", fixture("available.json")) end From 91facf6a10e2afa6566e1221945eea7ccd7f150c Mon Sep 17 00:00:00 2001 From: Nik Samokhvalov Date: Sat, 11 Jul 2026 07:06:46 -0700 Subject: [PATCH 3/6] fix(ruby): validate branch release dry runs --- .github/workflows/release-ruby.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-ruby.yml b/.github/workflows/release-ruby.yml index 86ef24d6..c4692670 100644 --- a/.github/workflows/release-ruby.yml +++ b/.github/workflows/release-ruby.yml @@ -22,7 +22,7 @@ concurrency: jobs: build: - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' || inputs.dry_run runs-on: ubuntu-latest services: postgres: From 2b459c7b6c2ee91a505f48f4d73d680dca236a48 Mon Sep 17 00:00:00 2001 From: Nik Samokhvalov Date: Sat, 11 Jul 2026 07:11:30 -0700 Subject: [PATCH 4/6] chore(ruby): bump release candidate to rc.2 --- clients/ruby/lib/pgque/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/ruby/lib/pgque/version.rb b/clients/ruby/lib/pgque/version.rb index 9ce00d79..7d369ae7 100644 --- a/clients/ruby/lib/pgque/version.rb +++ b/clients/ruby/lib/pgque/version.rb @@ -1,5 +1,5 @@ # Copyright 2026 Nikolay Samokhvalov. Apache-2.0 license. module Pgque - VERSION = "0.3.0.rc.1" + VERSION = "0.3.0.rc.2" end From 98a2e91dcaadc483ef06cc2c0fbd416b4f46d0ef Mon Sep 17 00:00:00 2001 From: Nik Samokhvalov Date: Sat, 11 Jul 2026 07:16:04 -0700 Subject: [PATCH 5/6] chore(ruby): test release workflow safety --- clients/ruby/test/test_validate_release.rb | 87 ++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 clients/ruby/test/test_validate_release.rb diff --git a/clients/ruby/test/test_validate_release.rb b/clients/ruby/test/test_validate_release.rb new file mode 100644 index 00000000..19d13546 --- /dev/null +++ b/clients/ruby/test/test_validate_release.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# Copyright 2026 Nikolay Samokhvalov. Apache-2.0 license. + +require "minitest/autorun" +require "open3" +require "rbconfig" +require "yaml" +require_relative "../lib/pgque/version" + +class TestValidateRelease < Minitest::Test + SCRIPT = File.expand_path("../script/validate_release.rb", __dir__) + WORKFLOW = File.expand_path("../../../.github/workflows/release-ruby.yml", __dir__) + + def run_validator(*arguments) + Open3.capture3(RbConfig.ruby, SCRIPT, *arguments) + end + + def workflow_steps + YAML.safe_load_file(WORKFLOW).fetch("jobs").values.flat_map do |job| + job.fetch("steps") + end + end + + def test_release_workflow_pins_privileged_gem_tooling + install = workflow_steps.find { |step| step["name"] == "Install release tooling" } + await_step = workflow_steps.find { |step| step["name"] == "Wait for release to propagate" } + + assert_equal "gem install rubygems-await --version 0.5.4 --no-document", install&.fetch("run") + assert_equal "gem exec --version 0.5.4 rubygems-await \"./pgque-${VERSION}.gem\"", await_step.fetch("run") + end + + def test_release_workflow_shell_contract + scripts = workflow_steps.filter_map { |step| step["run"] } + scripts.grep(/set -Eeuo pipefail/).each do |script| + assert_match(/\Aset -Eeuo pipefail\nIFS=\$'\\n\\t'\n/, script) + end + + combined = scripts.join("\n") + refute_includes combined, "$(seq " + if combined.match?(/\bpsql\b/) + assert_includes combined, "PAGER=cat" + assert_includes combined, "psql --no-psqlrc --set=ON_ERROR_STOP=1" + refute_match(/\bpsql\b[^\n]*\s-v(?:\s|$)/, combined) + end + end + + def test_accepts_matching_version_and_namespaced_tag + output, error, status = run_validator( + Pgque::VERSION, "ruby/v#{Pgque::VERSION}" + ) + + assert status.success?, error + assert_includes output, "release candidate verified: pgque #{Pgque::VERSION}" + end + + def test_rejects_unnamespaced_tag + _output, error, status = run_validator(Pgque::VERSION, "v#{Pgque::VERSION}") + + refute status.success? + assert_includes error, "tag must be \"ruby/v#{Pgque::VERSION}\"" + end + + def test_rejects_noncanonical_version + _output, error, status = run_validator("0.3.0-rc.1", "ruby/v0.3.0-rc.1") + + refute status.success? + assert_includes error, "invalid canonical RubyGems version" + end + + def test_rejects_version_that_differs_from_library + _output, error, status = run_validator("9.9.9", "ruby/v9.9.9") + + refute status.success? + assert_includes error, "!= Pgque::VERSION" + end + + def test_rejects_missing_artifact + missing = "pgque-#{Pgque::VERSION}.gem" + _output, error, status = run_validator( + Pgque::VERSION, "ruby/v#{Pgque::VERSION}", missing + ) + + refute status.success? + assert_includes error, "gem artifact not found" + end +end From 575563dc3a0b4c2511a362b5c3ce3c35ad3a9af8 Mon Sep 17 00:00:00 2001 From: Nik Samokhvalov Date: Sat, 11 Jul 2026 07:16:24 -0700 Subject: [PATCH 6/6] fix(ruby): harden release workflow tools --- .github/workflows/release-ruby.yml | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-ruby.yml b/.github/workflows/release-ruby.yml index c4692670..ca6841c8 100644 --- a/.github/workflows/release-ruby.yml +++ b/.github/workflows/release-ruby.yml @@ -61,6 +61,7 @@ jobs: - name: Verify clean release candidate run: | set -Eeuo pipefail + IFS=$'\n\t' git diff --exit-code git diff-index --quiet --cached HEAD ruby script/validate_release.rb "$VERSION" "$TAG_NAME" @@ -80,6 +81,7 @@ jobs: - name: Verify RubyGems version is available run: | set -Eeuo pipefail + IFS=$'\n\t' response=$(mktemp) trap 'rm -f "$response"' EXIT curl --fail-with-body --silent --show-error \ @@ -94,9 +96,12 @@ jobs: working-directory: ${{ github.workspace }} run: | set -Eeuo pipefail + IFS=$'\n\t' + export PAGER=cat + psql_base=(psql --no-psqlrc --set=ON_ERROR_STOP=1 "$PGQUE_TEST_DSN") ready_checks=0 - for _attempt in $(seq 1 30); do - if psql "$PGQUE_TEST_DSN" -v ON_ERROR_STOP=1 -c 'select 1' >/dev/null 2>&1; then + for ((_attempt = 1; _attempt <= 30; _attempt++)); do + if "${psql_base[@]}" --command='select 1' >/dev/null 2>&1; then ready_checks=$((ready_checks + 1)) if [ "$ready_checks" -ge 2 ]; then break @@ -116,11 +121,12 @@ jobs: echo "generated PgQue SQL is stale; run build/transform.sh and commit the result" exit 1 fi - psql "$PGQUE_TEST_DSN" -v ON_ERROR_STOP=1 -f devel/sql/pgque.sql + "${psql_base[@]}" --file=devel/sql/pgque.sql - name: Run tests and build gem run: | set -Eeuo pipefail + IFS=$'\n\t' test_log=$(mktemp) trap 'rm -f "$test_log"' EXIT bundle exec rake test 2>&1 | tee "$test_log" @@ -131,6 +137,7 @@ jobs: - name: Verify built gem installs run: | set -Eeuo pipefail + IFS=$'\n\t' tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT gem install --install-dir "$tmp" --no-document "./pgque-${VERSION}.gem" @@ -156,6 +163,7 @@ jobs: if: inputs.dry_run run: | set -Eeuo pipefail + IFS=$'\n\t' echo "dry run complete: validated pgque ${VERSION}" echo "real publish would create ${TAG_NAME} at ${GITHUB_SHA} and push the validated gem" @@ -191,9 +199,13 @@ jobs: name: ruby-gem-${{ inputs.version }} path: clients/ruby + - name: Install release tooling + run: gem install rubygems-await --version 0.5.4 --no-document + - name: Revalidate artifact and tag run: | set -Eeuo pipefail + IFS=$'\n\t' git diff --exit-code git diff-index --quiet --cached HEAD ruby script/validate_release.rb "$VERSION" "$TAG_NAME" "./pgque-${VERSION}.gem" @@ -213,6 +225,7 @@ jobs: - name: Recheck RubyGems version availability run: | set -Eeuo pipefail + IFS=$'\n\t' response=$(mktemp) trap 'rm -f "$response"' EXIT curl --fail-with-body --silent --show-error \ @@ -229,6 +242,7 @@ jobs: - name: Create namespaced Ruby release tag run: | set -Eeuo pipefail + IFS=$'\n\t' git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git tag -a "$TAG_NAME" "$GITHUB_SHA" -m "Ruby client ${VERSION}" @@ -238,4 +252,4 @@ jobs: run: gem push "./pgque-${VERSION}.gem" - name: Wait for release to propagate - run: gem exec rubygems-await "./pgque-${VERSION}.gem" + run: gem exec --version 0.5.4 rubygems-await "./pgque-${VERSION}.gem"