mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-09 22:35:56 +05:45
* 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.
273 lines
11 KiB
YAML
273 lines
11 KiB
YAML
name: "@RCGitBot please test"
|
|
|
|
on:
|
|
issue_comment:
|
|
types: [created]
|
|
|
|
# Deny-all baseline: each job must declare its own `permissions:` block.
|
|
permissions: {}
|
|
|
|
jobs:
|
|
approve-full-tests:
|
|
runs-on: ubuntu-latest
|
|
if: |
|
|
github.event.issue.pull_request &&
|
|
github.event.comment.body == '@RCGitBot please test' &&
|
|
github.repository == 'RevenueCat/purchases-ios' &&
|
|
github.event.comment.author_association == 'MEMBER'
|
|
permissions:
|
|
pull-requests: write
|
|
statuses: write
|
|
|
|
steps:
|
|
# GitHub Actions `issue_comment` workflows always run from the default branch,
|
|
# so we resolve the PR's head branch to find its CircleCI pipeline and approve
|
|
# the hold job in the existing workflow.
|
|
- name: Get PR branch
|
|
id: get-branch
|
|
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 }}
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Approve CircleCI hold job
|
|
id: approve
|
|
env:
|
|
CCI_TOKEN: ${{ secrets.CIRCLECI_TOKEN }}
|
|
BRANCH: ${{ steps.get-branch.outputs.branch }}
|
|
run: |
|
|
PROJECT_SLUG="gh/RevenueCat/purchases-ios"
|
|
WORKFLOW_NAME="run-all-tests"
|
|
APPROVAL_JOB_NAME="approve-full-tests"
|
|
|
|
# This repo uses CircleCI dynamic config: on push, a setup workflow runs
|
|
# first and generates a continuation config. The 'run-all-tests' workflow
|
|
# (and its 'approve-full-tests' hold job) only appears in the pipeline once
|
|
# setup finishes. If someone comments '@RCGitBot please test' while setup is
|
|
# still running, the workflow/approval job isn't there yet. So we poll for it
|
|
# instead of failing immediately.
|
|
# Keep total polling under ~1 minute: 4 attempts with 15s between them = ~45s
|
|
# of waiting (plus request time).
|
|
MAX_ATTEMPTS=4
|
|
SLEEP_SECONDS=15
|
|
|
|
# 1. Get the most recent pipeline for this branch
|
|
PIPELINE_ID=$(curl -s --max-time 15 -G \
|
|
-H "Circle-Token: $CCI_TOKEN" \
|
|
--data-urlencode "branch=$BRANCH" \
|
|
"https://circleci.com/api/v2/project/$PROJECT_SLUG/pipeline" \
|
|
| jq -r '.items[0].id')
|
|
|
|
if [[ -z "$PIPELINE_ID" || "$PIPELINE_ID" == "null" ]]; then
|
|
echo "::error::No CircleCI pipeline found for branch '$BRANCH'"
|
|
exit 1
|
|
fi
|
|
echo "Found pipeline: $PIPELINE_ID"
|
|
|
|
# 2. Poll for the run-all-tests workflow and its approval job. Both may be
|
|
# missing while the setup workflow is still generating the continuation config.
|
|
WORKFLOW_ID=""
|
|
APPROVAL_REQUEST_ID=""
|
|
for ((attempt=1; attempt<=MAX_ATTEMPTS; attempt++)); do
|
|
WORKFLOW_ID=$(curl -s --max-time 15 \
|
|
-H "Circle-Token: $CCI_TOKEN" \
|
|
"https://circleci.com/api/v2/pipeline/$PIPELINE_ID/workflow" \
|
|
| jq -r --arg name "$WORKFLOW_NAME" '.items[] | select(.name == $name) | .id')
|
|
|
|
if [[ -n "$WORKFLOW_ID" && "$WORKFLOW_ID" != "null" ]]; then
|
|
echo "Found workflow: $WORKFLOW_ID"
|
|
|
|
JOBS_RESPONSE=$(curl -s --max-time 15 \
|
|
-H "Circle-Token: $CCI_TOKEN" \
|
|
"https://circleci.com/api/v2/workflow/$WORKFLOW_ID/job")
|
|
|
|
JOB_STATUS=$(echo "$JOBS_RESPONSE" | jq -r --arg name "$APPROVAL_JOB_NAME" \
|
|
'.items[] | select(.name == $name and .type == "approval") | .status')
|
|
|
|
if [[ "$JOB_STATUS" == "success" ]]; then
|
|
echo "Hold job '$APPROVAL_JOB_NAME' was already approved"
|
|
exit 0
|
|
fi
|
|
|
|
APPROVAL_REQUEST_ID=$(echo "$JOBS_RESPONSE" | jq -r --arg name "$APPROVAL_JOB_NAME" \
|
|
'.items[] | select(.name == $name and .type == "approval") | .approval_request_id')
|
|
|
|
if [[ -n "$APPROVAL_REQUEST_ID" && "$APPROVAL_REQUEST_ID" != "null" ]]; then
|
|
break
|
|
fi
|
|
fi
|
|
|
|
if [[ "$attempt" -lt "$MAX_ATTEMPTS" ]]; then
|
|
echo "Attempt $attempt/$MAX_ATTEMPTS: '$WORKFLOW_NAME' workflow / '$APPROVAL_JOB_NAME' job not ready yet (setup may still be running). Retrying in ${SLEEP_SECONDS}s..."
|
|
sleep "$SLEEP_SECONDS"
|
|
fi
|
|
done
|
|
|
|
if [[ -z "$WORKFLOW_ID" || "$WORKFLOW_ID" == "null" ]]; then
|
|
echo "::error::No '$WORKFLOW_NAME' workflow found in pipeline $PIPELINE_ID after $MAX_ATTEMPTS attempts"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "$APPROVAL_REQUEST_ID" || "$APPROVAL_REQUEST_ID" == "null" ]]; then
|
|
echo "::error::Approval job '$APPROVAL_JOB_NAME' not found in workflow $WORKFLOW_ID after $MAX_ATTEMPTS attempts"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Found approval request: $APPROVAL_REQUEST_ID"
|
|
|
|
# 3. Approve it
|
|
HTTP_STATUS=$(curl -s --max-time 15 -o /dev/null -w "%{http_code}" -X POST \
|
|
-H "Circle-Token: $CCI_TOKEN" \
|
|
"https://circleci.com/api/v2/workflow/$WORKFLOW_ID/approve/$APPROVAL_REQUEST_ID")
|
|
|
|
if [[ "$HTTP_STATUS" != "202" ]]; then
|
|
echo "::error::Failed to approve job. CircleCI returned HTTP $HTTP_STATUS"
|
|
exit 1
|
|
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()
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
JOB_STATUS: ${{ job.status }}
|
|
REPO: ${{ github.repository }}
|
|
COMMENT_ID: ${{ github.event.comment.id }}
|
|
run: |
|
|
if [[ "$JOB_STATUS" == "success" ]]; then
|
|
REACTION="+1"
|
|
else
|
|
REACTION="-1"
|
|
fi
|
|
gh api "repos/$REPO/issues/comments/$COMMENT_ID/reactions" \
|
|
-f content="$REACTION"
|
|
|
|
# Trigger CircleCI job(s) on-demand via "@RCGitBot please test <job1> <job2> <job3>".
|
|
# Triggers a new CircleCI pipeline; the setup config generates a continuation
|
|
# config containing only a workflow with the requested job(s).
|
|
run-specific-jobs:
|
|
runs-on: ubuntu-latest
|
|
if: |
|
|
github.event.issue.pull_request &&
|
|
startsWith(github.event.comment.body, '@RCGitBot please test ') &&
|
|
github.repository == 'RevenueCat/purchases-ios' &&
|
|
github.event.comment.author_association == 'MEMBER'
|
|
permissions:
|
|
pull-requests: write
|
|
|
|
steps:
|
|
- name: Parse job names
|
|
id: parse
|
|
env:
|
|
COMMENT: ${{ github.event.comment.body }}
|
|
run: |
|
|
# GHA's startsWith() is case-insensitive, so lowercase the comment before stripping
|
|
# the prefix. Then use the length to slice the original, preserving job name case
|
|
# (e.g. backend-integration-tests-SK1).
|
|
PREFIX="@rcgitbot please test "
|
|
COMMENT_LOWER="$(echo "$COMMENT" | tr '[:upper:]' '[:lower:]')"
|
|
if [[ "$COMMENT_LOWER" != "$PREFIX"* ]]; then
|
|
echo "::error::Comment does not start with '@RCGitBot please test '"
|
|
exit 1
|
|
fi
|
|
JOBS_RAW="${COMMENT:${#PREFIX}}"
|
|
# Trim whitespace
|
|
JOBS_RAW=$(echo "$JOBS_RAW" | xargs)
|
|
if [[ -z "$JOBS_RAW" ]]; then
|
|
echo "::error::No job name(s) provided"
|
|
exit 1
|
|
fi
|
|
# Validate: only alphanumeric, hyphens, underscores, and spaces
|
|
if [[ ! "$JOBS_RAW" =~ ^[a-zA-Z0-9_[:space:]-]+$ ]]; then
|
|
echo "::error::Invalid input: $JOBS_RAW (only alphanumeric, hyphens, underscores, and spaces allowed)"
|
|
exit 1
|
|
fi
|
|
echo "job_names=$JOBS_RAW" >> "$GITHUB_OUTPUT"
|
|
echo "Parsed job(s): $JOBS_RAW"
|
|
|
|
- name: Get PR branch
|
|
id: get-branch
|
|
run: echo "branch=$(gh pr view "$PR_NO" --repo "$REPO" --json headRefName --jq '.headRefName')" >> "$GITHUB_OUTPUT"
|
|
env:
|
|
REPO: ${{ github.repository }}
|
|
PR_NO: ${{ github.event.issue.number }}
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Trigger CircleCI pipeline
|
|
id: trigger
|
|
env:
|
|
CCI_TOKEN: ${{ secrets.CIRCLECI_TOKEN }}
|
|
BRANCH: ${{ steps.get-branch.outputs.branch }}
|
|
JOB_NAMES: ${{ steps.parse.outputs.job_names }}
|
|
run: |
|
|
PROJECT_SLUG="gh/RevenueCat/purchases-ios"
|
|
|
|
# Build the payload with jq rather than string interpolation: branch names come
|
|
# from the PR head ref and may contain characters that need JSON escaping.
|
|
BODY_JSON=$(jq -n --arg branch "$BRANCH" --arg jobs "$JOB_NAMES" \
|
|
'{branch: $branch, parameters: {requested_jobs: $jobs}}')
|
|
|
|
RESPONSE=$(curl -s --max-time 15 -w "\n%{http_code}" -X POST \
|
|
-H "Circle-Token: $CCI_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$BODY_JSON" \
|
|
"https://circleci.com/api/v2/project/$PROJECT_SLUG/pipeline")
|
|
|
|
HTTP_STATUS=$(echo "$RESPONSE" | tail -1)
|
|
BODY=$(echo "$RESPONSE" | head -n -1)
|
|
|
|
if [[ "$HTTP_STATUS" != "201" ]]; then
|
|
echo "::error::Failed to trigger pipeline. CircleCI returned HTTP $HTTP_STATUS"
|
|
echo "$BODY"
|
|
exit 1
|
|
fi
|
|
|
|
PIPELINE_ID=$(echo "$BODY" | jq -r '.id')
|
|
PIPELINE_NUMBER=$(echo "$BODY" | jq -r '.number')
|
|
echo "pipeline_number=$PIPELINE_NUMBER" >> "$GITHUB_OUTPUT"
|
|
echo "Triggered pipeline #$PIPELINE_NUMBER (id: $PIPELINE_ID) for job(s) '$JOB_NAMES' on branch '$BRANCH'"
|
|
|
|
- name: Post pipeline link
|
|
if: success()
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
PIPELINE_NUMBER: ${{ steps.trigger.outputs.pipeline_number }}
|
|
JOB_NAMES: ${{ steps.parse.outputs.job_names }}
|
|
REPO: ${{ github.repository }}
|
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
run: |
|
|
gh api "repos/$REPO/issues/$ISSUE_NUMBER/comments" \
|
|
-f body="🚀 Triggered **$JOB_NAMES** → [Pipeline #$PIPELINE_NUMBER](https://app.circleci.com/pipelines/gh/RevenueCat/purchases-ios/$PIPELINE_NUMBER)"
|
|
|
|
- name: React to comment
|
|
if: always()
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
JOB_STATUS: ${{ job.status }}
|
|
REPO: ${{ github.repository }}
|
|
COMMENT_ID: ${{ github.event.comment.id }}
|
|
run: |
|
|
if [[ "$JOB_STATUS" == "success" ]]; then
|
|
REACTION="+1"
|
|
else
|
|
REACTION="-1"
|
|
fi
|
|
gh api "repos/$REPO/issues/comments/$COMMENT_ID/reactions" \
|
|
-f content="$REACTION"
|