diff --git a/.github/workflows/publish-gem.yml b/.github/workflows/publish-gem.yml new file mode 100644 index 0000000..5ebe916 --- /dev/null +++ b/.github/workflows/publish-gem.yml @@ -0,0 +1,92 @@ +name: Publish gem + +on: + workflow_dispatch: + inputs: + tag: + description: Existing release tag to publish or retry (for example v13.1.1) + required: true + type: string + +permissions: + contents: read + +concurrency: + group: publish-gem-${{ inputs.tag }} + cancel-in-progress: false + +env: + RELEASE_TAG: ${{ inputs.tag }} + +jobs: + verify: + if: github.repository == 'appium/ruby_console' + runs-on: ubuntu-latest + outputs: + gem_filename: ${{ steps.artifact.outputs.gem-filename }} + steps: + - name: Validate tag syntax + shell: bash + run: | + if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]]; then + echo 'Expected a release tag such as v13.1.1 or v13.2.0-rc.1' >&2 + exit 1 + fi + - uses: actions/checkout@v7 + with: + ref: refs/tags/${{ env.RELEASE_TAG }} + fetch-depth: 0 + persist-credentials: false + - name: Verify release commit belongs to master + run: git merge-base --is-ancestor HEAD origin/master + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '4.0' + bundler-cache: true + # Build before tools/tests can generate or update local files. + - name: Build gem from the tagged checkout + run: bundle exec rake build + - name: Run checks on the release commit + run: | + bundle exec rake rubocop + bundle exec ruby -Ilib bin/arc version + - name: Validate the built gem + id: package + uses: appium/appium-workflows/.github/actions/ruby-release-validate@b9358466aa945baa7afa4d8f177d9fa4f4515bc0 + with: + gem-name: appium_console + version-file: lib/appium_console/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 }} + if-no-files-found: error + retention-days: 14 + + publish: + needs: verify + runs-on: ubuntu-latest + environment: rubygems + permissions: + contents: read + id-token: write + steps: + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '4.0' + - uses: actions/download-artifact@v4 + with: + name: release-gem + path: pkg + - name: Authenticate with RubyGems Trusted Publishing + uses: rubygems/configure-rubygems-credentials@daab0479595bcc124493903b3df17273d86e073f + - name: Publish the verified gem + env: + 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-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..60a5642 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,55 @@ +name: Release Please + +on: + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-please + cancel-in-progress: false + +jobs: + release: + if: github.repository == 'appium/ruby_console' + permissions: + contents: write + pull-requests: write + issues: write + actions: write + runs-on: ubuntu-latest + env: + TZ: UTC + steps: + - name: Update release PR or create GitHub Release + id: release + uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + target-branch: master + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + # GITHUB_TOKEN does not automatically start downstream workflows. + # Explicit dispatch runs checks on the release PR commit without a bot key. + - name: Run release PR checks + if: steps.release.outputs.prs_created == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + RELEASE_PR: ${{ steps.release.outputs.pr }} + run: | + branch=$(jq -er '.headBranchName | select(type == "string" and length > 0)' <<< "$RELEASE_PR") + for workflow in release-validation.yml; do + gh workflow run "$workflow" --ref "$branch" + done + - name: Publish the new release + if: steps.release.outputs.release_created == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ steps.release.outputs.tag_name }} + run: gh workflow run publish-gem.yml --ref master -f "tag=$RELEASE_TAG" diff --git a/.github/workflows/release-validation.yml b/.github/workflows/release-validation.yml new file mode 100644 index 0000000..672672f --- /dev/null +++ b/.github/workflows/release-validation.yml @@ -0,0 +1,33 @@ +name: Release validation + +on: + workflow_dispatch: + pull_request: + push: + branches: [master] + +permissions: + contents: read + +jobs: + package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '4.0' + bundler-cache: true + - name: Check console + run: | + bundle exec rake rubocop + bundle exec ruby -Ilib bin/arc version + - 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_console + version-file: lib/appium_console/version.rb diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..34a3350 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "4.2.0" +} diff --git a/.rubocop.yml b/.rubocop.yml index f999288..3141405 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,6 @@ AllCops: TargetRubyVersion: 3.1 -Metrics/LineLength: +Layout/LineLength: Max: 128 Metrics/MethodLength: Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..be12a15 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +Historical commit-level notes through 4.2.0 are in [release_notes.md](release_notes.md). +New entries are maintained by Release Please. + +## [4.2.0] - 2025-01-25 + +- See the historical release notes for this release. diff --git a/Gemfile b/Gemfile index 604eed8..7a34208 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,5 @@ source 'https://rubygems.org' gemspec -gem 'appium_thor', '~> 2.0' gem 'rake', '~> 13.0' gem 'rubocop', '1.90.0' diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..2d0028c --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,167 @@ +# Releasing appium_console + +Release Please prepares the release PR, version, changelog, tag, and GitHub Release. +GitHub Actions builds and tests the tagged code, then publishes that exact gem to +RubyGems.org using Trusted Publishing. `appium_thor` is no longer required. + +After the one-time setup, the maintainer's normal release action is to review and +merge the release PR. There is no local version bump, tag push, or gem push. + +## One-time setup + +1. In repository **Settings → Actions → General → Workflow permissions**, enable + **Allow GitHub Actions to create and approve pull requests** (the organization + policy must permit it). Workflows declare their own permissions. Release Please + uses the built-in `GITHUB_TOKEN`; no GitHub App, PAT, or new GitHub secret is needed. +2. Keep the normal branch protection/rulesets on `master`. Require the `package` job from **Release validation** before merging + a release PR. Do not exempt the release bot from those merge checks. Its direct + writes are to the generated release PR branch and release tags, not master. +3. Create the GitHub Actions environment `rubygems`. Permit release tags (`v*`) and + the `master` branch (for automated publishing and retries). Add a required reviewer only if a + separate approval for each publication is wanted; otherwise merging the release + PR is the normal human decision. +4. As an owner of `appium_console` on RubyGems.org, configure a Trusted Publisher: + - Repository owner: `appium` + - Repository name: `ruby_console` + - Workflow filename: `publish-gem.yml` + - Environment: `rubygems` +5. Merge the migration after its CI passes. A push to master or a manual run of + **Release Please** updates the pending release PR. Configure the RubyGems + publisher before merging that first release PR. + +This follows the standard `GITHUB_TOKEN` and OIDC authentication used by Appium's +Python client and XCUITest driver release workflows. Release Please explicitly +uses `workflow_dispatch` to run the package validation +workflow on the release PR branch, and **Publish gem** on master with the new tag. +These events run with `GITHUB_TOKEN` without a separate bot credential. All workflow +files must first be on master for dispatch to work. + +GitHub may also show approval-required `pull_request` runs for bot-created PRs. +The dispatched package checks do not require that approval. If additional +PR-event-only checks are made required, adapt their triggers as well. + +No credentials, remote settings, tags, or releases are created by installing these +files. Configure the RubyGems publisher before merging the first release PR. + +## Normal release + +1. Merge ordinary changes using Conventional Commit titles/messages. A `fix:` + produces a patch bump, `feat:` a minor bump, and a breaking-change marker such as + `feat!:` a major bump. Chore-only changes do not normally create a release PR. +2. Release Please maintains a PR updating `lib/appium_console/version.rb`, + `.release-please-manifest.json`, and `CHANGELOG.md`. Its standard `x-release-please-date` annotation also updates + `Appium::Console::DATE` in the same PR. Wait for checks on the final PR commit. +3. Review the version and release notes, then merge the PR when ready. If master + has advanced, require current checks before merging through the repository rules. +4. Release Please creates `v` and a GitHub Release, then dispatches + **Publish gem** with that tag. **Publish gem** verifies + that the tag is on master's history, builds the gem before test tools can modify local files, runs checks, and verifies + the tag, source version, manifest, changelog date, and packaged gem identity. +5. Only a successful verification job uploads a gem for the separate publishing + job. That job has the OIDC permission, downloads the verified artifact, and uses + `gem push`. It does not rebuild the package or create another tag. + +A GitHub Release can exist before RubyGems publication succeeds. The **Publish gem** +workflow result is the publication status; creating a GitHub Release alone does +not mean the gem has been published. + +`release_notes.md` is retained as historical commit-level notes through 4.2.0. +New release notes are generated in `CHANGELOG.md` and GitHub Releases. `DATE` is the +release-preparation date recorded in that changelog, not the later RubyGems upload +timestamp. No public version/date constant is removed. + +## Choosing a version or publishing an RC + +For a one-off explicit version, use Release Please's `Release-As: 13.2.0` footer in +a commit included in the release, or the `release-as` package setting. If using the +setting, remove it after the release so it does not keep forcing the same version. +Review the generated PR instead of changing only VERSION independently of the +manifest and changelog. + +For an RC, configure the root package with `"prerelease": true` and an explicit +`"release-as": "13.2.0-rc.1"`. Release Please uses a SemVer tag such as +`v13.2.0-rc.1`; RubyGems normalizes the gem version/filename to `13.2.0.pre.rc.1`. +The release validator accepts that mapping. Use `rc.2`, etc. for further RCs. +Before the stable release, remove the prerelease flag and explicit RC version (or +set the intended stable release version explicitly). The default configuration +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, 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**; + rerunning Release Please may not return an unchanged PR in its outputs. +- **Tag/Release exists but publication failed:** correct external setup if needed + and rerun the failed workflow jobs. If dispatch itself failed, run + **Publish gem** from master with the existing tag; rerunning Release Please does + 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 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 + under the same version; use a new release when a code fix is needed. +- **A build or validation needs a source-code fix:** fix master and prepare a new + release. The retry path always checks out the original tag, so it cannot silently + publish newer master code under that tag. + +## Local checks without publication + +```sh +bundle install +bundle exec rake rubocop +bundle exec ruby -Ilib bin/arc version +bundle exec rake build +``` + +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_console --version-file lib/appium_console/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 `v4.2.0` commit +`2f63a20264d375195d49b975df974522605c54ce`, with manifest version `4.2.0`. This avoids +replaying all historical changes on the first release. Once Release Please has +made its first release, its bootstrap setting is ignored and can be removed. + +## References + +- [Release Please](https://github.com/googleapis/release-please) +- [GitHub workflow triggering and token behavior](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow) +- [Appium Python client release workflow](https://github.com/appium/python-client/blob/master/.github/workflows/publish.yml) +- [Appium XCUITest driver release workflow](https://github.com/appium/appium-xcuitest-driver/blob/master/.github/workflows/publish.js.yml) +- [Manifest and prerelease configuration](https://github.com/googleapis/release-please/blob/main/docs/manifest-releaser.md) +- [RubyGems Trusted Publishing](https://guides.rubygems.org/trusted-publishing/) +- [RubyGems credentials Action](https://github.com/rubygems/configure-rubygems-credentials) + +The credentials Action is used instead of `rubygems/release-gem` because Release +Please already owns the tag. Authentication plus `gem push` avoids invoking a +second tool's tag-creation/release sequence. + +The runtime `thor` dependency is retained for the `arc` CLI. Only the separate +`appium_thor` release tooling is removed. The old `thor spec` task referred to a +nonexistent `spec/` directory; release helper tests now live in the shared Action repository. diff --git a/Thorfile b/Thorfile deleted file mode 100644 index ea3b5a8..0000000 --- a/Thorfile +++ /dev/null @@ -1,20 +0,0 @@ -require 'appium_thor' - -Appium::Thor::Config.set do - gem_name 'appium_console' - github_owner 'appium' - github_name 'ruby_console' - version_file 'lib/appium_console/version.rb' -end - -# Must use '::' otherwise Default will point to Thor::Sandbox::Default -# Debug by calling Thor::Base.subclass_files via Pry -# -# https://github.com/erikhuda/thor/issues/484 -# -class ::Default < Thor - desc 'spec', 'Run RSpec tests' - def spec - exec 'rspec spec' - end -end diff --git a/appium_console.gemspec b/appium_console.gemspec index 37e0452..10aee6c 100644 --- a/appium_console.gemspec +++ b/appium_console.gemspec @@ -29,4 +29,10 @@ Gem::Specification.new do |s| s.executables = ['arc'] s.files = `git ls-files`.split "\n" s.metadata['rubygems_mfa_required'] = 'true' + # Keep release automation out of the published package. + s.files.reject! do |file| + file.start_with?('.github/', 'script/', 'test/release/') || + %w[RELEASING.md release-please-config.json .release-please-manifest.json].include?(file) + end + end diff --git a/lib/appium_console/version.rb b/lib/appium_console/version.rb index 1ce679e..a3d90e9 100644 --- a/lib/appium_console/version.rb +++ b/lib/appium_console/version.rb @@ -5,6 +5,6 @@ module Appium; end unless defined? Appium module Appium module Console VERSION = '4.2.0' unless defined? ::Appium::Console::VERSION - DATE = '2025-01-25' unless defined? ::Appium::Console::DATE + DATE = '2025-01-25' unless defined? ::Appium::Console::DATE # x-release-please-date end end diff --git a/readme.md b/readme.md index 5c39798..8a23bae 100644 --- a/readme.md +++ b/readme.md @@ -31,3 +31,8 @@ - 3.0.0 - Update ruby_lib version to v12 + +## Releasing + +Review and merge the Release Please PR to publish. See [RELEASING.md](RELEASING.md) +for one-time Trusted Publishing setup and recovery instructions. diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..beb2365 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "2f63a20264d375195d49b975df974522605c54ce", + "packages": { + ".": { + "release-type": "ruby", + "package-name": "appium_console", + "version-file": "lib/appium_console/version.rb", + "include-component-in-tag": false, + "include-v-in-tag": true, + "changelog-path": "CHANGELOG.md", + "extra-files": [ + "lib/appium_console/version.rb" + ] + } + } +}