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.
This commit is contained in:
Álvaro Brey authored and GitHub committed 2026-11-06 15:32:43 +00:00
1 parent 0ab6c0f032
commit 87417b48b2
3 files changed
+104 -34

No files matched your search

+74 -31
View File
@@ -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
+16 -1
View File
@@ -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()
+14 -2
View File
@@ -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