diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f92e570..9926ec8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,35 +1,40 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#versioning-strategy +# Dependabot: security updates only (no version-update PRs). +# Grouped security PRs open weekly on Tuesday morning (Europe/Bucharest). +# Auto-merge: .github/workflows/dependabot-auto-merge.yml (on PR + Tuesday cron). +# https://docs.github.com/en/code-security/dependabot/dependabot-security-updates version: 2 updates: - # Keep the Ruby dependencies up to date - package-ecosystem: "bundler" directory: "/" schedule: - interval: "weekly" - day: "monday" - target-branch: "develop" + interval: weekly + day: tuesday + time: "06:00" + timezone: Europe/Bucharest + open-pull-requests-limit: 0 labels: + - "dependabot-security" - "ruby dependencies" groups: - bundler: - applies-to: version-updates + bundler-security: + applies-to: security-updates patterns: - "*" - # Keep the Github Actions dependencies up to date - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" - day: "monday" - target-branch: "develop" + interval: weekly + day: tuesday + time: "06:00" + timezone: Europe/Bucharest + open-pull-requests-limit: 0 labels: + - "dependabot-security" - "github dependencies" groups: - github: - applies-to: version-updates + github-actions-security: + applies-to: security-updates patterns: - "*" diff --git a/.github/workflows/auto_assign_ci.yaml b/.github/workflows/auto_assign_ci.yaml index 0e6c515..59aa47c 100644 --- a/.github/workflows/auto_assign_ci.yaml +++ b/.github/workflows/auto_assign_ci.yaml @@ -1,4 +1,5 @@ name: Auto Assign +# Audit 2026-10-14: pull_request_target — do not checkout the PR head here; configuration-path only. on: pull_request_target: @@ -16,6 +17,6 @@ jobs: name: Auto assign (me only) runs-on: ubuntu-latest steps: - - uses: kentaro-m/auto-assign-action@v2.0.2 + - uses: kentaro-m/auto-assign-action@0a2c53d3721e4c5cfcce6afd4014aecc337979f6 # v2.0.2 with: configuration-path: ".github/auto_assign.yml" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index eb0e040..27ab7f8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -61,7 +61,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` @@ -71,7 +71,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v4.37.8 + uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -100,6 +100,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.37.8 + uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..138a59d --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,50 @@ +name: Dependabot security auto-merge + +on: + pull_request: + types: [opened, synchronize, reopened] + schedule: + - cron: "0 6 * * 2" + timezone: Europe/Bucharest + +permissions: + contents: write + pull-requests: write + +jobs: + approve-and-queue-merge: + runs-on: ubuntu-latest + if: >- + github.event_name == 'pull_request' + && github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - name: Approve Dependabot pull request + run: gh pr review --approve "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable auto-merge when checks pass + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + weekly-merge-ready: + runs-on: ubuntu-latest + if: github.event_name == 'schedule' + steps: + - name: Approve and queue merge for open Dependabot PRs + run: | + set -euo pipefail + mapfile -t urls < <(gh pr list --author app/dependabot --state open --json url --jq '.[].url') + if [ "${#urls[@]}" -eq 0 ]; then + echo "No open Dependabot pull requests." + exit 0 + fi + for url in "${urls[@]}"; do + gh pr review --approve "$url" || true + gh pr merge --auto --merge "$url" || true + done + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0d4b28e..3a77cf5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,15 +13,28 @@ permissions: jobs: validate-tag: + outputs: + tag_sha: ${{ steps.tag_sha.outputs.sha }} name: Validate tag runs-on: ubuntu-latest steps: + - name: Validate tag input + env: + TAG: ${{ github.event.inputs.tag }} + run: | + re='^v?[0-9]+\.[0-9]+\.[0-9]+([.+-][0-9A-Za-z.-]+)?$' + if [[ ! "$TAG" =~ $re ]]; then + echo "Error: the tag input must be a release tag such as v1.2.3." + exit 1 + fi + - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - ref: ${{ github.event.inputs.tag }} + ref: refs/tags/${{ github.event.inputs.tag }} + persist-credentials: false - name: Verify tag and gem version run: | TAG="${{ github.event.inputs.tag }}" @@ -29,7 +42,7 @@ jobs: echo "Error: Tag is required." exit 1 fi - if ! git rev-parse "$TAG" >/dev/null 2>&1; then + if ! git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then echo "Error: Tag '$TAG' does not exist." exit 1 fi @@ -43,7 +56,14 @@ jobs: echo "Error: lib/zerobounce/version.rb ($PKG_VERSION) does not match tag ($TAG)." exit 1 fi - echo "Publishing tag: $TAG (zerobounce-sdk@$PKG_VERSION)" + + - name: Record tag commit SHA + id: tag_sha + env: + TAG: ${{ github.event.inputs.tag }} + run: | + SHA=$(git rev-parse "refs/tags/$TAG^{commit}") + echo "sha=$SHA" >> "$GITHUB_OUTPUT" test: name: Test @@ -54,12 +74,12 @@ jobs: ZEROBOUNCE_API_KEY: "invalid_key_for_tests" steps: - name: Checkout tag - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ github.event.inputs.tag }} - + ref: ${{ needs.validate-tag.outputs.tag_sha }} + persist-credentials: false - name: Set up Ruby - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1 with: ruby-version: "3.2" bundler-cache: true @@ -69,7 +89,7 @@ jobs: publish: name: Build and publish to RubyGems - needs: [test] + needs: [validate-tag, test] if: ${{ !cancelled() && !failure() }} runs-on: ubuntu-latest environment: release @@ -78,18 +98,18 @@ jobs: contents: read steps: - name: Checkout tag - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ github.event.inputs.tag }} - + ref: ${{ needs.validate-tag.outputs.tag_sha }} + persist-credentials: false - name: Set up Ruby - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1 with: ruby-version: "3.2" bundler-cache: true - name: Configure RubyGems credentials - uses: rubygems/configure-rubygems-credentials@v2.1.0 + uses: rubygems/configure-rubygems-credentials@dc5a8d8553e6ee01fc26761a49e99e733d17954a # v2.1.0 with: api-token: ${{ secrets.RUBYGEMS_API_TOKEN }} @@ -104,28 +124,29 @@ jobs: release: name: Create GitHub Release - needs: [publish] + needs: [validate-tag, publish] if: ${{ !cancelled() && !failure() }} runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ github.event.inputs.tag }} + ref: ${{ needs.validate-tag.outputs.tag_sha }} fetch-depth: 0 + persist-credentials: false - name: Create release if missing env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${{ github.event.inputs.tag }}" - if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then echo "Release $TAG already exists, skipping." else gh release create "$TAG" \ - --repo "${{ github.repository }}" \ + --repo "$GITHUB_REPOSITORY" \ --title "$TAG" \ --generate-notes fi diff --git a/.github/workflows/sdk_ci.yml b/.github/workflows/sdk_ci.yml index c44317a..49d7a27 100644 --- a/.github/workflows/sdk_ci.yml +++ b/.github/workflows/sdk_ci.yml @@ -17,10 +17,12 @@ jobs: env: ZEROBOUNCE_API_KEY: "invalid_key_for_tests" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Ruby - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1 with: ruby-version: "3.2" bundler-cache: true diff --git a/Dockerfile b/Dockerfile index d373a3c..b175471 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,4 +14,7 @@ RUN gem install bundler -v 2.4.22 && \ # Dummy key for VCR/cassette-based specs; override with env for live API ENV ZEROBOUNCE_API_KEY="${ZEROBOUNCE_API_KEY:-invalid_key_for_tests}" + +USER 65532:65532 + CMD ["bundle", "_2.4.22_", "exec", "rspec"] diff --git a/Gemfile.lock b/Gemfile.lock index eb5893b..52097f9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,6 @@ PATH remote: . specs: zerobounce-sdk (2.1.8) - dotenv rest-client (~> 2.1) GEM @@ -107,6 +106,7 @@ GEM PLATFORMS arm64-darwin-24 arm64-darwin-25 + arm64-darwin-27 x86_64-linux DEPENDENCIES @@ -114,6 +114,7 @@ DEPENDENCIES base64 bigdecimal bundler (~> 2.4.6) + dotenv logger ostruct pry (~> 0.14.1) diff --git a/lib/zerobounce/configuration.rb b/lib/zerobounce/configuration.rb index 7074711..b611560 100644 --- a/lib/zerobounce/configuration.rb +++ b/lib/zerobounce/configuration.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'rest-client' -require 'dotenv' require_relative 'api_urls' module Zerobounce @@ -30,7 +29,6 @@ class Configuration attr_accessor :headers, :apikey, :api_root_url, :bulk_api_root_url, :valid_statuses, :mock def initialize(mock = false) - File.file?('.env') ? Dotenv.load('.env') : Dotenv.load self.apikey = ENV['ZEROBOUNCE_API_KEY'] self.api_root_url = ENV['ZEROBOUNCE_API_URL'] || ApiUrls::DEFAULT_URL self.bulk_api_root_url = ENV['ZEROBOUNCE_BULK_API_URL'] || ApiUrls::BULK_DEFAULT_URL @@ -38,5 +36,14 @@ def initialize(mock = false) self.headers = { user_agent: "ZerobounceRubyGem/#{Zerobounce::VERSION}" } self.mock = mock end + + # Keep the API key out of logs, consoles and error pages that call #inspect. + def inspect + vars = instance_variables.map do |var| + value = var == :@apikey && apikey ? '[FILTERED]' : instance_variable_get(var).inspect + "#{var}=#{value}" + end + "#<#{self.class.name} #{vars.join(', ')}>" + end end end diff --git a/zerobounce.gemspec b/zerobounce.gemspec index 2ecf819..7a23a9e 100644 --- a/zerobounce.gemspec +++ b/zerobounce.gemspec @@ -15,9 +15,7 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength spec.homepage = 'https://zerobounce.net' spec.license = 'MIT' - spec.files = `git ls-files -z`.split("\x0").reject do |f| - f.match(%r{^(test|spec|features)/}) - end + spec.files = Dir['lib/**/*', 'README.md', 'LICENSE*', 'CHANGELOG*'].select { |f| File.file?(f) } spec.bindir = 'exe' spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] @@ -27,8 +25,8 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength spec.required_ruby_version = '>= 2.5' spec.add_dependency 'rest-client', '~>2.1' - spec.add_dependency 'dotenv' + spec.add_development_dependency 'dotenv' spec.add_development_dependency 'base64' # stdlib gem on Ruby 3.4+ spec.add_development_dependency 'bigdecimal' # required by crack (webmock) on Ruby 3.4+ spec.add_development_dependency 'ostruct' # stdlib gem on Ruby 3.5+ (json/simplecov load chain)