From 3039f8dc861a2d1566cb1d32ed04b87e5d3a071e Mon Sep 17 00:00:00 2001 From: Shabini Rajadas Date: Wed, 10 Dec 2025 13:36:10 -0800 Subject: [PATCH 01/11] Added specs to access secrets in CI pipeline --- .../project_secret_ci_access_spec.rb | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb diff --git a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb new file mode 100644 index 00000000000000..d12b65ee7805da --- /dev/null +++ b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +module QA + RSpec.describe( + 'Software Supply Chain Security', + :secrets_manager, + only: { job: 'gdk-instance-secrets-manager' }, + feature_category: :secrets_management + ) do + include_context 'secrets manager base' + + describe 'Project Secret CI Access' do + let(:secret_name) { 'Test' } + let(:secret_value) { 'my-secret-value-for-ci' } + let(:ci_file_content) do + <<~YAML + job: + secrets: + TEST_SECRET: + gitlab_secrets_manager: + name: #{secret_name} + script: + - echo "testing OpenBao in CI" + - cat $TEST_SECRET | wc + - echo "done." + YAML + end + + before do + response = project.send(:put, project.send(:request_url, project.api_put_path), shared_runners_enabled: true) + raise "Failed to enable shared runners: #{response.body}" unless response.code == 200 + + runners_response = project.send(:get, project.send(:request_url, '/runners', scope: 'online')) + available_runners = JSON.parse(runners_response.body) + online_runner = available_runners.first + + if online_runner + project.send(:post, project.send(:request_url, project.api_runners_path), + runner_id: online_runner['id']) + else + skip "No online runners available in the environment" + end + + Page::Main::Menu.perform(&:sign_out) + Flow::Login.sign_in(as: owner) + project.visit! + + Page::Project::Menu.perform(&:go_to_secrets_manager) + EE::Page::Project::Secure::SecretsManager.perform do |secrets_page| + secrets_page.click_new_secret + secrets_page.create_secret( + name: secret_name, + value: secret_value, + description: "Secret for CI pipeline test", + environment: '*', + branch: 'main' + ) + end + end + + def create_or_update_ci_file(content, commit_message) + file_exists = project.has_file?('.gitlab-ci.yml') + + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.api_client = Runtime::User::Store.admin_api_client + commit.project = project + commit.commit_message = commit_message + + if file_exists + commit.update_files([{ file_path: '.gitlab-ci.yml', content: content }]) + else + commit.add_files([{ file_path: '.gitlab-ci.yml', content: content }]) + end + end + + project.commits.first[:id] + end + + def wait_for_pipeline_completion(commit_sha) + pipeline = project.wait_for_pipeline(ref: project.default_branch, sha: commit_sha) + expect(pipeline).not_to be_nil, "Pipeline was not created" + + Support::Waiter.wait_until(max_duration: 300, sleep_interval: 5) do + project.pipelines.first[:status].in?(%w[success failed]) + end + end + + context 'when accessing secrets in CI pipeline' do + it 'successfully accesses secret in CI pipeline job', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/583388' do + latest_commit_sha = create_or_update_ci_file(ci_file_content, 'Add CI config to test secrets access') + wait_for_pipeline_completion(latest_commit_sha) + project.visit_latest_pipeline + + Page::Project::Pipeline::Show.perform do |pipeline_page| + expect(pipeline_page).to have_job('job') + pipeline_page.click_job('job') + end + + Page::Project::Job::Show.perform do |job_page| + expect(job_page.successful?(timeout: 120)).to be_truthy, "Job did not complete successfully" + job_log = job_page.output(wait: 10) + expect(job_log).to include('testing OpenBao in CI') + expect(job_log).to include('done.') + expect(job_log).to include('Job succeeded') + expect(job_log).to match(/\s+\d+\s+\d+\s+#{secret_value.length}/) + end + end + + it 'fails when accessing non-existent secret in CI pipeline job', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/583389' do + nonexistent_secret_ci_config = <<~YAML + job: + secrets: + TEST_SECRET: + gitlab_secrets_manager: + name: non_existent_secret + script: + - echo "Attempting to access non-existent secret" + - test -n "$TEST_SECRET" || exit 1 + - cat $TEST_SECRET + - echo "This should not be reached" + YAML + + latest_commit_sha = create_or_update_ci_file(nonexistent_secret_ci_config, 'Test non-existent secret access') + wait_for_pipeline_completion(latest_commit_sha) + project.visit_latest_pipeline + + Page::Project::Pipeline::Show.perform do |pipeline_page| + expect(pipeline_page).to have_job('job') + pipeline_page.click_job('job') + end + + Page::Project::Job::Show.perform do |job_page| + expect(job_page.has_job_log?(wait: 60)).to be_truthy, "Job log did not appear" + + job_log = job_page.output(wait: 10) + expect(job_log).to include('Job failed') + end + end + end + end + end +end -- GitLab From b053cc9f64df4373e84a4d67a0ecd5fa9182054a Mon Sep 17 00:00:00 2001 From: Shabini Rajadas Date: Wed, 10 Dec 2025 17:21:10 -0800 Subject: [PATCH 02/11] MR review suggestions --- .../secrets_management/project_secret_ci_access_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb index d12b65ee7805da..ecbbe8a3254985 100644 --- a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb +++ b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb @@ -21,8 +21,8 @@ module QA name: #{secret_name} script: - echo "testing OpenBao in CI" - - cat $TEST_SECRET | wc - - echo "done." + - cat $TEST_SECRET | wc | grep '22$' + - echo "This is my secret: $(cat $TEST_SECRET)" YAML end -- GitLab From 06e53a85c18e7b1a22ec9d16b489ee33653cf790 Mon Sep 17 00:00:00 2001 From: Shabini Rajadas Date: Thu, 11 Dec 2025 12:58:26 -0800 Subject: [PATCH 03/11] Duo suggestion --- .../secrets_management/project_secret_ci_access_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb index ecbbe8a3254985..c54f212729a6e4 100644 --- a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb +++ b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb @@ -21,7 +21,7 @@ module QA name: #{secret_name} script: - echo "testing OpenBao in CI" - - cat $TEST_SECRET | wc | grep '22$' + - cat $TEST_SECRET | wc | grep '21$' - echo "This is my secret: $(cat $TEST_SECRET)" YAML end -- GitLab From a0ccb59efa80c8c409199f98ab19f4c06426f086 Mon Sep 17 00:00:00 2001 From: Shabini Rajadas Date: Thu, 11 Dec 2025 16:00:36 -0800 Subject: [PATCH 04/11] Added a dedicated runner for the test --- .../project_secret_ci_access_spec.rb | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb index c54f212729a6e4..87f53bdde895f0 100644 --- a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb +++ b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb @@ -12,9 +12,23 @@ module QA describe 'Project Secret CI Access' do let(:secret_name) { 'Test' } let(:secret_value) { 'my-secret-value-for-ci' } + let(:executor) { "qa-runner-#{SecureRandom.hex(6)}" } + let(:runner_tag) { "secrets-test-#{SecureRandom.hex(4)}" } + + # Create a dedicated runner for this test + let!(:runner) do + Resource::Ci::ProjectRunner.fabricate_via_api! do |r| + r.project = project + r.name = executor + r.tags = [runner_tag] + end + end + let(:ci_file_content) do <<~YAML job: + tags: + - #{runner_tag} secrets: TEST_SECRET: gitlab_secrets_manager: @@ -26,21 +40,11 @@ module QA YAML end - before do - response = project.send(:put, project.send(:request_url, project.api_put_path), shared_runners_enabled: true) - raise "Failed to enable shared runners: #{response.body}" unless response.code == 200 - - runners_response = project.send(:get, project.send(:request_url, '/runners', scope: 'online')) - available_runners = JSON.parse(runners_response.body) - online_runner = available_runners.first - - if online_runner - project.send(:post, project.send(:request_url, project.api_runners_path), - runner_id: online_runner['id']) - else - skip "No online runners available in the environment" - end + after do + runner&.remove_via_api! + end + before do Page::Main::Menu.perform(&:sign_out) Flow::Login.sign_in(as: owner) project.visit! @@ -111,6 +115,8 @@ def wait_for_pipeline_completion(commit_sha) testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/583389' do nonexistent_secret_ci_config = <<~YAML job: + tags: + - #{runner_tag} secrets: TEST_SECRET: gitlab_secrets_manager: -- GitLab From 379852dfbfa13bbdc52881aba91260633c648b7f Mon Sep 17 00:00:00 2001 From: Shabini Rajadas Date: Thu, 11 Dec 2025 17:03:52 -0800 Subject: [PATCH 05/11] Updated the runners in spec --- .../secrets_management/project_secret_ci_access_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb index 87f53bdde895f0..5835262a50c51b 100644 --- a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb +++ b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb @@ -18,6 +18,7 @@ module QA # Create a dedicated runner for this test let!(:runner) do Resource::Ci::ProjectRunner.fabricate_via_api! do |r| + r.api_client = Runtime::User::Store.admin_api_client r.project = project r.name = executor r.tags = [runner_tag] -- GitLab From f34c24064bfd9741b96cdbf3050227c7acdba8b8 Mon Sep 17 00:00:00 2001 From: Shabini Rajadas Date: Mon, 15 Dec 2025 17:49:48 -0800 Subject: [PATCH 06/11] Removed runners from spec --- .../project_secret_ci_access_spec.rb | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb index 5835262a50c51b..fcac6557d0e798 100644 --- a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb +++ b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb @@ -12,24 +12,10 @@ module QA describe 'Project Secret CI Access' do let(:secret_name) { 'Test' } let(:secret_value) { 'my-secret-value-for-ci' } - let(:executor) { "qa-runner-#{SecureRandom.hex(6)}" } - let(:runner_tag) { "secrets-test-#{SecureRandom.hex(4)}" } - - # Create a dedicated runner for this test - let!(:runner) do - Resource::Ci::ProjectRunner.fabricate_via_api! do |r| - r.api_client = Runtime::User::Store.admin_api_client - r.project = project - r.name = executor - r.tags = [runner_tag] - end - end let(:ci_file_content) do <<~YAML job: - tags: - - #{runner_tag} secrets: TEST_SECRET: gitlab_secrets_manager: -- GitLab From 29edeb6b7d0ae10e305b864e8681bc0b6fba695c Mon Sep 17 00:00:00 2001 From: Shabini Rajadas Date: Mon, 15 Dec 2025 18:55:46 -0800 Subject: [PATCH 07/11] Removed Runner references --- .../secrets_management/project_secret_ci_access_spec.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb index fcac6557d0e798..a30e732d672b52 100644 --- a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb +++ b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb @@ -27,10 +27,6 @@ module QA YAML end - after do - runner&.remove_via_api! - end - before do Page::Main::Menu.perform(&:sign_out) Flow::Login.sign_in(as: owner) @@ -102,8 +98,6 @@ def wait_for_pipeline_completion(commit_sha) testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/583389' do nonexistent_secret_ci_config = <<~YAML job: - tags: - - #{runner_tag} secrets: TEST_SECRET: gitlab_secrets_manager: -- GitLab From b90811a7ba6155e0d1beb058cf514990b4333cde Mon Sep 17 00:00:00 2001 From: Shabini Rajadas Date: Tue, 16 Dec 2025 13:11:14 -0800 Subject: [PATCH 08/11] Updated spec --- .../project_secret_ci_access_spec.rb | 50 +++++++------------ 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb index a30e732d672b52..cac985a37f4e10 100644 --- a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb +++ b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb @@ -45,38 +45,18 @@ module QA end end - def create_or_update_ci_file(content, commit_message) - file_exists = project.has_file?('.gitlab-ci.yml') - - Resource::Repository::Commit.fabricate_via_api! do |commit| - commit.api_client = Runtime::User::Store.admin_api_client - commit.project = project - commit.commit_message = commit_message - - if file_exists - commit.update_files([{ file_path: '.gitlab-ci.yml', content: content }]) - else - commit.add_files([{ file_path: '.gitlab-ci.yml', content: content }]) - end - end - - project.commits.first[:id] - end - - def wait_for_pipeline_completion(commit_sha) - pipeline = project.wait_for_pipeline(ref: project.default_branch, sha: commit_sha) - expect(pipeline).not_to be_nil, "Pipeline was not created" - - Support::Waiter.wait_until(max_duration: 300, sleep_interval: 5) do - project.pipelines.first[:status].in?(%w[success failed]) - end + def add_ci_file(content, commit_message) + create(:commit, project: project, commit_message: commit_message, actions: [ + { action: 'create', file_path: '.gitlab-ci.yml', content: content } + ]) + Flow::Pipeline.wait_for_pipeline_creation_via_api(project: project) + Flow::Pipeline.wait_for_latest_pipeline_to_have_status(project: project, status: 'success') end context 'when accessing secrets in CI pipeline' do it 'successfully accesses secret in CI pipeline job', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/583388' do - latest_commit_sha = create_or_update_ci_file(ci_file_content, 'Add CI config to test secrets access') - wait_for_pipeline_completion(latest_commit_sha) + add_ci_file(ci_file_content, 'Add CI config to test secrets access') project.visit_latest_pipeline Page::Project::Pipeline::Show.perform do |pipeline_page| @@ -87,10 +67,15 @@ def wait_for_pipeline_completion(commit_sha) Page::Project::Job::Show.perform do |job_page| expect(job_page.successful?(timeout: 120)).to be_truthy, "Job did not complete successfully" job_log = job_page.output(wait: 10) - expect(job_log).to include('testing OpenBao in CI') - expect(job_log).to include('done.') - expect(job_log).to include('Job succeeded') - expect(job_log).to match(/\s+\d+\s+\d+\s+#{secret_value.length}/) + + aggregate_failures 'Job output verification' do + expect(job_log).to include('testing OpenBao in CI') + expect(job_log).to include('done.') + expect(job_log).to include('Job succeeded') + expect(job_log).to match(/\s+\d+\s+\d+\s+#{secret_value.length}/) + expect(job_log).not_to include(secret_value), "Secret value should be masked in logs" + expect(job_log).to include('***'), "Secret should be masked with asterisks in logs" + end end end @@ -109,8 +94,7 @@ def wait_for_pipeline_completion(commit_sha) - echo "This should not be reached" YAML - latest_commit_sha = create_or_update_ci_file(nonexistent_secret_ci_config, 'Test non-existent secret access') - wait_for_pipeline_completion(latest_commit_sha) + add_ci_file(nonexistent_secret_ci_config, 'Test non-existent secret access') project.visit_latest_pipeline Page::Project::Pipeline::Show.perform do |pipeline_page| -- GitLab From 9228e62066d3880deef3d632fe5dd4f7a830d15e Mon Sep 17 00:00:00 2001 From: Shabini Rajadas Date: Tue, 16 Dec 2025 16:25:22 -0800 Subject: [PATCH 09/11] Updated spec --- .../project_secret_ci_access_spec.rb | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb index cac985a37f4e10..27fe5a6caab1e9 100644 --- a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb +++ b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb @@ -46,9 +46,20 @@ module QA end def add_ci_file(content, commit_message) - create(:commit, project: project, commit_message: commit_message, actions: [ - { action: 'create', file_path: '.gitlab-ci.yml', content: content } - ]) + file_exists = project.has_file?('.gitlab-ci.yml') + + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.api_client = Runtime::User::Store.admin_api_client + commit.project = project + commit.commit_message = commit_message + + if file_exists + commit.update_files([{ file_path: '.gitlab-ci.yml', content: content }]) + else + commit.add_files([{ file_path: '.gitlab-ci.yml', content: content }]) + end + end + Flow::Pipeline.wait_for_pipeline_creation_via_api(project: project) Flow::Pipeline.wait_for_latest_pipeline_to_have_status(project: project, status: 'success') end -- GitLab From 02bf18b86564bcbcec94548fe2e7da92abaa43e4 Mon Sep 17 00:00:00 2001 From: Shabini Rajadas Date: Wed, 17 Dec 2025 17:30:22 -0800 Subject: [PATCH 10/11] debugging ci pipeline in test --- .../project_secret_ci_access_spec.rb | 181 +++++++++++++++++- 1 file changed, 175 insertions(+), 6 deletions(-) diff --git a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb index 27fe5a6caab1e9..48cb2fe44416b6 100644 --- a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb +++ b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb @@ -28,6 +28,9 @@ module QA end before do + # Debug: Check runner availability FIRST + check_runners_before_test + Page::Main::Menu.perform(&:sign_out) Flow::Login.sign_in(as: owner) project.visit! @@ -45,7 +48,75 @@ module QA end end - def add_ci_file(content, commit_message) + # Add this helper method + def check_runners_before_test + QA::Runtime::Logger.info("=" * 80) + QA::Runtime::Logger.info("CHECKING RUNNER AVAILABILITY") + QA::Runtime::Logger.info("=" * 80) + + # Check instance-level runners via API + instance_runners = get_instance_runners + QA::Runtime::Logger.info("Instance runners: #{instance_runners.count}") + instance_runners.each do |runner| + QA::Runtime::Logger.info(" - #{runner[:description]} (ID: #{runner[:id]})") + log_runner_info(runner) + QA::Runtime::Logger.info(" Tags: #{runner[:tag_list].inspect}") + QA::Runtime::Logger.info(" Run untagged: #{runner[:run_untagged]}") + end + + # Check project-specific runners + project_runners = get_project_runners + QA::Runtime::Logger.info("Project runners: #{project_runners.count}") + project_runners.each do |runner| + QA::Runtime::Logger.info(" - #{runner[:description]} (ID: #{runner[:id]})") + log_runner_info(runner) + QA::Runtime::Logger.info(" Tags: #{runner[:tag_list].inspect}") + QA::Runtime::Logger.info(" Run untagged: #{runner[:run_untagged]}") + end + + # Check if project can use any runners + total_available = instance_runners.count + project_runners.count + online_runners = (instance_runners + project_runners).count { |r| r[:online] && r[:active] } + + QA::Runtime::Logger.info("=" * 80) + QA::Runtime::Logger.info("SUMMARY: #{total_available} total runners, #{online_runners} online and active") + QA::Runtime::Logger.info("=" * 80) + + return unless online_runners == 0 + + raise "NO RUNNERS AVAILABLE! Cannot execute CI pipeline tests. " \ + "The GDK instance needs at least one registered, active, and online runner." + end + + def log_runner_info(runner) + status = runner[:status] + active = runner[:active] + online = runner[:online] + QA::Runtime::Logger.info(" Status: #{status}, Active: #{active}, Online: #{online}") + end + + def get_instance_runners + response = get Runtime::API::Request.new(api_client, '/runners/all').url + parse_body(response) + end + + def get_project_runners + response = get Runtime::API::Request.new(api_client, "/projects/#{project.id}/runners").url + parse_body(response) + end + + def api_client + @api_client ||= Runtime::API::Client.as_admin + end + + def add_ci_file(content, commit_message, expected_status: 'success') + create_ci_file_commit(content, commit_message) + Flow::Pipeline.wait_for_pipeline_creation_via_api(project: project) + log_pipeline_status + wait_for_pipeline_with_diagnostics(expected_status) + end + + def create_ci_file_commit(content, commit_message) file_exists = project.has_file?('.gitlab-ci.yml') Resource::Repository::Commit.fabricate_via_api! do |commit| @@ -59,15 +130,102 @@ def add_ci_file(content, commit_message) commit.add_files([{ file_path: '.gitlab-ci.yml', content: content }]) end end + end - Flow::Pipeline.wait_for_pipeline_creation_via_api(project: project) - Flow::Pipeline.wait_for_latest_pipeline_to_have_status(project: project, status: 'success') + def log_pipeline_status + pipeline = project.pipelines.first + QA::Runtime::Logger.info("Pipeline created: #{pipeline.web_url}") + QA::Runtime::Logger.info("Initial status: #{pipeline.status}") + + sleep 5 + pipeline.reload! + QA::Runtime::Logger.info("Status after 5s: #{pipeline.status}") + + return unless pipeline.status == 'created' || pipeline.status == 'pending' + + QA::Runtime::Logger.error("Pipeline is stuck in '#{pipeline.status}' status!") + QA::Runtime::Logger.error("This means no runner is picking up the job.") + log_job_details(pipeline.id) + QA::Runtime::Logger.error("Re-checking runners...") + check_runners_before_test + end + + def log_job_details(pipeline_id) + jobs = get_pipeline_jobs(pipeline_id) + jobs.each do |job| + QA::Runtime::Logger.error("Job '#{job[:name]}':") + QA::Runtime::Logger.error(" Status: #{job[:status]}") + QA::Runtime::Logger.error(" Tags: #{job[:tag_list].inspect}") + QA::Runtime::Logger.error(" Runner: #{job[:runner].inspect}") + end + end + + def wait_for_pipeline_with_diagnostics(expected_status) + Flow::Pipeline.wait_for_latest_pipeline_to_have_status( + project: project, + status: expected_status + ) + rescue StandardError => e + log_pipeline_diagnostics + raise e + end + + def log_pipeline_diagnostics + QA::Runtime::Logger.error("=" * 80) + QA::Runtime::Logger.error("PIPELINE TIMEOUT DIAGNOSTICS") + QA::Runtime::Logger.error("=" * 80) + + pipeline = project.pipelines.first + pipeline.reload! + QA::Runtime::Logger.error("Final pipeline status: #{pipeline.status}") + QA::Runtime::Logger.error("Pipeline URL: #{pipeline.web_url}") + + jobs = get_pipeline_jobs(pipeline.id) + jobs.each do |job| + log_job_diagnostics(job) + end + end + + def log_job_diagnostics(job) + QA::Runtime::Logger.error("\nJob '#{job[:name]}':") + QA::Runtime::Logger.error(" ID: #{job[:id]}") + QA::Runtime::Logger.error(" Status: #{job[:status]}") + QA::Runtime::Logger.error(" Tags: #{job[:tag_list].inspect}") + QA::Runtime::Logger.error(" Created: #{job[:created_at]}") + QA::Runtime::Logger.error(" Started: #{job[:started_at]}") + QA::Runtime::Logger.error(" Runner: #{job[:runner].inspect}") + QA::Runtime::Logger.error(" Failure reason: #{job[:failure_reason]}") + + log_job_trace(job[:id]) + end + + def log_job_trace(job_id) + trace = get_job_trace(job_id) + if trace && !trace.empty? + QA::Runtime::Logger.error(" Trace:\n#{trace}") + else + QA::Runtime::Logger.error(" Trace: (empty or not available)") + end + rescue StandardError => e + QA::Runtime::Logger.error(" Could not fetch trace: #{e.message}") + end + + def get_pipeline_jobs(pipeline_id) + response = get Runtime::API::Request.new(api_client, + "/projects/#{project.id}/pipelines/#{pipeline_id}/jobs").url + parse_body(response) + end + + def get_job_trace(job_id) + response = get Runtime::API::Request.new(api_client, "/projects/#{project.id}/jobs/#{job_id}/trace").url + response.body end context 'when accessing secrets in CI pipeline' do it 'successfully accesses secret in CI pipeline job', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/583388' do - add_ci_file(ci_file_content, 'Add CI config to test secrets access') + add_ci_file(ci_file_content, 'Add CI config to test secrets access', expected_status: 'success') + project.visit_latest_pipeline Page::Project::Pipeline::Show.perform do |pipeline_page| @@ -105,7 +263,19 @@ def add_ci_file(content, commit_message) - echo "This should not be reached" YAML - add_ci_file(nonexistent_secret_ci_config, 'Test non-existent secret access') + # Don't use add_ci_file helper since this pipeline SHOULD fail + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.api_client = Runtime::User::Store.admin_api_client + commit.project = project + commit.commit_message = 'Test non-existent secret access' + commit.add_files([{ file_path: '.gitlab-ci.yml', content: nonexistent_secret_ci_config }]) + end + + Flow::Pipeline.wait_for_pipeline_creation_via_api(project: project) + + # Expect FAILED status, not success + Flow::Pipeline.wait_for_latest_pipeline_to_have_status(project: project, status: 'failed') + project.visit_latest_pipeline Page::Project::Pipeline::Show.perform do |pipeline_page| @@ -115,7 +285,6 @@ def add_ci_file(content, commit_message) Page::Project::Job::Show.perform do |job_page| expect(job_page.has_job_log?(wait: 60)).to be_truthy, "Job log did not appear" - job_log = job_page.output(wait: 10) expect(job_log).to include('Job failed') end -- GitLab From ce3e1a388dbafe9d05c6bccf9837661f4aad33c8 Mon Sep 17 00:00:00 2001 From: Shabini Rajadas Date: Wed, 17 Dec 2025 18:32:29 -0800 Subject: [PATCH 11/11] spec changes --- .../project_secret_ci_access_spec.rb | 208 ++++++++++-------- 1 file changed, 112 insertions(+), 96 deletions(-) diff --git a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb index 48cb2fe44416b6..1f7be060356826 100644 --- a/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb +++ b/qa/qa/specs/features/ee/browser_ui/10_software_supply_chain_security/secrets_management/project_secret_ci_access_spec.rb @@ -48,72 +48,85 @@ module QA end end - # Add this helper method def check_runners_before_test + log_runner_check_header + admin_client = Runtime::API::Client.as_admin + + instance_runners = fetch_instance_runners(admin_client) + project_runners = fetch_project_runners(admin_client) + + log_runners_info(instance_runners, project_runners) + validate_runners_available(instance_runners, project_runners) + end + + def log_runner_check_header QA::Runtime::Logger.info("=" * 80) QA::Runtime::Logger.info("CHECKING RUNNER AVAILABILITY") QA::Runtime::Logger.info("=" * 80) + end - # Check instance-level runners via API - instance_runners = get_instance_runners - QA::Runtime::Logger.info("Instance runners: #{instance_runners.count}") - instance_runners.each do |runner| - QA::Runtime::Logger.info(" - #{runner[:description]} (ID: #{runner[:id]})") - log_runner_info(runner) - QA::Runtime::Logger.info(" Tags: #{runner[:tag_list].inspect}") - QA::Runtime::Logger.info(" Run untagged: #{runner[:run_untagged]}") - end + def fetch_instance_runners(admin_client) + response = admin_client.get("/runners/all") + parse_body(response) + end + + def fetch_project_runners(admin_client) + response = admin_client.get("/projects/#{project.id}/runners") + parse_body(response) + end + + def log_runners_info(instance_runners, project_runners) + log_runner_list("Instance runners", instance_runners) + log_runner_list("Project runners", project_runners) + log_runner_summary(instance_runners, project_runners) + end - # Check project-specific runners - project_runners = get_project_runners - QA::Runtime::Logger.info("Project runners: #{project_runners.count}") - project_runners.each do |runner| + def log_runner_list(label, runners) + QA::Runtime::Logger.info("#{label}: #{runners.count}") + runners.each do |runner| QA::Runtime::Logger.info(" - #{runner[:description]} (ID: #{runner[:id]})") - log_runner_info(runner) + log_runner_status(runner) QA::Runtime::Logger.info(" Tags: #{runner[:tag_list].inspect}") QA::Runtime::Logger.info(" Run untagged: #{runner[:run_untagged]}") end - - # Check if project can use any runners - total_available = instance_runners.count + project_runners.count - online_runners = (instance_runners + project_runners).count { |r| r[:online] && r[:active] } - - QA::Runtime::Logger.info("=" * 80) - QA::Runtime::Logger.info("SUMMARY: #{total_available} total runners, #{online_runners} online and active") - QA::Runtime::Logger.info("=" * 80) - - return unless online_runners == 0 - - raise "NO RUNNERS AVAILABLE! Cannot execute CI pipeline tests. " \ - "The GDK instance needs at least one registered, active, and online runner." end - def log_runner_info(runner) + def log_runner_status(runner) status = runner[:status] active = runner[:active] online = runner[:online] QA::Runtime::Logger.info(" Status: #{status}, Active: #{active}, Online: #{online}") end - def get_instance_runners - response = get Runtime::API::Request.new(api_client, '/runners/all').url - parse_body(response) - end + def log_runner_summary(instance_runners, project_runners) + total = instance_runners.count + project_runners.count + online = (instance_runners + project_runners).count { |r| r[:online] && r[:active] } - def get_project_runners - response = get Runtime::API::Request.new(api_client, "/projects/#{project.id}/runners").url - parse_body(response) + QA::Runtime::Logger.info("=" * 80) + QA::Runtime::Logger.info("SUMMARY: #{total} total runners, #{online} online and active") + QA::Runtime::Logger.info("=" * 80) end - def api_client - @api_client ||= Runtime::API::Client.as_admin + def validate_runners_available(instance_runners, project_runners) + online_runners = (instance_runners + project_runners).count { |r| r[:online] && r[:active] } + + return unless online_runners == 0 + + raise "NO RUNNERS AVAILABLE! Cannot execute CI pipeline tests. " \ + "The GDK instance needs at least one registered, active, and online runner." end def add_ci_file(content, commit_message, expected_status: 'success') create_ci_file_commit(content, commit_message) Flow::Pipeline.wait_for_pipeline_creation_via_api(project: project) - log_pipeline_status - wait_for_pipeline_with_diagnostics(expected_status) + + admin_client = Runtime::API::Client.as_admin + pipeline = fetch_latest_pipeline(admin_client) + + log_initial_pipeline_status(pipeline) + check_pipeline_stuck_status(admin_client, pipeline) + + wait_for_pipeline_completion(expected_status, admin_client, pipeline[:id]) end def create_ci_file_commit(content, commit_message) @@ -132,26 +145,40 @@ def create_ci_file_commit(content, commit_message) end end - def log_pipeline_status - pipeline = project.pipelines.first - QA::Runtime::Logger.info("Pipeline created: #{pipeline.web_url}") - QA::Runtime::Logger.info("Initial status: #{pipeline.status}") + def fetch_latest_pipeline(admin_client) + response = admin_client.get("/projects/#{project.id}/pipelines") + pipelines = parse_body(response) + pipelines.first + end + def log_initial_pipeline_status(pipeline) + QA::Runtime::Logger.info("Pipeline created: ID=#{pipeline[:id]}, Status=#{pipeline[:status]}") sleep 5 - pipeline.reload! - QA::Runtime::Logger.info("Status after 5s: #{pipeline.status}") - return unless pipeline.status == 'created' || pipeline.status == 'pending' + admin_client = Runtime::API::Client.as_admin + response = admin_client.get("/projects/#{project.id}/pipelines/#{pipeline[:id]}") + updated_pipeline = parse_body(response) + QA::Runtime::Logger.info("Status after 5s: #{updated_pipeline[:status]}") + end + + def check_pipeline_stuck_status(admin_client, pipeline) + response = admin_client.get("/projects/#{project.id}/pipelines/#{pipeline[:id]}") + current_pipeline = parse_body(response) + + return unless current_pipeline[:status] == 'created' || current_pipeline[:status] == 'pending' - QA::Runtime::Logger.error("Pipeline is stuck in '#{pipeline.status}' status!") + QA::Runtime::Logger.error("Pipeline is stuck in '#{current_pipeline[:status]}' status!") QA::Runtime::Logger.error("This means no runner is picking up the job.") - log_job_details(pipeline.id) + + log_stuck_pipeline_jobs(admin_client, pipeline[:id]) QA::Runtime::Logger.error("Re-checking runners...") check_runners_before_test end - def log_job_details(pipeline_id) - jobs = get_pipeline_jobs(pipeline_id) + def log_stuck_pipeline_jobs(admin_client, pipeline_id) + response = admin_client.get("/projects/#{project.id}/pipelines/#{pipeline_id}/jobs") + jobs = parse_body(response) + jobs.each do |job| QA::Runtime::Logger.error("Job '#{job[:name]}':") QA::Runtime::Logger.error(" Status: #{job[:status]}") @@ -160,65 +187,54 @@ def log_job_details(pipeline_id) end end - def wait_for_pipeline_with_diagnostics(expected_status) + def wait_for_pipeline_completion(expected_status, _admin_client, pipeline_id) Flow::Pipeline.wait_for_latest_pipeline_to_have_status( project: project, status: expected_status ) rescue StandardError => e - log_pipeline_diagnostics + log_pipeline_diagnostics(pipeline_id) raise e end - def log_pipeline_diagnostics + def log_pipeline_diagnostics(pipeline_id) + admin_client = Runtime::API::Client.as_admin + QA::Runtime::Logger.error("=" * 80) QA::Runtime::Logger.error("PIPELINE TIMEOUT DIAGNOSTICS") QA::Runtime::Logger.error("=" * 80) - pipeline = project.pipelines.first - pipeline.reload! - QA::Runtime::Logger.error("Final pipeline status: #{pipeline.status}") - QA::Runtime::Logger.error("Pipeline URL: #{pipeline.web_url}") - - jobs = get_pipeline_jobs(pipeline.id) - jobs.each do |job| - log_job_diagnostics(job) - end - end + pipeline_response = admin_client.get("/projects/#{project.id}/pipelines/#{pipeline_id}") + pipeline = parse_body(pipeline_response) + QA::Runtime::Logger.error("Final pipeline status: #{pipeline[:status]}") + QA::Runtime::Logger.error("Pipeline web_url: #{pipeline[:web_url]}") - def log_job_diagnostics(job) - QA::Runtime::Logger.error("\nJob '#{job[:name]}':") - QA::Runtime::Logger.error(" ID: #{job[:id]}") - QA::Runtime::Logger.error(" Status: #{job[:status]}") - QA::Runtime::Logger.error(" Tags: #{job[:tag_list].inspect}") - QA::Runtime::Logger.error(" Created: #{job[:created_at]}") - QA::Runtime::Logger.error(" Started: #{job[:started_at]}") - QA::Runtime::Logger.error(" Runner: #{job[:runner].inspect}") - QA::Runtime::Logger.error(" Failure reason: #{job[:failure_reason]}") - - log_job_trace(job[:id]) - end + jobs_response = admin_client.get("/projects/#{project.id}/pipelines/#{pipeline_id}/jobs") + jobs = parse_body(jobs_response) - def log_job_trace(job_id) - trace = get_job_trace(job_id) - if trace && !trace.empty? - QA::Runtime::Logger.error(" Trace:\n#{trace}") - else - QA::Runtime::Logger.error(" Trace: (empty or not available)") + jobs.each do |job| + QA::Runtime::Logger.error("\nJob '#{job[:name]}':") + QA::Runtime::Logger.error(" ID: #{job[:id]}") + QA::Runtime::Logger.error(" Status: #{job[:status]}") + QA::Runtime::Logger.error(" Tags: #{job[:tag_list].inspect}") + QA::Runtime::Logger.error(" Created: #{job[:created_at]}") + QA::Runtime::Logger.error(" Started: #{job[:started_at]}") + QA::Runtime::Logger.error(" Runner: #{job[:runner].inspect}") + QA::Runtime::Logger.error(" Failure reason: #{job[:failure_reason]}") + + # Try to get trace + begin + trace_response = admin_client.get("/projects/#{project.id}/jobs/#{job[:id]}/trace") + trace = trace_response.body + if trace && !trace.empty? + QA::Runtime::Logger.error(" Trace:\n#{trace}") + else + QA::Runtime::Logger.error(" Trace: (empty or not available)") + end + rescue StandardError => trace_error + QA::Runtime::Logger.error(" Could not fetch trace: #{trace_error.message}") + end end - rescue StandardError => e - QA::Runtime::Logger.error(" Could not fetch trace: #{e.message}") - end - - def get_pipeline_jobs(pipeline_id) - response = get Runtime::API::Request.new(api_client, - "/projects/#{project.id}/pipelines/#{pipeline_id}/jobs").url - parse_body(response) - end - - def get_job_trace(job_id) - response = get Runtime::API::Request.new(api_client, "/projects/#{project.id}/jobs/#{job_id}/trace").url - response.body end context 'when accessing secrets in CI pipeline' do @@ -263,7 +279,7 @@ def get_job_trace(job_id) - echo "This should not be reached" YAML - # Don't use add_ci_file helper since this pipeline SHOULD fail + # Manually create commit and wait for FAILED status Resource::Repository::Commit.fabricate_via_api! do |commit| commit.api_client = Runtime::User::Store.admin_api_client commit.project = project -- GitLab