From 625c7e8270eabcb874d819d3d5d9214295be3c7a Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Thu, 9 Jul 2026 18:03:52 -0400 Subject: [PATCH] Harden CI: Artifactory OIDC, RubyGems Trusted Publishing, Gemfile.lock - Add Artifactory OIDC composite action for build-time dep resolution - Replace ruby.yml with fork-aware ci.yml (rubocop/test/build, SHA-pinned) - Add deploy.yml: RubyGems Trusted Publishing, no stored API key - Add Gemfile.lock with arm64-darwin + x86_64-linux platforms - Update e2e-tests.yml: remove E2E_TESTS_TOKEN, SHA-pin actions, fork-aware - Add devbox.json, add .claude/ to .gitignore --- .github/actions/artifactory-oidc/action.yml | 51 +++++++++ .github/workflows/ci.yml | 108 ++++++++++++++++++++ .github/workflows/deploy.yml | 62 +++++++++++ .github/workflows/e2e-tests.yml | 22 ++-- .gitignore | 2 +- Gemfile.lock | 104 +++++++++++++++++++ 6 files changed, 340 insertions(+), 9 deletions(-) create mode 100644 .github/actions/artifactory-oidc/action.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 Gemfile.lock diff --git a/.github/actions/artifactory-oidc/action.yml b/.github/actions/artifactory-oidc/action.yml new file mode 100644 index 0000000..eb2c5d1 --- /dev/null +++ b/.github/actions/artifactory-oidc/action.yml @@ -0,0 +1,51 @@ +name: 'Artifactory OIDC Authentication' +description: 'Exchanges a GitHub OIDC token for an Artifactory access token and configures Bundler to use the Artifactory RubyGems mirror.' + +inputs: + artifactory_url: + description: 'The base URL of the Artifactory instance (e.g., https://segment.jfrog.io)' + required: true + +outputs: + token: + description: 'The Artifactory access token' + value: ${{ steps.exchange.outputs.token }} + +runs: + using: 'composite' + steps: + - name: Get OIDC token + id: oidc + shell: bash + run: | + OIDC_TOKEN=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ + "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=jfrog-github" | jq -r '.value') + echo "::add-mask::$OIDC_TOKEN" + echo "oidc_token=$OIDC_TOKEN" >> "$GITHUB_OUTPUT" + + - name: Exchange for Artifactory token + id: exchange + shell: bash + env: + ARTIFACTORY_URL: ${{ inputs.artifactory_url }} + run: | + ART_TOKEN=$(curl -sS -X POST \ + "${ARTIFACTORY_URL}/access/api/v1/oidc/token" \ + -H "Content-Type: application/json" \ + -d "{\"grant_type\": \"urn:ietf:params:oauth:grant-type:token-exchange\", \"subject_token\": \"${{ steps.oidc.outputs.oidc_token }}\", \"subject_token_type\": \"urn:ietf:params:oauth:token-type:id_token\", \"provider_name\": \"github\"}" \ + | jq -r '.access_token') + echo "::add-mask::$ART_TOKEN" + echo "token=$ART_TOKEN" >> "$GITHUB_OUTPUT" + + - name: Configure Bundler Artifactory mirror + shell: bash + env: + ART_TOKEN: ${{ steps.exchange.outputs.token }} + ARTIFACTORY_URL: ${{ inputs.artifactory_url }} + run: | + # Extract the hostname from the Artifactory URL + HOST=$(echo "$ARTIFACTORY_URL" | sed 's|https\?://||') + + # Configure Bundler to mirror rubygems.org through Artifactory + bundle config mirror.https://rubygems.org "https://${HOST}/artifactory/api/gems/virtual-gems-thirdparty/" + bundle config "https://${HOST}/artifactory/api/gems/virtual-gems-thirdparty/" "${ART_TOKEN}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f5e8630 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,108 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +permissions: + id-token: write + contents: read + +env: + ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }} + +jobs: + lint: + name: Lint + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }} + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e01a7b8b7f2007 # v1 + with: + ruby-version: '3.3' + bundler-cache: true + + - name: Authenticate with Artifactory + if: ${{ !github.event.pull_request.head.repo.fork }} + uses: ./.github/actions/artifactory-oidc + with: + artifactory_url: ${{ env.ARTIFACTORY_URL }} + + - name: Install dependencies + run: bundle install + + - name: Run RuboCop + run: bundle exec rubocop + + test: + name: Test (Ruby ${{ matrix.ruby-version }}) + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }} + strategy: + fail-fast: false + matrix: + ruby-version: ['3.1', '3.2', '3.3'] + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e01a7b8b7f2007 # v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + + - name: Authenticate with Artifactory + if: ${{ !github.event.pull_request.head.repo.fork }} + uses: ./.github/actions/artifactory-oidc + with: + artifactory_url: ${{ env.ARTIFACTORY_URL }} + + - name: Install dependencies + run: bundle install + + - name: Run tests + run: bundle exec rake + + build: + name: Build + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }} + needs: [lint, test] + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e01a7b8b7f2007 # v1 + with: + ruby-version: '3.3' + bundler-cache: true + + - name: Authenticate with Artifactory + if: ${{ !github.event.pull_request.head.repo.fork }} + uses: ./.github/actions/artifactory-oidc + with: + artifactory_url: ${{ env.ARTIFACTORY_URL }} + + - name: Install dependencies + run: bundle install + + - name: Build gem + run: gem build analytics-ruby.gemspec + + - name: Verify gem is installable + run: gem install ./analytics-ruby-*.gem + + - name: Upload gem artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: analytics-ruby-gem + path: analytics-ruby-*.gem + if-no-files-found: error diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..123c6a6 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,62 @@ +name: Release + +on: + release: + types: [published] + +permissions: + id-token: write + contents: read + +env: + ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }} + +jobs: + test: + name: Verify + runs-on: ubuntu-x64 + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e01a7b8b7f2007 # v1 + with: + ruby-version: '3.3' + bundler-cache: true + + - name: Authenticate with Artifactory + uses: ./.github/actions/artifactory-oidc + with: + artifactory_url: ${{ env.ARTIFACTORY_URL }} + + - name: Install dependencies + run: bundle install + + - name: Run tests + run: bundle exec rake + + deploy: + name: Publish to RubyGems + runs-on: ubuntu-x64 + needs: [test] + environment: rubygems + permissions: + id-token: write + contents: read + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e01a7b8b7f2007 # v1 + with: + ruby-version: '3.3' + + - name: Build gem + run: gem build analytics-ruby.gemspec + + - name: Publish to RubyGems (Trusted Publishing) + uses: rubygems/release-gem@612653d273a73bdba6a52d58baaac4a0d1aa8374 # v1 diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 0180b0e..f515c8e 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -12,29 +12,35 @@ on: required: false default: 'main' +permissions: + id-token: write + contents: read + +env: + ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }} + jobs: e2e-tests: - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }} steps: - name: Checkout SDK - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: path: sdk - name: Checkout sdk-e2e-tests - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: repository: segmentio/sdk-e2e-tests ref: ${{ inputs.e2e_tests_ref || 'main' }} - token: ${{ secrets.E2E_TESTS_TOKEN }} path: sdk-e2e-tests - name: Setup Ruby - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e01a7b8b7f2007 # v1 with: - ruby-version: '3.2' + ruby-version: '3.3' - name: Setup Node.js uses: actions/setup-node@v4 @@ -50,7 +56,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: e2e-test-results path: sdk-e2e-tests/test-results/ diff --git a/.gitignore b/.gitignore index ab09e0c..4d724cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ *.gem -Gemfile.lock .ruby-version coverage/ +.claude/ diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..40c20dc --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,104 @@ +PATH + remote: . + specs: + analytics-ruby (2.5.0) + +GEM + remote: http://rubygems.org/ + specs: + activesupport (5.2.8.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + ast (2.4.3) + bigdecimal (4.1.2) + commander (4.6.0) + highline (~> 2.0.0) + concurrent-ruby (1.3.7) + diff-lcs (1.6.2) + docile (1.4.1) + highline (2.0.3) + i18n (1.15.2) + concurrent-ruby (~> 1.0) + json (2.20.0) + language_server-protocol (3.17.0.6) + lint_roller (1.1.0) + minitest (5.27.0) + oj (3.17.3) + bigdecimal (>= 3.0) + ostruct (>= 0.2) + ostruct (0.6.3) + parallel (1.28.0) + parser (3.3.11.1) + ast (~> 2.4.1) + racc + prism (1.9.0) + racc (1.8.1) + rainbow (3.1.1) + rake (13.4.2) + regexp_parser (2.12.0) + rexml (3.4.4) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.7) + rubocop (1.88.2) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (>= 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.50.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + ruby-progressbar (1.13.0) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-cobertura (3.2.0) + rexml + simplecov (~> 0.19) + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) + thread_safe (0.3.6) + tzinfo (1.2.11) + thread_safe (~> 0.1) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + +PLATFORMS + arm64-darwin-24 + ruby + x86_64-linux + +DEPENDENCIES + activesupport (~> 5.2.0) + analytics-ruby! + commander (~> 4.4) + oj (~> 3.6) + rake (~> 13.0) + rspec (~> 3.0) + rubocop (~> 1.0) + simplecov + simplecov-cobertura + tzinfo (~> 1.2) + +BUNDLED WITH + 2.7.2