Skip to content

CI Gate

CI Gate #15606

Workflow file for this run

name: CI Gate
on:
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
merge_group:
workflow_dispatch:
concurrency:
# For PRs: one run per branch; new push cancels the old run.
# For push/merge_group/workflow_dispatch: unique per run so runs never cancel each other.
# merge_group orphans from queue reshuffles are cancelled by
# cancel-orphaned-merge-queue-runs.yml when the ephemeral branch is deleted.
group: >-
${{
github.event_name == 'pull_request' &&
format('{0}-{1}', github.workflow, github.head_ref) ||
format('{0}-{1}', github.workflow, github.run_id)
}}
cancel-in-progress: true
# actions: write lets merge-queue runs call `gh run cancel` on a leaf
# failure so the gate doesn't stall waiting for still-running siblings.
# pull-requests: write lets the gate call the `dequeuePullRequest` GraphQL
# mutation on its own merge-group PR when a required check actually fails,
# since GitHub does not auto-remove UNMERGEABLE entries from the queue.
# checks: read lets the gate read a cancelled leaf's annotations, the only
# signal that separates a `timeout-minutes` kill from an external cancel.
# Reusable leaves inherit these from the caller.
permissions:
actions: write
checks: read
contents: read
pull-requests: write
jobs:
# Fast path: detect whether any non-docs files changed.
# For merge_group and workflow_dispatch always returns code=true/docs_site=true.
# Jobs guarded by `needs.changes.outputs.code != 'false'` are skipped
# for docs-only PRs, saving 20-30 min of unnecessary Go CI.
# Jobs guarded by `needs.changes.outputs.docs_site == 'true'` only run when
# docs/site/** is touched.
changes:
runs-on: ubuntu-latest
timeout-minutes: 60
outputs:
code: ${{ steps.check.outputs.code }}
docs_site: ${{ steps.check.outputs.docs_site }}
steps:
- id: check
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "code=true" >> "$GITHUB_OUTPUT"
echo "docs_site=true" >> "$GITHUB_OUTPUT"
exit 0
fi
files=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \
--paginate --jq '.[].filename') \
|| { echo "Failed to fetch PR files from GitHub API"; exit 1; }
# "code" = any file outside: docs/*, root-level llms*.txt, root-level *.md
# (nested *.md like execution/README.md still counts as a code change)
if echo "$files" | grep -qvE '^(docs/|llms[^/]*\.txt$|[^/]*\.md$)'; then
echo "code=true" >> "$GITHUB_OUTPUT"
else
echo "code=false" >> "$GITHUB_OUTPUT"
fi
if echo "$files" | grep -qE '^docs/site/'; then
echo "docs_site=true" >> "$GITHUB_OUTPUT"
else
echo "docs_site=false" >> "$GITHUB_OUTPUT"
fi
lint:
if: needs.changes.outputs.code != 'false'
needs: [changes]
uses: ./.github/workflows/lint.yml
tests:
if: needs.changes.outputs.code != 'false'
needs: [changes]
uses: ./.github/workflows/test-all-erigon.yml
race-tests:
if: needs.changes.outputs.code != 'false'
needs: [changes]
uses: ./.github/workflows/test-all-erigon-race.yml
eest-spec-tests:
if: needs.changes.outputs.code != 'false'
needs: [changes]
uses: ./.github/workflows/test-eest-spec.yml
hive:
if: needs.changes.outputs.code != 'false'
needs: [changes]
uses: ./.github/workflows/test-hive.yml
secrets: inherit
hive-eest:
if: github.event_name != 'pull_request'
uses: ./.github/workflows/test-hive-eest.yml
secrets: inherit
caplin:
if: needs.changes.outputs.code != 'false'
needs: [changes]
uses: ./.github/workflows/test-integration-caplin.yml
docs-site:
if: needs.changes.outputs.docs_site == 'true'
needs: [changes]
uses: ./.github/workflows/docs-site-build.yml
# Only meaningful for PRs: needs github.event.pull_request.base.sha.
large-files:
if: github.event_name == 'pull_request'
uses: ./.github/workflows/check-large-files.yml
bench:
if: needs.changes.outputs.code != 'false'
needs: [changes]
uses: ./.github/workflows/test-bench.yml
kurtosis:
if: needs.changes.outputs.code != 'false'
needs: [changes]
uses: ./.github/workflows/test-kurtosis-assertoor.yml
secrets: inherit
repro:
if: needs.changes.outputs.code != 'false'
needs: [changes]
uses: ./.github/workflows/reproducible-build.yml
sonar:
# SONAR_TOKEN is a repo secret unavailable to fork PRs and Dependabot PRs
# (GitHub withholds non-GITHUB_TOKEN secrets from Dependabot even for same-repo PRs),
# so skip the scan for those to avoid a guaranteed failure in ci-gate.
if: |
needs.changes.outputs.code != 'false' &&
((github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]') ||
github.event_name != 'pull_request')
needs: [changes]
uses: ./.github/workflows/sonar.yml
secrets: inherit
ci-gate:
if: always()
needs:
- changes
- lint
- tests
- race-tests
- eest-spec-tests
- hive
- hive-eest
- caplin
- docs-site
- large-files
- bench
- kurtosis
- repro
- sonar
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
# cancelled() is only valid in `if:`, so capture it as a step output for
# the gate logic below to read.
- name: Detect run cancellation
id: cancelflag
if: cancelled()
run: echo "cancelled=true" >> "$GITHUB_OUTPUT"
- name: Checkout
if: always()
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Check all required jobs
if: always()
env:
GH_TOKEN: ${{ github.token }}
NEEDS: ${{ toJSON(needs) }}
RUN_CANCELLED: ${{ steps.cancelflag.outputs.cancelled }}
run: bash .github/workflows/scripts/ci-gate-check.sh
# GitHub does not auto-remove entries left UNMERGEABLE by a failed check,
# so dequeue explicitly; the gate only fails the job on a real failure.
- name: Dequeue failed merge-queue PR
if: failure() && github.event_name == 'merge_group'
env:
GH_TOKEN: ${{ github.token }}
REF: ${{ github.ref_name }}
OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
run: |
pr_number=$(echo "$REF" | grep -oE 'pr-[0-9]+' | head -1 | cut -d- -f2)
if [ -z "$pr_number" ]; then
echo "::warning::Could not parse PR number from ref $REF"
exit 0
fi
# $owner/$name/$number/$id in the query strings are GraphQL variables, not shell.
# shellcheck disable=SC2016
pr_id=$(gh api graphql \
-F owner="$OWNER" \
-F name="$REPO_NAME" \
-F number="$pr_number" \
-f query='query($owner: String!, $name: String!, $number: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $number) { id }
}
}' --jq '.data.repository.pullRequest.id')
if [ -z "$pr_id" ] || [ "$pr_id" = "null" ]; then
echo "::warning::Could not resolve PR #$pr_number to a GraphQL node ID"
exit 0
fi
echo "Dequeuing PR #$pr_number ($pr_id) after ci-gate failure"
# shellcheck disable=SC2016
gh api graphql \
-F id="$pr_id" \
-f query='mutation($id: ID!) {
dequeuePullRequest(input: {id: $id}) {
mergeQueueEntry { position }
}
}' || echo "::warning::dequeuePullRequest mutation failed (already dequeued, or insufficient permissions)"