diff --git a/.github/workflows/release-ruby.yml b/.github/workflows/release-ruby.yml index b157201..0ce2cb4 100644 --- a/.github/workflows/release-ruby.yml +++ b/.github/workflows/release-ruby.yml @@ -123,6 +123,7 @@ jobs: sha: ${{ inputs.sha || github.event.pull_request.head.sha }} dry_run: ${{ inputs.dry_run || github.event_name == 'pull_request' }} working_directory: test/release/ruby + package_name: bt-publishing-test # exercises the RubyGems-availability fail-fast check prepare: needs: validate diff --git a/actions/release/lang/js/publish/action.yml b/actions/release/lang/js/publish/action.yml index c0ab986..568ede4 100644 --- a/actions/release/lang/js/publish/action.yml +++ b/actions/release/lang/js/publish/action.yml @@ -421,7 +421,7 @@ runs: echo "=== pnpm ===" pnpm --version || echo "(unavailable)" echo "=== Registry ===" - npm config get registry + pnpm config get registry echo "=== package.json version ===" node -p "require('./package.json').version" 2>/dev/null || echo "(unavailable)" diff --git a/actions/release/lang/js/validate/action.yml b/actions/release/lang/js/validate/action.yml index c76c4a2..cb9737e 100644 --- a/actions/release/lang/js/validate/action.yml +++ b/actions/release/lang/js/validate/action.yml @@ -159,11 +159,24 @@ runs: echo "No package_name provided; skipping npm availability check." exit 0 fi - if npm view "$PACKAGE_NAME@$VERSION" version --registry "$REGISTRY" >/dev/null 2>&1; then - echo "Error: $PACKAGE_NAME@$VERSION already exists on npm — has the version been bumped?" - exit 1 - fi - echo "$PACKAGE_NAME@$VERSION is available on npm." + REGISTRY="${REGISTRY%/}" + CODE=$(curl -sS -o /dev/null -w '%{http_code}' \ + --retry 3 --retry-connrefused --max-time 30 \ + "$REGISTRY/$PACKAGE_NAME/$VERSION") + case "$CODE" in + 200) + echo "Error: $PACKAGE_NAME@$VERSION already exists on npm — has the version been bumped?" + exit 1 + ;; + 404) + echo "$PACKAGE_NAME@$VERSION is available on npm." + ;; + *) + echo "Error: unexpected response from the npm registry (HTTP $CODE) checking $PACKAGE_NAME@$VERSION;" + echo "cannot confirm the version is unpublished — aborting rather than risk a bad release." + exit 1 + ;; + esac - name: Validate release channel shell: bash @@ -251,6 +264,6 @@ runs: echo "=== pnpm ===" pnpm --version || echo "(unavailable)" echo "=== Registry ===" - npm config get registry + pnpm config get registry echo "=== package.json version ===" node -p "require('./package.json').version" 2>/dev/null || echo "(unavailable)" diff --git a/actions/release/lang/ruby/validate/action.yml b/actions/release/lang/ruby/validate/action.yml index b2e77e1..7f887b6 100644 --- a/actions/release/lang/ruby/validate/action.yml +++ b/actions/release/lang/ruby/validate/action.yml @@ -2,8 +2,8 @@ name: Validate Ruby Release description: > Checks out the release SHA, sets up Ruby, reads the gem version, validates - the release (tag existence, branch, commit metadata), and runs lint + build - to confirm the gem is publishable before the approval gate. + the release (RubyGems availability, tag existence, branch, commit metadata), + and runs lint + build to confirm the gem is publishable before the approval gate. inputs: sha: @@ -32,6 +32,13 @@ inputs: description: "Path to the gem root (where Gemfile and gemspec live)" required: false default: '.' + package_name: + description: > + RubyGems gem name. When set, validate fails fast if this version is already + published — the registry, not the git tag, is the source of truth (covers + prereleases and force-deleted tags). Leave empty to skip. + required: false + default: '' ruby_version: description: "Ruby version to use, or a version file path (.tool-versions, .ruby-version)" required: false @@ -83,6 +90,34 @@ runs: echo "version=$VERSION" >> $GITHUB_OUTPUT fi + - name: Check RubyGems version availability + shell: bash + env: + PACKAGE_NAME: ${{ inputs.package_name }} + VERSION: ${{ steps.read-version.outputs.version }} + run: | + if [ -z "$PACKAGE_NAME" ]; then + echo "No package_name provided; skipping RubyGems availability check." + exit 0 + fi + CODE=$(curl -sS -o /dev/null -w '%{http_code}' \ + --retry 3 --retry-connrefused --max-time 30 \ + "https://rubygems.org/api/v2/rubygems/$PACKAGE_NAME/versions/$VERSION.json") + case "$CODE" in + 200) + echo "Error: $PACKAGE_NAME $VERSION already exists on RubyGems — has the version been bumped?" + exit 1 + ;; + 404) + echo "$PACKAGE_NAME $VERSION is available on RubyGems." + ;; + *) + echo "Error: unexpected response from RubyGems (HTTP $CODE) checking $PACKAGE_NAME $VERSION;" + echo "cannot confirm the version is unpublished — aborting rather than risk a bad release." + exit 1 + ;; + esac + - name: Validate SHA format shell: bash env: diff --git a/templates/actions/release/lang/ruby/validate.yml.erb b/templates/actions/release/lang/ruby/validate.yml.erb index 4b918d7..f5e5967 100644 --- a/templates/actions/release/lang/ruby/validate.yml.erb +++ b/templates/actions/release/lang/ruby/validate.yml.erb @@ -1,8 +1,8 @@ name: Validate Ruby Release description: > Checks out the release SHA, sets up Ruby, reads the gem version, validates - the release (tag existence, branch, commit metadata), and runs lint + build - to confirm the gem is publishable before the approval gate. + the release (RubyGems availability, tag existence, branch, commit metadata), + and runs lint + build to confirm the gem is publishable before the approval gate. inputs: sha: @@ -31,6 +31,13 @@ inputs: description: "Path to the gem root (where Gemfile and gemspec live)" required: false default: '.' + package_name: + description: > + RubyGems gem name. When set, validate fails fast if this version is already + published — the registry, not the git tag, is the source of truth (covers + prereleases and force-deleted tags). Leave empty to skip. + required: false + default: '' ruby_version: description: "Ruby version to use, or a version file path (.tool-versions, .ruby-version)" required: false @@ -64,6 +71,8 @@ runs: <%= render_step('lang/ruby/read-version') %> +<%= render_step('release/lang/ruby/rubygems-availability') %> + <%= render_step('release/validate', version: "${{ steps.read-version.outputs.version }}") %> - name: Lint and build diff --git a/templates/steps/lang/js/env-dump.yml.erb b/templates/steps/lang/js/env-dump.yml.erb index 3319b4c..920fe52 100644 --- a/templates/steps/lang/js/env-dump.yml.erb +++ b/templates/steps/lang/js/env-dump.yml.erb @@ -15,6 +15,6 @@ echo "=== pnpm ===" pnpm --version || echo "(unavailable)" echo "=== Registry ===" - npm config get registry + pnpm config get registry echo "=== package.json version ===" node -p "require('./package.json').version" 2>/dev/null || echo "(unavailable)" diff --git a/templates/steps/release/lang/js/npm-availability.yml.erb b/templates/steps/release/lang/js/npm-availability.yml.erb index 63b078d..e5ac1a8 100644 --- a/templates/steps/release/lang/js/npm-availability.yml.erb +++ b/templates/steps/release/lang/js/npm-availability.yml.erb @@ -1,8 +1,11 @@ <%# Fails fast if the package version is already published to the npm registry. - Read-only registry query (no auth). Self-guards: skips when package_name is - empty. Complements release/validate's git-tag check by covering prereleases, - which aren't tagged. Assumes the read-version step (id: read-version) has run. + Read-only registry HTTP query — no auth, and no package-manager CLI, so it works + in pnpm-strict repos (a `npm view` here trips devEngines/engines with + EBADDEVENGINES, and swallowing that error silently passes the check). Self-guards: + skips when package_name is empty. Complements release/validate's git-tag check by + covering prereleases, which aren't tagged. Assumes the read-version step + (id: read-version) has run. @param package_name [String] expr for the npm package name. Default: ${{ inputs.package_name }} @param registry [String] expr for the npm registry URL. Default: ${{ inputs.registry }} @@ -18,8 +21,21 @@ echo "No package_name provided; skipping npm availability check." exit 0 fi - if npm view "$PACKAGE_NAME@$VERSION" version --registry "$REGISTRY" >/dev/null 2>&1; then - echo "Error: $PACKAGE_NAME@$VERSION already exists on npm — has the version been bumped?" - exit 1 - fi - echo "$PACKAGE_NAME@$VERSION is available on npm." + REGISTRY="${REGISTRY%/}" + CODE=$(curl -sS -o /dev/null -w '%{http_code}' \ + --retry 3 --retry-connrefused --max-time 30 \ + "$REGISTRY/$PACKAGE_NAME/$VERSION") + case "$CODE" in + 200) + echo "Error: $PACKAGE_NAME@$VERSION already exists on npm — has the version been bumped?" + exit 1 + ;; + 404) + echo "$PACKAGE_NAME@$VERSION is available on npm." + ;; + *) + echo "Error: unexpected response from the npm registry (HTTP $CODE) checking $PACKAGE_NAME@$VERSION;" + echo "cannot confirm the version is unpublished — aborting rather than risk a bad release." + exit 1 + ;; + esac diff --git a/templates/steps/release/lang/ruby/rubygems-availability.yml.erb b/templates/steps/release/lang/ruby/rubygems-availability.yml.erb new file mode 100644 index 0000000..178d8f9 --- /dev/null +++ b/templates/steps/release/lang/ruby/rubygems-availability.yml.erb @@ -0,0 +1,36 @@ +<%# + Fails fast if the gem version is already published to RubyGems. Read-only + registry HTTP query (no auth). The registry — not the git tag — is the source of + truth, so this catches a duplicate even if the tag was force-deleted or never + pushed, and it covers prereleases (which aren't tagged). Self-guards: skips when + package_name is empty. Assumes the read-version step (id: read-version) has run. + + @param package_name [String] expr for the RubyGems gem name. Default: ${{ inputs.package_name }} +-%> +- name: Check RubyGems version availability + shell: bash + env: + PACKAGE_NAME: <%= locals.fetch(:package_name) { "${{ inputs.package_name }}" } %> + VERSION: ${{ steps.read-version.outputs.version }} + run: | + if [ -z "$PACKAGE_NAME" ]; then + echo "No package_name provided; skipping RubyGems availability check." + exit 0 + fi + CODE=$(curl -sS -o /dev/null -w '%{http_code}' \ + --retry 3 --retry-connrefused --max-time 30 \ + "https://rubygems.org/api/v2/rubygems/$PACKAGE_NAME/versions/$VERSION.json") + case "$CODE" in + 200) + echo "Error: $PACKAGE_NAME $VERSION already exists on RubyGems — has the version been bumped?" + exit 1 + ;; + 404) + echo "$PACKAGE_NAME $VERSION is available on RubyGems." + ;; + *) + echo "Error: unexpected response from RubyGems (HTTP $CODE) checking $PACKAGE_NAME $VERSION;" + echo "cannot confirm the version is unpublished — aborting rather than risk a bad release." + exit 1 + ;; + esac