From 87417b48b221f308b2d481b1dc7d00a8ceb5d742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Brey?= Date: Fri, 6 Nov 2026 16:32:43 +0100 Subject: [PATCH] ci: merge gate that tightens once the full suite is approved (#8235) * ci: post a merge gate status that tightens once the full suite is approved Adds a ci/all-tests-gate commit status driven from CircleCI. A no-requires job posts success at pipeline start, approving the full-suite hold flips it to pending, and all-tasks-passed posts success again once every full job is green. Release pipelines post it from all-tests-succeeded so release PRs are not blocked once the context is required. The reduced suite needs no gate of its own: its jobs are already required individually. * test: find the merge gate by its instance name all-tasks-passed is now an instance of the parameterized post-gate-status job, so the workflow entry is keyed by the job name and carries name:. * ci: never post the default gate success once the full suite is requested full-suite-not-requested waits for the reduced suite and checks the hold first: approved means halt without posting, so the gate goes from unreported straight to pending; anything but on_hold fails the job rather than posting success. all-tasks-passed reaches the reduced suite through full-suite-started, so it keeps a single list, and the test guards the list that gates normal PRs. * ci: retry the gate status post A failed POST leaves the gate at its previous value: green if the pending post fails, stuck if the final success post fails. Retrying transient errors narrows both. --retry-all-errors is not used: the cimg/base curl predates it. * ci: mark the merge gate pending when the bot approves the full suite The bare @RCGitBot please test comment is the usual way to request the full suite. Posting pending from the action right after a successful approval closes the window on that path; a re-comment on an already approved hold posts nothing. Manual approvals in the CircleCI UI keep the window of one container start. * ci: pin the gate's ordering in a test and trim the fan-in list Asserts the state of each gate instance and the requires edges that keep pending after the default success and the final success last. Drops the two reduced-suite jobs all-tasks-passed still listed directly, and lets the hold check log a curl failure instead of a bare exit code. --- .circleci/default_config.yml | 105 +++++++++++++------ .github/workflows/rcgitbot_please_test.yml | 17 ++- fastlane/performance_baseline_helper_test.rb | 16 ++- 3 files changed, 104 insertions(+), 34 deletions(-) diff --git a/.circleci/default_config.yml b/.circleci/default_config.yml index 942191dfa2..3ceb05f4b2 100644 --- a/.circleci/default_config.yml +++ b/.circleci/default_config.yml @@ -2453,23 +2453,49 @@ jobs: name: Trigger RC App Pipeline command: bundle exec fastlane trigger_bump_sdk_in_rc_mobile_app - all-tests-succeeded: + post-gate-status: + parameters: + state: + type: enum + enum: [success, pending] + unless_full_suite_approved: + type: boolean + default: false docker: - image: cimg/base:stable resource_class: small steps: + - when: + condition: << parameters.unless_full_suite_approved >> + steps: + - run: + name: Halt if approve-full-tests was already approved + command: | + approval=$(curl --fail --silent --show-error --max-time 30 --retry 5 \ + -H "Circle-Token: ${CIRCLECI_API_TOKEN}" \ + "https://circleci.com/api/v2/workflow/${CIRCLE_WORKFLOW_ID}/job" \ + | jq -r '.items[] | select(.name == "approve-full-tests") | .status') + case "$approval" in + success) + echo "Full suite already requested, leaving the gate to it" + circleci-agent step halt + ;; + on_hold) ;; + *) + echo "Unexpected approve-full-tests status '${approval}', not posting" + exit 1 + ;; + esac - run: - name: All tests succeeded - command: echo "All tests succeeded" - - all-tasks-passed: - docker: - - image: cimg/base:stable - resource_class: small - steps: - - run: - name: All tasks passed - command: echo "All tasks passed" + name: Post ci/all-tests-gate status (<< parameters.state >>) + environment: + GATE_STATE: << parameters.state >> + command: | + curl --fail --silent --show-error --output /dev/null --retry 5 \ + -X POST "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/statuses/${CIRCLE_SHA1}" \ + -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + -d "{\"state\":\"${GATE_STATE}\",\"context\":\"ci/all-tests-gate\",\"target_url\":\"${CIRCLE_BUILD_URL}\"}" workflows: record-performance-baselines: @@ -2585,12 +2611,38 @@ workflows: - revenuecat-ads-tests: context: - slack-secrets + - post-gate-status: + name: full-suite-not-requested + state: success + unless_full_suite_approved: true + requires: + - performance-tests + - performance-automation-tests + - lint + - check-api-changes + - run-test-ios-27 + - build-samplecat + - build-purchase-tester + - pod-lib-lint + - run-revenuecat-ui-ios-27 + - run-revenuecat-ui-ios-26 + - emerge_purchases_ui_snapshot_tests + - binary_size_analysis + - build-tv-watch-mac-and-visionos + - revenuecat-admob-tests + - revenuecat-ads-tests # ============================================================= # Hold gate for the Full Test Suite # ============================================================= - approve-full-tests: type: approval + - post-gate-status: + name: full-suite-started + state: pending + requires: + - approve-full-tests + - full-suite-not-requested # ============================================================= # Full Test Suite: Gated behind approve-full-tests @@ -2745,22 +2797,11 @@ workflows: # ============================================================= # Summary gate: blocks merge until all jobs pass # ============================================================= - - all-tasks-passed: + - post-gate-status: + name: all-tasks-passed + state: success requires: - # Reduced Test Suite - - performance-tests - - performance-automation-tests - - lint - - check-api-changes - - run-test-ios-27 - - build-samplecat - - build-purchase-tester - - pod-lib-lint - - run-revenuecat-ui-ios-27 - - run-revenuecat-ui-ios-26 - - emerge_purchases_ui_snapshot_tests - - binary_size_analysis - - build-tv-watch-mac-and-visionos + - full-suite-started # Full Test Suite - backend-integration-tests-SK1 - backend-integration-tests-SK2 @@ -2784,8 +2825,6 @@ workflows: - api-tests - spm-receipt-parser - check-app-extension-safe-api-usage - - revenuecat-admob-tests - - revenuecat-ads-tests - deploy-purchase-tester - run-workflow-maestro-tests - run-all-maestro-e2e-tests @@ -3136,7 +3175,9 @@ workflows: # ============================================================= # All tests gate for release branches # ============================================================= - - all-tests-succeeded: + - post-gate-status: + name: all-tests-succeeded + state: success requires: - check-api-changes - run-all-maestro-e2e-tests @@ -3192,7 +3233,9 @@ workflows: - all-tests-succeeded <<: *release-branches - - all-tasks-passed: + - post-gate-status: + name: all-tasks-passed + state: success requires: - revenuecat/tag-current-branch <<: *release-branches diff --git a/.github/workflows/rcgitbot_please_test.yml b/.github/workflows/rcgitbot_please_test.yml index 68eebc29dd..1a94937086 100644 --- a/.github/workflows/rcgitbot_please_test.yml +++ b/.github/workflows/rcgitbot_please_test.yml @@ -17,6 +17,7 @@ jobs: github.event.comment.author_association == 'MEMBER' permissions: pull-requests: write + statuses: write steps: # GitHub Actions `issue_comment` workflows always run from the default branch, @@ -24,7 +25,9 @@ jobs: # the hold job in the existing workflow. - name: Get PR branch id: get-branch - run: echo "branch=$(gh pr view "$PR_NO" --repo "$REPO" --json headRefName --jq '.headRefName')" >> "$GITHUB_OUTPUT" + run: | + gh pr view "$PR_NO" --repo "$REPO" --json headRefName,headRefOid \ + --jq '"branch=\(.headRefName)\nsha=\(.headRefOid)"' >> "$GITHUB_OUTPUT" env: REPO: ${{ github.repository }} PR_NO: ${{ github.event.issue.number }} @@ -126,6 +129,18 @@ jobs: fi echo "Successfully approved '$APPROVAL_JOB_NAME' in workflow '$WORKFLOW_NAME'" + echo "approved=true" >> "$GITHUB_OUTPUT" + + - name: Mark the merge gate pending + if: steps.approve.outputs.approved == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + SHA: ${{ steps.get-branch.outputs.sha }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + gh api "repos/$REPO/statuses/$SHA" \ + -f state=pending -f context=ci/all-tests-gate -f target_url="$RUN_URL" - name: React to comment if: always() diff --git a/fastlane/performance_baseline_helper_test.rb b/fastlane/performance_baseline_helper_test.rb index 2ad9bf07b9..a5cfb1e093 100644 --- a/fastlane/performance_baseline_helper_test.rb +++ b/fastlane/performance_baseline_helper_test.rb @@ -72,8 +72,20 @@ class PerformanceBaselineHelperTest < Minitest::Test def test_pr_merge_gate_requires_the_memory_benchmark config = YAML.safe_load(File.read(File.join(ROOT, '.circleci/default_config.yml')), aliases: true) jobs = config.fetch('workflows').fetch('run-all-tests').fetch('jobs') - gate = jobs.find { |job| job.is_a?(Hash) && job.key?('all-tasks-passed') } - assert_includes gate.fetch('all-tasks-passed').fetch('requires'), 'performance-tests' + gate = jobs.find { |job| job.is_a?(Hash) && job.values.first['name'] == 'full-suite-not-requested' } + assert_includes gate.values.first.fetch('requires'), 'performance-tests' + end + + def test_merge_gate_posts_pending_after_the_default_success_and_success_last + config = YAML.safe_load(File.read(File.join(ROOT, '.circleci/default_config.yml')), aliases: true) + jobs = config.fetch('workflows').fetch('run-all-tests').fetch('jobs') + gates = jobs.select { |job| job.is_a?(Hash) && job.key?('post-gate-status') }.to_h { |job| [job['post-gate-status']['name'], job['post-gate-status']] } + assert_equal 'success', gates.fetch('full-suite-not-requested').fetch('state') + assert_equal true, gates.fetch('full-suite-not-requested').fetch('unless_full_suite_approved') + assert_equal 'pending', gates.fetch('full-suite-started').fetch('state') + assert_equal %w[approve-full-tests full-suite-not-requested], gates.fetch('full-suite-started').fetch('requires').sort + assert_equal 'success', gates.fetch('all-tasks-passed').fetch('state') + assert_includes gates.fetch('all-tasks-passed').fetch('requires'), 'full-suite-started' end def test_requested_job_enables_record_and_push_with_boolean_parameters