From c6e14724e85c59ec7733ad09c1f6a8c879f9d640 Mon Sep 17 00:00:00 2001 From: Chris Zetter <253059100+zetter-rpf@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:56:16 +0100 Subject: [PATCH 1/2] Add integration_tests:dispatch rake task Triggers the learner-experience-integration-tests GitHub Actions workflow via repository_dispatch after a deploy to the editor-api-test Heroku app, so end-to-end tests run automatically against the test environment. Errors are reported to Sentry rather than failing the deploy step. Using the release step isn't perfect as the app won't have switched over until after it completes, but since this is the last release step and the integration tests take a while to start I think this is safe. It's not easy to get the git commit message in heroku so I'm linking to the commit on github instead Co-Authored-By: Claude Sonnet 5 --- .env.example | 3 +- Procfile | 2 +- lib/tasks/integration_tests.rake | 47 +++++++++ spec/lib/tasks/integration_tests_spec.rb | 116 +++++++++++++++++++++++ 4 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 lib/tasks/integration_tests.rake create mode 100644 spec/lib/tasks/integration_tests_spec.rb diff --git a/.env.example b/.env.example index df654703d..f423df83a 100644 --- a/.env.example +++ b/.env.example @@ -73,4 +73,5 @@ PARDOT_SUBSCRIPTION_URL= # https://developers.cloudflare.com/turnstile/troubleshooting/testing/. CLOUDFLARE_TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA -HOSTNAME=localhost \ No newline at end of file +HOSTNAME=localhost +LEARNER_EXPERIENCE_TESTS_DISPATCH_TOKEN=changeme \ No newline at end of file diff --git a/Procfile b/Procfile index 394d20da5..3813fe26d 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ web: bundle exec puma -C config/puma.rb -release: bundle exec rails db:migrate && bundle exec rake projects:create_experience_cs_examples +release: bundle exec rails db:migrate && bundle exec rake projects:create_experience_cs_examples integration_tests:dispatch worker: bundle exec good_job start --max-threads=8 diff --git a/lib/tasks/integration_tests.rake b/lib/tasks/integration_tests.rake new file mode 100644 index 000000000..06faeb68d --- /dev/null +++ b/lib/tasks/integration_tests.rake @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +namespace :integration_tests do + desc 'Trigger the learner-experience-integration-tests GitHub Actions workflow against the deployed test environment' + task dispatch: :environment do + next unless on_test_app? + + response = connection.post(dispatch_url, dispatch_payload) + raise "GitHub dispatch API returned status #{response.status}" unless response.status == 204 + rescue StandardError => e + Sentry.capture_exception(e) + end + + def dispatch_url + 'https://api.github.com/repos/RaspberryPiFoundation/learner-experience-integration-tests/dispatches' + end + + def on_test_app? + ENV.fetch('HEROKU_APP_NAME') == 'editor-api-test' + end + + def connection + Faraday.new do |faraday| + faraday.request :json + faraday.headers = { + 'Accept' => 'application/vnd.github+json', + 'Authorization' => "Bearer #{ENV.fetch('LEARNER_EXPERIENCE_TESTS_DISPATCH_TOKEN')}" + } + end + end + + def dispatch_payload + sha = ENV.fetch('HEROKU_SLUG_COMMIT') + + { + event_type: 'learner-experience-test', + client_payload: { + repo: 'editor-api', + sha:, + ref: 'main', + commit_message: "https://github.com/RaspberryPiFoundation/editor-api/commit/#{sha}", + deploy_url: ENV.fetch('HOST_URL'), + triggered_at: Time.now.utc.iso8601 + } + } + end +end diff --git a/spec/lib/tasks/integration_tests_spec.rb b/spec/lib/tasks/integration_tests_spec.rb new file mode 100644 index 000000000..6ff2a89e8 --- /dev/null +++ b/spec/lib/tasks/integration_tests_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'rake' + +RSpec.describe 'integration_tests', type: :task do + describe ':dispatch' do + let(:task) { Rake::Task['integration_tests:dispatch'] } + let(:dispatch_url) { 'https://api.github.com/repos/RaspberryPiFoundation/learner-experience-integration-tests/dispatches' } + let(:heroku_app_name) { 'editor-api-test' } + let(:host_url) { 'https://test-editor-api.raspberrypi.org' } + let(:sha) { SecureRandom.hex(20) } + let(:token) { 'dispatch-token' } + let(:env) do + { + 'HEROKU_APP_NAME' => heroku_app_name, + 'HEROKU_SLUG_COMMIT' => sha, + 'HOST_URL' => host_url, + 'LEARNER_EXPERIENCE_TESTS_DISPATCH_TOKEN' => token + } + end + + around { |example| ClimateControl.modify(env) { example.run } } + + before do + stub_request(:post, dispatch_url).to_return(status: 204) + end + + context 'when HEROKU_APP_NAME is editor-api-test' do + it 'dispatches the workflow with the expected payload and auth header' do + task.invoke + + expect(WebMock).to have_requested(:post, dispatch_url) + .with( + headers: { + 'Authorization' => "Bearer #{token}", + 'Accept' => 'application/vnd.github+json' + }, + body: { + event_type: 'learner-experience-test', + client_payload: { + repo: 'editor-api', + sha:, + ref: 'main', + commit_message: "https://github.com/RaspberryPiFoundation/editor-api/commit/#{sha}", + deploy_url: host_url, + triggered_at: /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\z/ + } + } + ) + end + end + + context 'when HEROKU_APP_NAME is not editor-api-test' do + let(:heroku_app_name) { 'editor-api-staging' } + + it 'does not dispatch the workflow' do + task.invoke + + expect(WebMock).not_to have_requested(:post, dispatch_url) + end + end + + context 'when a required env var is missing' do + let(:env) { super().merge('HEROKU_SLUG_COMMIT' => nil) } + + it 'does not raise' do + expect { task.invoke }.not_to raise_error + end + + it 'reports the error to Sentry' do + allow(Sentry).to receive(:capture_exception) + + task.invoke + + expect(Sentry).to have_received(:capture_exception).with(an_instance_of(KeyError)) + end + end + + context 'when the request fails' do + before do + stub_request(:post, dispatch_url).to_return(status: 500) + end + + it 'does not raise' do + expect { task.invoke }.not_to raise_error + end + + it 'reports the error to Sentry' do + allow(Sentry).to receive(:capture_exception) + + task.invoke + + expect(Sentry).to have_received(:capture_exception).with(an_instance_of(RuntimeError)) + end + end + + context 'when an unexpected error is raised' do + before do + allow_any_instance_of(Faraday::Connection).to receive(:post).and_raise(Faraday::ConnectionFailed, 'boom') # rubocop:disable RSpec/AnyInstance + end + + it 'does not raise' do + expect { task.invoke }.not_to raise_error + end + + it 'reports the error to Sentry' do + allow(Sentry).to receive(:capture_exception) + + task.invoke + + expect(Sentry).to have_received(:capture_exception).with(an_instance_of(Faraday::ConnectionFailed)) + end + end + end +end From 5b2ae56605b4ad325904c46e8273de252d85e216 Mon Sep 17 00:00:00 2001 From: Chris Zetter <253059100+zetter-rpf@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:09:50 +0100 Subject: [PATCH 2/2] Don't double load tasks In CI the app is fully loaded already including tasks. This might cause the tasks to be loaded (and run) twice. This line still needs to be here for running tests locally. --- spec/rails_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 93941246a..528fd668d 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -116,7 +116,7 @@ end config.before(:suite) do - Rails.application.load_tasks + Rails.application.load_tasks if Rake::Task.tasks.empty? db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).first Rails.logger.debug { "Running tests in environment: #{Rails.env}" }