diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 52ba11d..356bbf1 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -2,6 +2,10 @@ name: Style Checks on: pull_request +permissions: + contents: read + pull-requests: write + jobs: rubocop: name: runner / rubocop @@ -13,7 +17,7 @@ jobs: - name: Install Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.2.2 + ruby-version: 3.4.1 - name: rubocop uses: reviewdog/action-rubocop@b6d5e953a5fc0bf3ab65254e77730ea2174d6d6d #v2.22.0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..054781d --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,50 @@ +name: Tests + +on: pull_request + +permissions: + contents: read + pull-requests: write + +jobs: + rspec: + name: rspec tests + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Install Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4.1 + bundler-cache: true + + - name: Run rspec tests + run: | + bundle exec rspec + + - name: Upload coverage results + uses: actions/upload-artifact@v4 + with: + include-hidden-files: 'true' + name: coverage-results + path: coverage + retention-days: 5 + + coverage: + needs: rspec + runs-on: ubuntu-latest + steps: + - name: Download coverage results + uses: actions/download-artifact@v4 + with: + name: coverage-results + path: coverage + + - name: Simplecov Report + uses: aki77/simplecov-report-action@7fd5fa551dd583dd437a11c640b2a1cf23d6cdaa + with: + token: ${{ secrets.GITHUB_TOKEN }} + failedThreshold: 70 + resultPath: coverage/.last_run.json diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..7a2cc1a --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--require spec_helper +--format documentation +--color diff --git a/.rubocop.yml b/.rubocop.yml index 7987fc3..740d08f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,11 +1,12 @@ AllCops: DisplayCopNames: true - TargetRubyVersion: 3.2 + TargetRubyVersion: 3.4 StyleGuideBaseURL: https://github.com/rewindio/ruby-style-configs/ NewCops: enable Exclude: - .git/**/* - bin/**/* + - lambda_layer/**/* - tmp/**/* - vendor/**/* diff --git a/.ruby-version b/.ruby-version index be94e6f..47b322c 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +3.4.1 diff --git a/Gemfile b/Gemfile index b821b67..7876d55 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,9 @@ gem 'logger-colors', '~>1.0' gem 'octokit', '~> 8.0' gem 'retriable', '~> 3.1' -group :development do +group :development, :test do + gem 'aws-sdk-ssm', '~> 1.0' + gem 'rspec', '~> 3.13' gem 'rubocop' + gem 'simplecov', require: false end diff --git a/Gemfile.lock b/Gemfile.lock index db31184..805c5c2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,62 +1,113 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.8.5) - public_suffix (>= 2.0.2, < 6.0) - ast (2.4.2) - base64 (0.2.0) + addressable (2.8.8) + public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) + aws-eventstream (1.4.0) + aws-partitions (1.1210.0) + aws-sdk-core (3.241.4) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-ssm (1.210.0) + aws-sdk-core (~> 3, >= 3.241.4) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + base64 (0.3.0) + bigdecimal (4.0.1) + diff-lcs (1.6.2) + docile (1.4.1) dotenv (2.8.1) - faraday (2.7.11) + faraday (2.14.0) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.2) + net-http (~> 0.5) + jmespath (1.6.2) + json (2.18.0) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + logger-colors (1.1.0) + logger + net-http (0.9.1) + uri (>= 0.11.1) + octokit (8.1.0) base64 - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - json (2.6.3) - language_server-protocol (3.17.0.3) - logger-colors (1.0.0) - octokit (8.0.0) faraday (>= 1, < 3) sawyer (~> 0.9) - parallel (1.23.0) - parser (3.2.2.4) + parallel (1.27.0) + parser (3.3.10.1) ast (~> 2.4.1) racc - public_suffix (5.0.4) - racc (1.7.3) + prism (1.9.0) + public_suffix (7.0.2) + racc (1.8.1) rainbow (3.1.1) - regexp_parser (2.8.2) + regexp_parser (2.11.3) retriable (3.1.2) - rexml (3.4.2) - rubocop (1.57.2) + 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.7) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.6) + rubocop (1.84.0) json (~> 2.3) - language_server-protocol (>= 3.17.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) - parser (>= 3.2.2.4) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.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, < 3.0) - rubocop-ast (1.30.0) - parser (>= 3.2.1.0) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.0) + parser (>= 3.3.7.2) + prism (~> 1.7) ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) - sawyer (0.9.2) + sawyer (0.9.3) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - unicode-display_width (2.5.0) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) PLATFORMS ruby x86_64-linux DEPENDENCIES + aws-sdk-ssm (~> 1.0) dotenv (~> 2.8) logger-colors (~> 1.0) octokit (~> 8.0) retriable (~> 3.1) + rspec (~> 3.13) rubocop + simplecov BUNDLED WITH - 2.4.22 + 2.4.10 diff --git a/README.md b/README.md index 6fb1084..e48cee0 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ To read more about how GitHub's search syntax works, see [understanding the sear ### Execution -This requires [ruby](https://www.ruby-lang.org/en/documentation/installation/) to be installed on your machine. It was tested on `Ruby 3.2.2`. Other versions may work. +This requires [ruby](https://www.ruby-lang.org/en/documentation/installation/) to be installed on your machine. It was tested on `Ruby 3.4.1`. Other versions may work. ```shell bundler install diff --git a/lambda_layer/Makefile b/lambda_layer/Makefile index ece6f3a..72ea2a2 100644 --- a/lambda_layer/Makefile +++ b/lambda_layer/Makefile @@ -1,12 +1,12 @@ # SAM does not package ruby layers correctly # AWS Lambda has a GEM_PATH of: -# /var/task/vendor/bundle/ruby/3.2.0:/opt/ruby/gems/3.2.0 -# but the layer is zipped up as: /opt/ruby/3.2.0/gems +# /var/task/vendor/bundle/ruby/3.4.0:/opt/ruby/gems/3.4.0 +# but the layer is zipped up as: /opt/ruby/3.4.0/gems # # See: https://github.com/aws/aws-lambda-builders/issues/177 # and: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/building-custom-runtimes.html -RUBY_VERSION = 3.2.0 +RUBY_VERSION = 3.4.0 mkfile_path := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) diff --git a/spec/lambda_spec.rb b/spec/lambda_spec.rb new file mode 100644 index 0000000..2150bbf --- /dev/null +++ b/spec/lambda_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'json' +require 'aws-sdk-ssm' + +# Stub the SSM client before loading lambda.rb to prevent AWS connection attempts +SSM_CLIENT_DOUBLE = Aws::SSM::Client.new(stub_responses: true) + +# Temporarily replace SSM::Client.new to return our stubbed client +original_new = Aws::SSM::Client.method(:new) +Aws::SSM::Client.define_singleton_method(:new) { |**_| SSM_CLIENT_DOUBLE } + +# Load the lambda file +lambda_file = File.expand_path('../src/lambda.rb', __dir__) +load lambda_file + +# Restore original behavior +Aws::SSM::Client.define_singleton_method(:new, original_new) + +RSpec.describe 'Lambda functions' do + let(:ssm_client) { instance_double(Aws::SSM::Client) } + + before do + stub_const('SSM_CLIENT', ssm_client) + end + + describe '#get_ssm_parameter' do + it 'retrieves a parameter with decryption enabled' do + parameter_response = instance_double( + Aws::SSM::Types::GetParameterResult, + parameter: instance_double(Aws::SSM::Types::Parameter, value: 'secret-value') + ) + + expect(ssm_client).to receive(:get_parameter).with( + name: '/test/parameter', + with_decryption: true + ).and_return(parameter_response) + + result = get_ssm_parameter('/test/parameter') + expect(result).to eq(parameter_response) + end + end + + describe '#update_ssm_parameter' do + it 'updates a parameter with overwrite enabled' do + expect(ssm_client).to receive(:put_parameter).with( + name: '/test/parameter', + overwrite: true, + value: 'new-value' + ) + + update_ssm_parameter('/test/parameter', 'new-value') + end + end + + describe '#github_token_from_ssm' do + before do + allow(ENV).to receive(:fetch).with('GITHUB_TOKEN_SSM_PATH', nil).and_return('/github/token/path') + end + + it 'returns the GitHub token from SSM' do + parameter_response = instance_double( + Aws::SSM::Types::GetParameterResult, + parameter: instance_double(Aws::SSM::Types::Parameter, value: 'ghp_test_token') + ) + + expect(ssm_client).to receive(:get_parameter).with( + name: '/github/token/path', + with_decryption: true + ).and_return(parameter_response) + + expect(github_token_from_ssm).to eq('ghp_test_token') + end + end + + describe '#last_time_checked_from_ssm' do + before do + allow(ENV).to receive(:fetch).with('LAST_TIME_CHECKED_SSM_PATH', nil).and_return('/last/checked/path') + end + + context 'when parameter has a valid timestamp' do + it 'returns the stored timestamp' do + stored_time = '2025-01-15T10:30:00.000+00:00' + parameter_response = instance_double( + Aws::SSM::Types::GetParameterResult, + parameter: instance_double(Aws::SSM::Types::Parameter, value: stored_time) + ) + + expect(ssm_client).to receive(:get_parameter).with( + name: '/last/checked/path', + with_decryption: true + ).and_return(parameter_response) + + expect(last_time_checked_from_ssm).to eq(stored_time) + end + end + + context 'when parameter value is "null"' do + it 'returns a timestamp from 1 day ago' do + parameter_response = instance_double( + Aws::SSM::Types::GetParameterResult, + parameter: instance_double(Aws::SSM::Types::Parameter, value: 'null') + ) + + expect(ssm_client).to receive(:get_parameter).with( + name: '/last/checked/path', + with_decryption: true + ).and_return(parameter_response) + + frozen_time = DateTime.new(2025, 1, 28, 12, 0, 0) + allow(DateTime).to receive(:now).and_return(frozen_time) + + result = last_time_checked_from_ssm + expected_time = (frozen_time - 1).iso8601(3) + + expect(result).to eq(expected_time) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..331880e --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'simplecov' +SimpleCov.start do + add_filter '/spec/' + enable_coverage :branch +end + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups + config.filter_run_when_matching :focus + config.disable_monkey_patching! + config.order = :random + Kernel.srand config.seed +end diff --git a/template.yml b/template.yml index a9de07a..2aae400 100644 --- a/template.yml +++ b/template.yml @@ -57,7 +57,7 @@ Resources: MemorySize: 384 ReservedConcurrentExecutions: 1 Role: !GetAtt LambdaRole.Arn - Runtime: ruby3.2 + Runtime: ruby3.4 Timeout: 300 Layers: - !Ref AuditorLambdaLayer @@ -76,7 +76,7 @@ Resources: Description: Dependencies for github-pr-auditor ContentUri: lambda_layer CompatibleRuntimes: - - ruby3.2 + - ruby3.4 RetentionPolicy: Retain Metadata: BuildMethod: makefile