diff --git a/ruby/build/action.yml b/ruby/build/action.yml new file mode 100644 index 0000000..5619680 --- /dev/null +++ b/ruby/build/action.yml @@ -0,0 +1,83 @@ +name: Build Gem +description: Build a gem for a DBX Ruby project +inputs: + app_id: + description: The APP_ID defined for this project + required: true + app_private_key: + description: The APP_PRIVATE_KEY defined for this project + required: true + artifact: + description: The name to give the generated artifact (e.g. "ruby" or "jruby") + required: false + default: ruby + bundler_cache_version: + description: The cache-version to use for the bundler cache + required: false + default: '0' + gem_name: + description: The name (sans extension) of the gemspec file (e.g. "mongo") + required: true + ref: + description: The reference to checkout (branch, tag, sha, etc) + required: true + ruby_version: + description: The version of Ruby to use (see setup-ruby/action.yml) + default: '3.2' + required: false + rubygems_version: + description: The version of Rubygems to use (see setup-ruby/action.yml) + required: false + default: latest + +runs: + using: composite + steps: + - name: Check out the repository + # 58501b85eae697e451b5d1d7dba53f69f65d1909 => the 'v2' tag as of 2025-07-28 + uses: mongodb-labs/drivers-github-tools/secure-checkout@58501b85eae697e451b5d1d7dba53f69f65d1909 + with: + app_id: ${{ inputs.app_id }} + private_key: ${{ inputs.app_private_key }} + ref: ${{ inputs.ref }} + submodules: true + + - name: Setup Ruby + # bb6434c747fa7022e12fa1cae2a0951fcffcff26 => the 'v1' branch as of 2025-07-28 + uses: ruby/setup-ruby@a9bfc2ecf3dd40734a9418f89a7e9d484c32b990 + with: + ruby-version: ${{ inputs.ruby_version }} + rubygems: ${{ inputs.rubygems_version }} + bundler-cache: true + cache-version: ${{ inputs.bundler_cache_version }} + + - name: Get the release version + id: release_version + shell: bash + run: echo "version=$(bundle exec rake version)" >> "$GITHUB_OUTPUT" + + - name: Get the gem file name + shell: bash + id: gem_name + env: + GEM_NAME: ${{ inputs.gem_name }} + ACTION_PATH: ${{ github.action_path }} + RELEASE_VERSION: ${{ steps.release_version.outputs.version }} + run: echo "name=$(ruby ${ACTION_PATH}/gem_name.rb ${GEM_NAME} ${RELEASE_VERSION})" >> "$GITHUB_OUTPUT" + + - name: Build the gem + shell: bash + env: + GEM_NAME: ${{ inputs.gem_name }} + GEM_FILE_NAME: ${{ steps.gem_name.outputs.name }} + run: | + bundle exec rake build GEMSPEC="${GEM_NAME}.gemspec" GEM_FILE_NAME="${GEM_FILE_NAME}" + + - name: Save the generated gem file for later + # ea165f8d65b6e75b540449e92b4886f43607fa02 => the 'v4' tag as of 2025-07-28 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: ${{ inputs.artifact }} + path: ${{ steps.gem_name.outputs.name }} + retention-days: 1 + overwrite: true diff --git a/ruby/build/gem_name.rb b/ruby/build/gem_name.rb new file mode 100644 index 0000000..a99284a --- /dev/null +++ b/ruby/build/gem_name.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# This script generates the name of a gem file based on the provided +# gem name and version. It takes into account whether it is running +# under JRuby to append "-java" to the gem name if necessary. +# +# Usage: +# ruby gem_name.rb + +if ARGV.length != 2 + puts "Usage: ruby gem_name.rb " + exit 1 +end + +gem_name = ARGV.first +gem_version = ARGV.last + +base_name = "#{gem_name}-#{gem_version}" +base_name = "#{base_name}-java" if defined?(JRUBY_VERSION) + +puts "#{base_name}.gem" diff --git a/ruby/cleanup/action.yml b/ruby/cleanup/action.yml index 1f144a1..1f98cab 100644 --- a/ruby/cleanup/action.yml +++ b/ruby/cleanup/action.yml @@ -15,7 +15,8 @@ runs: using: composite steps: - name: 'Check out the repository' - uses: mongodb-labs/drivers-github-tools/secure-checkout@v2 + # 58501b85eae697e451b5d1d7dba53f69f65d1909 => the 'v2' tag as of 2025-07-28 + uses: mongodb-labs/drivers-github-tools/secure-checkout@58501b85eae697e451b5d1d7dba53f69f65d1909 with: app_id: ${{ inputs.app_id }} private_key: ${{ inputs.app_private_key }} diff --git a/ruby/pr-check/action.yml b/ruby/pr-check/action.yml new file mode 100644 index 0000000..e43308d --- /dev/null +++ b/ruby/pr-check/action.yml @@ -0,0 +1,113 @@ +# PRs are only eligible for release if they are merged and have +# the `release-candidate` label. +# +# The only events allowed to trigger this action are: +# - push (in which case the commit sha is used to find the corresponding +# PR) +# - workflow_dispatch (in which case the PR is found from the inputs +# on the event) + +name: PR Check +description: Check that a PR is eligible for release + +outputs: + message: + description: The body of the pull request that is being released. + value: ${{ steps.check_pr.outputs.message }} + ref: + description: The ref of the pull request that is being released. + value: ${{ steps.check_pr.outputs.ref }} + +runs: + using: composite + steps: + - name: "Check PR Eligibility" + id: check_pr + # 60a0d83039c74a4aee543508d2ffcb1c3799cdea => 'v7' tag as of 2025-07-28 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea + with: + script: | + let pr; + + // was this triggered by a push event? + if (context.eventName == 'push') { + // if so, we need to find the PR that corresponds to the commit + // that was pushed. + // + // because only maintainers can push to protected branches, + // we can assume the user has the correct permissions to do + // this. + const { data: listing } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: context.payload.after, + }); + + if (listing.length == 0) { + throw new Error(`Workflow aborted: No pull request found for the pushed commit (${context.payload.after}).`); + } + + const response = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: listing[0].number, + }); + + pr = response.data; + + // if it wasn't triggered by a push event, was it triggered by + // a workflow_dispatch event? + } else if (context.eventName == 'workflow_dispatch') { + // it is technically possible for users with only write access + // to trigger workflows; we need to make sure that the user + // who triggered this has either admin or maintain access to the + // repository. + const username = context.triggering_actor || context.actor; + + const { data: perms } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username, + }); + + if (perms.role_name !== 'admin' && perms.role_name !== 'maintain') { + throw new Error(`User ${username} must have 'admin' or 'maintain' role to initiate the release process. (${perms.role_name})`); + } + + // if so, we grab the PR with the number that was passed in with + // the inputs. + const number = context.payload.inputs.pr; + if (!number) { + throw new Error('Workflow aborted: No pull request number provided. (need `pr` input)'); + } + + const response = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: number, + }); + + pr = response.data; + + // workflow was triggered by an unrecognized/unsupported event + } else { + throw new Error(`Workflow aborted: Unsupported event type: ${context.eventName}.`); + } + + if (!pr) { + throw new Error('No pull request found for the triggered event.'); + } + + if (!pr.merged) { + throw new Error('Pull request is not merged.'); + } + + if (!pr.labels.some(label => label.name == 'release-candidate')) { + throw new Error('Pull request is not a release candidate.'); + } + + console.log('body: >>', pr.body, '<<'); + console.log('ref: >>', pr.merge_commit_sha, '<<'); + + core.setOutput('message', pr.body); + core.setOutput('ref', pr.merge_commit_sha); diff --git a/ruby/publish/action.yml b/ruby/publish/action.yml index 711e99d..d3ad53d 100644 --- a/ruby/publish/action.yml +++ b/ruby/publish/action.yml @@ -1,5 +1,5 @@ name: Publish Ruby -description: Generate and publish gems, signatures, and assets for MongoDB Ruby projects +description: Publish gems, signatures, and assets for MongoDB Ruby projects inputs: app_id: description: The APP_ID defined for this project @@ -32,13 +32,20 @@ inputs: product_id: description: The identifier of the product being published (e.g. "mongo-ruby-driver") required: true - release_message_template: - description: The template for the release message. Use "{0}" in the text to refer to the current version. + ref: + description: The reference to checkout (branch, tag, sha, etc) + required: true + release_message: + description: The (markdown-formatted) text to post as the description of the new release required: true rubygems_version: description: The version of Rubygems to use (see setup-ruby/action.yml) required: false default: latest + ruby_version: + description: The version of Ruby to use (see setup-ruby/action.yml) + default: '3.2' + required: false silk_asset_group: description: The Silk asset group for the project required: true @@ -47,92 +54,160 @@ runs: using: composite steps: - name: Check out the repository - uses: mongodb-labs/drivers-github-tools/secure-checkout@v2 + # 58501b85eae697e451b5d1d7dba53f69f65d1909 => the 'v2' tag as of 2025-07-28 + uses: mongodb-labs/drivers-github-tools/secure-checkout@58501b85eae697e451b5d1d7dba53f69f65d1909 with: app_id: ${{ inputs.app_id }} private_key: ${{ inputs.app_private_key }} + ref: ${{ inputs.ref }} + submodules: true - name: Setup Ruby - uses: ruby/setup-ruby@dffc446db9ba5a0c4446edb5bca1c5c473a806c5 # v1 + # bb6434c747fa7022e12fa1cae2a0951fcffcff26 => the 'v1' branch as of 2025-07-28 + uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 with: - ruby-version: '3.2' + ruby-version: ${{ inputs.ruby_version }} rubygems: ${{ inputs.rubygems_version }} bundler-cache: true cache-version: ${{ inputs.bundler_cache_version }} - name: Get the release version + id: release_version shell: bash - run: echo "RELEASE_VERSION=$(bundle exec rake version)" >> "$GITHUB_ENV" + run: echo "version=$(bundle exec rake version)" >> "$GITHUB_OUTPUT" - name: Setup GitHub tooling for DBX Drivers - uses: mongodb-labs/drivers-github-tools/setup@v2 + # 58501b85eae697e451b5d1d7dba53f69f65d1909 => the 'v2' tag as of 2025-07-28 + uses: mongodb-labs/drivers-github-tools/setup@58501b85eae697e451b5d1d7dba53f69f65d1909 with: aws_role_arn: ${{ inputs.aws_role_arn }} aws_region_name: ${{ inputs.aws_region_name }} aws_secret_id: ${{ inputs.aws_secret_id }} - - name: Set output gem file name - shell: bash - run: | - echo "GEM_FILE_NAME=${{ inputs.gem_name }}-${{ env.RELEASE_VERSION }}.gem" >> "$GITHUB_ENV" - - - name: Build the gem - shell: bash - run: | - gem build --output=${{ env.GEM_FILE_NAME }} ${{ inputs.gem_name }}.gemspec + - name: Fetch the gem artifacts + # d3f86a106a0bac45b974a628896c90dbdf5c8093 => the 'v4' tag as of 2025-07-28 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + merge-multiple: true - - name: Sign the gem - uses: mongodb-labs/drivers-github-tools/gpg-sign@v2 + - name: Sign the gems + # 58501b85eae697e451b5d1d7dba53f69f65d1909 => the 'v2' tag as of 2025-07-28 + uses: mongodb-labs/drivers-github-tools/gpg-sign@58501b85eae697e451b5d1d7dba53f69f65d1909 with: - filenames: '${{ env.GEM_FILE_NAME }}' + filenames: '*.gem' - name: Generate SSDLC Reports - uses: mongodb-labs/drivers-github-tools/full-report@v2 + # 58501b85eae697e451b5d1d7dba53f69f65d1909 => the 'v2' tag as of 2025-07-28 + uses: mongodb-labs/drivers-github-tools/full-report@58501b85eae697e451b5d1d7dba53f69f65d1909 with: product_name: ${{ inputs.product_name }} - release_version: ${{ env.RELEASE_VERSION }} - dist_filenames: ${{ env.GEM_FILE_NAME }} + release_version: ${{ steps.release_version.outputs.version }} + dist_filenames: '*.gem' silk_asset_group: ${{ inputs.silk_asset_group }} + - name: Look for existing tag + id: tag_exists + shell: bash + env: + RELEASE_VERSION: ${{ steps.release_version.outputs.version }} + run: | + if git rev-parse "v${RELEASE_VERSION}" >/dev/null 2>&1; then + echo "Tag v${RELEASE_VERSION} already exists." + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "Tag v${RELEASE_VERSION} does not exist." + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + - name: Create the tag - uses: mongodb-labs/drivers-github-tools/tag-version@v2 + # 58501b85eae697e451b5d1d7dba53f69f65d1909 => the 'v2' tag as of 2025-07-28 + uses: mongodb-labs/drivers-github-tools/tag-version@58501b85eae697e451b5d1d7dba53f69f65d1909 + if: steps.tag_exists.outputs.exists == 'false' with: - version: ${{ env.RELEASE_VERSION }} + version: ${{ steps.release_version.outputs.version }} tag_template: "v${VERSION}" tag_message_template: "Release tag for v${VERSION}" - - name: Create a new release + - name: Look for existing release + id: release_exists shell: bash - run: gh release create v${{ env.RELEASE_VERSION }} --title ${{ env.RELEASE_VERSION }} --generate-notes --draft - - - name: Capture the changelog + env: + RELEASE_VERSION: ${{ steps.release_version.outputs.version }} + run: | + if gh release view "v${RELEASE_VERSION}" >/dev/null 2>&1; then + echo "Release v${RELEASE_VERSION} already exists." + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "Release v${RELEASE_VERSION} does not exist." + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Write release notes to file shell: bash - run: gh release view v${{ env.RELEASE_VERSION }} --json body --template '{{ .body }}' >> changelog + env: + RELEASE_NOTES: ${{ inputs.release_message }} + run: | + # identifier is intentionally obscure to avoid potential conflicts + # with supplied text from the release notes themselves. + cat <<'__RELEASE_NOTES_IKDJAIELD__' > release_notes.txt + ${RELEASE_NOTES} + __RELEASE_NOTES_IKDJAIELD__ - - name: Prepare release message + - name: Create a new release + if: steps.release_exists.outputs.exists == 'false' shell: bash - run: | - echo "${{ format(inputs.release_message_template, env.RELEASE_VERSION) }}" > release-message - cat changelog >> release-message + env: + RELEASE_VERSION: ${{ steps.release_version.outputs.version }} + run: gh release create v${RELEASE_VERSION} --title ${RELEASE_VERSION} --notes-file release_notes.txt --draft - - name: Update release information + - name: Else update the existing release + if: steps.release_exists.outputs.exists == 'true' shell: bash - run: | - echo "RELEASE_URL=$(gh release edit v${{ env.RELEASE_VERSION }} --notes-file release-message)" >> "$GITHUB_ENV" + env: + RELEASE_VERSION: ${{ steps.release_version.outputs.version }} + run: gh release edit v${RELEASE_VERSION} --notes-file release_notes.txt - name: Upload release artifacts shell: bash - run: gh release upload v${{ env.RELEASE_VERSION }} ${{ env.GEM_FILE_NAME }} ${{ env.RELEASE_ASSETS }}/${{ env.GEM_FILE_NAME }}.sig + env: + RELEASE_VERSION: ${{ steps.release_version.outputs.version }} + RELEASE_ASSETS: ${{ env.RELEASE_ASSETS }} + run: gh release upload --clobber v${RELEASE_VERSION} *.gem ${RELEASE_ASSETS}/*.sig - name: Upload S3 assets - uses: mongodb-labs/drivers-github-tools/upload-s3-assets@v2 + # 58501b85eae697e451b5d1d7dba53f69f65d1909 => the 'v2' tag as of 2025-07-28 + uses: mongodb-labs/drivers-github-tools/upload-s3-assets@58501b85eae697e451b5d1d7dba53f69f65d1909 with: - version: ${{ env.RELEASE_VERSION }} + version: ${{ steps.release_version.outputs.version }} product_name: ${{ inputs.product_id }} dry_run: ${{ inputs.dry_run }} + - name: Look for existing gem + id: gem_exists + shell: bash + env: + GEM_NAME: ${{ inputs.gem_name }} + RELEASE_VERSION: ${{ steps.release_version.outputs.version }} + run: | + if gem search --remote ${GEM_NAME} --version ${RELEASE_VERSION} | grep -q "${RELEASE_VERSION}"; then + echo "Gem ${GEM_NAME} version ${RELEASE_VERSION} already exists." + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "Gem ${GEM_NAME} version ${RELEASE_VERSION} does not exist." + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + - name: Publish the gem - uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1 - if: inputs.dry_run == 'false' + # ebe1ec66bd8d2c709ac29aa2b43438d450e7a0a6 => the 'v1' branch as of 2025-07-28 + uses: rubygems/release-gem@ebe1ec66bd8d2c709ac29aa2b43438d450e7a0a6 + if: inputs.dry_run == 'false' && steps.gem_exists.outputs.exists == 'false' with: await-release: false + + - name: Publish the release + if: inputs.dry_run == 'false' + shell: bash + env: + RELEASE_VERSION: ${{ steps.release_version.outputs.version }} + run: gh release edit v${RELEASE_VERSION} --draft=false +