diff --git a/.github/workflows/publish-gem.yml b/.github/workflows/publish-gem.yml index 1224e1cb..102ff7ae 100644 --- a/.github/workflows/publish-gem.yml +++ b/.github/workflows/publish-gem.yml @@ -23,7 +23,7 @@ jobs: if: github.repository == 'appium/ruby_lib_core' runs-on: ubuntu-latest outputs: - gem_path: ${{ steps.package.outputs.gem_path }} + gem_filename: ${{ steps.artifact.outputs.gem-filename }} steps: - name: Validate tag syntax shell: bash @@ -52,14 +52,22 @@ jobs: run: | bundle exec rbs collection install bundle exec rake rubocop steep test:unit - bundle exec ruby test/release/release_test.rb - name: Validate the built gem id: package - run: ruby script/release.rb verify "$RELEASE_TAG" >> "$GITHUB_OUTPUT" + uses: appium/appium-workflows/.github/actions/ruby-release-validate@b9358466aa945baa7afa4d8f177d9fa4f4515bc0 + with: + gem-name: appium_lib_core + version-file: lib/appium_lib_core/version.rb + tag: ${{ env.RELEASE_TAG }} + - name: Record artifact filename for the publishing job + id: artifact + env: + VERIFIED_GEM_PATH: ${{ steps.package.outputs.gem-path }} + run: echo "gem-filename=$(basename "$VERIFIED_GEM_PATH")" >> "$GITHUB_OUTPUT" - uses: actions/upload-artifact@v4 with: name: release-gem - path: ${{ steps.package.outputs.gem_path }} + path: ${{ steps.package.outputs.gem-path }} if-no-files-found: error retention-days: 14 @@ -82,5 +90,5 @@ jobs: uses: rubygems/configure-rubygems-credentials@daab0479595bcc124493903b3df17273d86e073f - name: Publish the verified gem env: - GEM_PATH_TO_PUBLISH: ${{ needs.verify.outputs.gem_path }} + GEM_PATH_TO_PUBLISH: pkg/${{ needs.verify.outputs.gem_filename }} run: gem push --host https://rubygems.org "$GEM_PATH_TO_PUBLISH" diff --git a/.github/workflows/release-validation.yml b/.github/workflows/release-validation.yml index 206ecfc7..f9de898a 100644 --- a/.github/workflows/release-validation.yml +++ b/.github/workflows/release-validation.yml @@ -20,11 +20,10 @@ jobs: with: ruby-version: '4.0' bundler-cache: true - - name: Test release validation - run: | - bundle exec rubocop script/release.rb - bundle exec ruby test/release/release_test.rb - - name: Build and validate gem - run: | - bundle exec rake build - ruby script/release.rb verify + - name: Build gem + run: bundle exec rake build + - name: Validate built gem + uses: appium/appium-workflows/.github/actions/ruby-release-validate@b9358466aa945baa7afa4d8f177d9fa4f4515bc0 + with: + gem-name: appium_lib_core + version-file: lib/appium_lib_core/version.rb diff --git a/RELEASING.md b/RELEASING.md index a1b89865..8e420d48 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -92,8 +92,8 @@ is for stable releases from master, not a parallel prerelease branch. ## Recovery - **Release PR has a date mismatch:** rerun **Release Please**. Alternatively, check - out its branch, run `ruby script/release.rb prepare-date`, and commit the date - change. Do not edit the date to the upload day independently of the changelog. + out its branch, use the shared validator’s `prepare-date` command described + below, and commit the date change. Do not edit the date to the upload day independently of the changelog. - **PR checks do not start:** inspect the **Run release PR checks** step, Actions policy, and workflow permissions. Each dispatched workflow must exist on master. Retry the affected workflow from the generated PR branch using **Run workflow**; @@ -104,7 +104,7 @@ is for stable releases from master, not a parallel prerelease branch. not necessarily emit an already-created release again. The verified artifact is kept for 14 days. If it has expired, run **Publish gem** manually from master with the same existing release tag; it rechecks and rebuilds that tag. This retry route is for releases - containing this workflow and validation script, not older historical tags. + containing this publishing workflow, not older historical tags. - **RubyGems already has that version:** confirm the existing release before retrying. The workflow deliberately does not treat every push failure as success. Published versions cannot be overwritten. Do not move the tag or rebuild different source @@ -119,14 +119,32 @@ is for stable releases from master, not a parallel prerelease branch. bundle install bundle exec rbs collection install UNIT_TEST=true bundle exec rake rubocop steep test:unit -bundle exec ruby test/release/release_test.rb bundle exec rake build -ruby script/release.rb verify ``` -To validate a prospective tag, pass `v` to `verify`. This checks metadata; -it does not create the tag. `prepare-date` changes only the local version file. -Neither script command pushes or publishes. `bundle exec rake build` only builds. +The CI workflows use the shared [ruby-release-validate Action](https://github.com/appium/appium-workflows/tree/b9358466aa945baa7afa4d8f177d9fa4f4515bc0/.github/actions/ruby-release-validate), +pinned to commit `b9358466aa945baa7afa4d8f177d9fa4f4515bc0`. Its validation tests are maintained +in that repository; this repository keeps its own application tests. + +For local metadata/package verification, clone the shared repository alongside +this checkout, then run the same validator (adjust paths as needed): + +```sh +git clone https://github.com/appium/appium-workflows.git ../appium-workflows +git -C ../appium-workflows checkout b9358466aa945baa7afa4d8f177d9fa4f4515bc0 +ruby ../appium-workflows/.github/actions/ruby-release-validate/release.rb \ + verify --root . --gem-name appium_lib_core --version-file lib/appium_lib_core/version.rb +``` + +Add `--tag v` to verify a prospective tag. For date recovery, replace +`verify` with `prepare-date` and omit `--tag`; it copies the changelog date into +the local version file. Neither command pushes or publishes. If the shared +checkout already exists, use it instead of cloning again. + +The Action returns an absolute `gem-path` for artifact upload. The workflow passes +only the filename to the separate publishing job, which downloads the artifact +into its own `pkg/` directory. The publishing workflow, `rubygems` environment, +and Trusted Publisher registration remain in this repository. The repository bootstrap is pinned to the existing `v13.1.0` commit `e099307ee1868986215f66002caf05b6f514aafe`, with manifest version `13.1.0`. This avoids diff --git a/script/release.rb b/script/release.rb deleted file mode 100644 index 5075d185..00000000 --- a/script/release.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -require 'date' -require 'json' -require 'rubygems/package' - -# Deterministic release checks shared by release PRs and the publishing workflow. -# This script never commits, tags, or publishes anything. -module ReleaseTools - VERSION_FILE = 'lib/appium_lib_core/version.rb' - TAG_PATTERN = /\Av\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?\z/ - - module_function - - def metadata(root) - source = File.read(File.join(root, VERSION_FILE)) - version = source.match(/VERSION\s*=\s*'([^']+)'/)&.[](1) - date = source.match(/DATE\s*=\s*'([^']+)'/)&.[](1) - raise 'Missing VERSION or DATE' unless version && date - - manifest = JSON.parse(File.read(File.join(root, '.release-please-manifest.json'))).fetch('.') - raise 'Manifest and VERSION differ' unless manifest == version - - # Accept both historical headers and Release Please's linked headers. - header = File.read(File.join(root, 'CHANGELOG.md')).lines.find { |line| line.start_with?('## ') } - match = header&.match(/^## \[([^\]]+)\](?:\([^)]*\))? (?:- |\()(\d{4}-\d{2}-\d{2})\)?\s*$/) - raise 'Latest changelog entry does not match VERSION' unless match && match[1] == version - - release_date = match[2] - Date.iso8601(release_date) - [version, date, release_date] - end - - def prepare_date(root) - _version, current_date, release_date = metadata(root) - return if current_date == release_date - - path = File.join(root, VERSION_FILE) - source = File.read(path) - File.write(path, source.sub(/(DATE\s*=\s*)'[^']+'/, "\\1'#{release_date}'")) - end - - def verify(root, tag = nil) - version, date, release_date = metadata(root) - raise 'DATE does not match the changelog; run prepare-date' unless date == release_date - - if tag - raise 'Invalid release tag' unless TAG_PATTERN.match?(tag) - raise 'Tag and VERSION differ' unless tag == "v#{version}" - end - - gem_version = Gem::Version.new(version) - gem_path = "pkg/appium_lib_core-#{gem_version}.gem" - package = Gem::Package.new(File.join(root, gem_path)) - package.verify - spec = package.spec - raise 'Gem name or version differs from release metadata' unless spec.name == 'appium_lib_core' && spec.version == gem_version - - packaged_source = package.contents.include?(VERSION_FILE) - raise 'Gem is missing version.rb' unless packaged_source - - gem_path - end -end - -if $PROGRAM_NAME == __FILE__ - root = File.expand_path('..', __dir__) - case ARGV.shift - when 'prepare-date' - ReleaseTools.prepare_date(root) - when 'verify' - puts "gem_path=#{ReleaseTools.verify(root, ARGV.shift)}" - else - abort 'Usage: ruby script/release.rb prepare-date|verify [tag]' - end -end diff --git a/test/release/release_test.rb b/test/release/release_test.rb deleted file mode 100644 index 4e4edabd..00000000 --- a/test/release/release_test.rb +++ /dev/null @@ -1,116 +0,0 @@ -# frozen_string_literal: true - -require 'fileutils' -require 'minitest/autorun' -require 'tmpdir' -require_relative '../../script/release' - -class ReleaseTest < Minitest::Test - def setup - @root = Dir.mktmpdir('appium-release-test') - write_metadata - end - - def teardown - FileUtils.remove_entry(@root) - end - - def test_existing_changelog_format - build_gem - assert_equal 'pkg/appium_lib_core-13.1.1.gem', ReleaseTools.verify(@root, 'v13.1.1') - end - - def test_release_please_date_update_is_idempotent - write_metadata(date: '2026-07-13', linked: true) - ReleaseTools.prepare_date(@root) - path = File.join(@root, ReleaseTools::VERSION_FILE) - updated = File.read(path) - assert_includes updated, "DATE = '2026-09-12'" - assert_includes updated, "VERSION = '13.1.1'" - ReleaseTools.prepare_date(@root) - assert_equal updated, File.read(path) - build_gem - assert_equal 'pkg/appium_lib_core-13.1.1.gem', ReleaseTools.verify(@root) - end - - def test_manifest_mismatch - File.write(File.join(@root, '.release-please-manifest.json'), JSON.generate('.' => '13.1.0')) - assert_raises(RuntimeError) { ReleaseTools.verify(@root) } - end - - def test_changelog_mismatch - File.write(File.join(@root, 'CHANGELOG.md'), "## [13.1.0] - 2026-09-12\n") - assert_raises(RuntimeError) { ReleaseTools.prepare_date(@root) } - end - - def test_missing_changelog_entry - File.write(File.join(@root, 'CHANGELOG.md'), '# Changelog') - assert_raises(RuntimeError) { ReleaseTools.verify(@root) } - end - - def test_invalid_calendar_date - write_metadata(release_date: '2026-02-30') - assert_raises(Date::Error) { ReleaseTools.prepare_date(@root) } - end - - def test_stale_date_blocks_publication - write_metadata(date: '2026-07-13') - assert_raises(RuntimeError) { ReleaseTools.verify(@root) } - end - - def test_wrong_or_malformed_tag - ['v13.1.2', '13.1.1', 'master', 'v13.1.1/other', "v13.1.1\n", 'v13.1.1;command'].each do |tag| - assert_raises(RuntimeError, tag.inspect) { ReleaseTools.verify(@root, tag) } - end - end - - def test_prerelease_uses_rubygems_normalized_filename - write_metadata(version: '13.2.0-rc.1', linked: true) - build_gem(version: '13.2.0-rc.1') - assert_equal 'pkg/appium_lib_core-13.2.0.pre.rc.1.gem', ReleaseTools.verify(@root, 'v13.2.0-rc.1') - end - - def test_wrong_gem_identity - path = build_gem(name: 'different-gem') - FileUtils.mv(path, File.join(@root, 'pkg/appium_lib_core-13.1.1.gem')) - assert_raises(RuntimeError) { ReleaseTools.verify(@root) } - end - - def test_wrong_packaged_version - path = build_gem(version: '13.1.0') - FileUtils.mv(path, File.join(@root, 'pkg/appium_lib_core-13.1.1.gem')) - assert_raises(RuntimeError) { ReleaseTools.verify(@root) } - end - - def test_missing_packaged_version_file - build_gem(include_version: false) - assert_raises(RuntimeError) { ReleaseTools.verify(@root) } - end - - private - - def write_metadata(version: '13.1.1', date: '2026-09-12', release_date: '2026-09-12', linked: false) - path = File.join(@root, ReleaseTools::VERSION_FILE) - FileUtils.mkdir_p(File.dirname(path)) - File.write(path, "VERSION = '#{version}'\nDATE = '#{date}'\n") - File.write(File.join(@root, '.release-please-manifest.json'), JSON.generate('.' => version)) - header = linked ? "## [#{version}](https://example.com/compare) (#{release_date})" : "## [#{version}] - #{release_date}" - File.write(File.join(@root, 'CHANGELOG.md'), "# Changelog\n\n#{header}\n\n- A change\n") - end - - def build_gem(name: 'appium_lib_core', version: '13.1.1', include_version: true) - spec = Gem::Specification.new do |gem| - gem.name = name - gem.version = version - gem.summary = 'Release validation fixture' - gem.authors = ['Test'] - gem.files = include_version ? [ReleaseTools::VERSION_FILE] : [] - gem.license = 'Apache-2.0' - gem.homepage = 'https://example.com' - end - output = File.join(@root, "pkg/#{spec.file_name}") - FileUtils.mkdir_p(File.dirname(output)) - capture_io { Dir.chdir(@root) { Gem::Package.build(spec, true, false, output) } } - output - end -end