Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ updates:
- "dependencies"
- "backend"
groups:
# Separate major upgrades so routine minor/patch updates can land independently.
root-backend-runtime:
patterns:
- "*"
Expand All @@ -112,6 +113,22 @@ updates:
- "stripe"
- "httpx"
- "opentelemetry-*"
update-types: ["minor", "patch"]
root-backend-major:
patterns:
- "*"
exclude-patterns:
- "cryptography"
- "pyjwt"
- "python-jose"
- "sqlalchemy"
- "alembic"
- "fastapi"
- "starlette"
- "stripe"
- "httpx"
- "opentelemetry-*"
update-types: ["major"]

# Services directories no longer have their own requirements files;
# all Python dependencies are managed at the repository root.
Expand All @@ -135,6 +152,12 @@ updates:
- "dependencies"
- "ci"
groups:
# Separate major upgrades so routine minor/patch updates can land independently.
all-actions:
patterns:
- "*"
update-types: ["minor", "patch"]
actions-major:
patterns:
- "*"
update-types: ["major"]
22 changes: 20 additions & 2 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
name: Backend Services CI/CD

on:
merge_group:
types: [checks_requested]
# Nightly run against main. The push/pull_request triggers below are
# path-filtered, so a stretch of docs- or frontend-only merges leaves the
# backend gauntlet unexercised — that let seven gates rot silently on main
Expand Down Expand Up @@ -72,6 +74,20 @@ permissions:
contents: read

jobs:
# Compile without dependencies so syntax errors fail before image startup.
compile-check:
name: Python compile check
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Compile backend Python sources
run: python -m compileall -q -j 0 -x '(^|/)(tests|node_modules)(/|$)' server services alembic plugins/fsma scripts

# ── Dependency Lock Freshness ────────────────────────────
dependency-lock:
name: Dependency Lock Freshness
Expand Down Expand Up @@ -1487,11 +1503,12 @@ jobs:
# jobs that were aborted mid-flight. `!cancelled()` does not relax the
# script below: every dependency still has to be success.
if: always() && !cancelled()
needs: [dependency-lock, test, lint, lint-shared, lint-repo-roots, ingestion-full-suite, repo-root-suite, seeded-migration-rehearsal, live-db-suite, docker-build, security, rls-fallback-check, alembic-revision-check, dep-version-check, tenant-id-uuid-check]
needs: [compile-check, dependency-lock, test, lint, lint-shared, lint-repo-roots, ingestion-full-suite, repo-root-suite, seeded-migration-rehearsal, live-db-suite, docker-build, security, rls-fallback-check, alembic-revision-check, dep-version-check, tenant-id-uuid-check]
runs-on: ubuntu-latest
steps:
- name: Check results
run: |
echo "Python compile: ${{ needs.compile-check.result }}"
echo "Dependency lock: ${{ needs.dependency-lock.result }}"
echo "Test: ${{ needs.test.result }}"
echo "Lint: ${{ needs.lint.result }}"
Expand All @@ -1511,7 +1528,8 @@ jobs:
# have caught PR #1482's NLP NameError class of bugs if this
# gate had been enforcing. Promoting lint to a hard gate so
# the next occurrence trips on PR open rather than in prod.
if [[ "${{ needs.dependency-lock.result }}" != "success" \
if [[ "${{ needs.compile-check.result }}" != "success" \
|| "${{ needs.dependency-lock.result }}" != "success" \
|| "${{ needs.test.result }}" != "success" \
|| "${{ needs.lint.result }}" != "success" \
|| "${{ needs.lint-shared.result }}" != "success" \
Expand Down
13 changes: 7 additions & 6 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ on:
# the four rollouts between 2026-07-23 and 2026-07-29 (CI + CLI paths) all
# passed /readiness on the first attempt, and per-container memory sits at
# ~1.3 GB against a 24 GB replica limit. The wait-for-ci job below still
# blocks any SHA without a green Backend CI Status Gate, and the
# cancel-in-progress concurrency group collapses push storms.
# blocks any SHA without a green Backend CI Status Gate. Railway rollouts
# are serialized at job level while CI waits run in parallel. Concurrency
# preserves the in-flight rollout and keeps only the newest pending deploy,
# collapsing push storms without cancelling railway up.
push:
branches: ["main"]
# Deploy only on backend-relevant changes. This list MUST stay a subset
Expand Down Expand Up @@ -35,10 +37,6 @@ permissions:
checks: read
contents: read

concurrency:
group: deploy-main
cancel-in-progress: true

jobs:
# Gate: manual deploys must wait for a green CI Status Gate check on the
# selected ref before Railway deployment can start.
Expand Down Expand Up @@ -79,6 +77,9 @@ jobs:

deploy:
name: Deploy RegEngine Service
concurrency:
group: deploy-main
cancel-in-progress: false
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
needs: [wait-for-ci]
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
name: Frontend CI/CD

on:
merge_group:
types: [checks_requested]
# Nightly run against main, mirroring backend-ci: the path-filtered
# triggers below leave these gates unexercised during stretches of
# backend-only merges, letting drift (promise-audit URL rot, e2e flake)
Expand Down
51 changes: 33 additions & 18 deletions .github/workflows/pr-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@
name: PR Gate

on:
merge_group:
types: [checks_requested]
pull_request:
branches: [main]

concurrency:
group: pr-gate-${{ github.event.pull_request.number }}
cancel-in-progress: true
group: pr-gate-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions: {}

Expand All @@ -67,8 +69,8 @@ jobs:
- name: Detect backend-relevant changes
id: detect
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
run: |
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import os, subprocess
Expand Down Expand Up @@ -110,7 +112,7 @@ jobs:
if: steps.detect.outputs.changed == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
Comment thread
PetrefiedThunder marked this conversation as resolved.
Resolved
CHECK_NAME: "Backend CI Status Gate"
with:
github-token: ${{ github.token }}
Expand Down Expand Up @@ -225,7 +227,7 @@ jobs:
repo,
workflow_id: 'backend-ci.yml',
head_sha: sha,
event: 'pull_request',
event: context.eventName,
per_page: 20,
})
);
Expand Down Expand Up @@ -271,12 +273,13 @@ jobs:
actions: read
name: "PR Gate: Frontend"
runs-on: ubuntu-latest
# Short bound: when the runner queue is backed up the Status Gate may not
Comment thread
PetrefiedThunder marked this conversation as resolved.
Resolved
# Short PR bound: when the runner queue is backed up the Status Gate may not
# exist yet. pr-gate-frontend-rerun.yml re-runs this job when Frontend
# CI/CD completes, so a timeout here is recovered automatically.
timeout-minutes: 15
# Merge groups cannot use the PR-only rerun helper, so wait for real CI.
timeout-minutes: ${{ github.event_name == 'merge_group' && 150 || 15 }}
concurrency:
group: pr-gate-frontend-${{ github.event.pull_request.number }}
group: pr-gate-frontend-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
Expand All @@ -285,8 +288,8 @@ jobs:
- name: Detect frontend-relevant changes
id: detect
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
run: |
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import os, subprocess
Expand Down Expand Up @@ -334,20 +337,22 @@ jobs:
if: steps.detect.outputs.changed == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
CHECK_NAME: "Frontend CI Status Gate"
with:
github-token: ${{ github.token }}
# "Frontend CI Status Gate" is created only after earlier frontend
# jobs finish. A gate that is missing or still queued is pending, not
# failed: we poll up to 12 minutes, then fail with an explicit
# failed: PRs poll up to 12 minutes (merge groups: 140), then fail with an explicit
# "still pending" message. pr-gate-frontend-rerun.yml re-runs this job
# once Frontend CI/CD completes, which then mirrors the real result.
script: |
const { owner, repo } = context.repo;
const sha = process.env.HEAD_SHA;
const CHECK_NAME = process.env.CHECK_NAME;
const deadline = Date.now() + 12 * 60 * 1000;
// Merge groups have no PR metadata for the rerun helper.
const waitMinutes = context.eventName === 'merge_group' ? 140 : 12;
const deadline = Date.now() + waitMinutes * 60 * 1000;
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const callWithRetry = async (fn) => {
Expand Down Expand Up @@ -466,9 +471,8 @@ jobs:
}

core.setFailed(
`${CHECK_NAME} is still pending (not created yet or queued) after 12 minutes; ` +
`this is a CI queue delay, not a frontend failure. This job is re-run ` +
`automatically when Frontend CI/CD completes (pr-gate-frontend-rerun.yml).`
`${CHECK_NAME} is still pending (not created yet or queued) after ${waitMinutes} minutes; ` +
`inspect the Frontend CI/CD run for this SHA.`
);
- name: No frontend-relevant changes
if: steps.detect.outputs.changed != 'true'
Expand Down Expand Up @@ -522,7 +526,7 @@ jobs:
gitleaks version
- name: Scan PR commits with gitleaks
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
run: |
gitleaks detect \
--source . \
Expand Down Expand Up @@ -572,6 +576,7 @@ jobs:

# Shared PR file detection. Required gate jobs above remain unconditional.
changes:
if: github.event_name == 'pull_request'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge queue fails required status gate

High Severity

Skipping changes on merge_group also skips jev-evidence, which has no if and needs that job. CI Status Gate still requires jev-evidence to be success and fail-closes on skipped dependencies, so every merge-queue run fails the required aggregate check and the queue cannot merge.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2929ed9. Configure here.

name: "PR Gate: Path Detection"
runs-on: ubuntu-latest
permissions:
Expand Down Expand Up @@ -630,6 +635,7 @@ jobs:
# Jobs migrated from PR Quality Gate.
# ── PR Size Check ───────────────────────────────────────
pr-size:
if: github.event_name == 'pull_request'
needs: changes
permissions:
contents: read
Expand Down Expand Up @@ -671,6 +677,7 @@ jobs:
# ── Conventional Commits ────────────────────────────────

commit-lint:
if: github.event_name == 'pull_request'
needs: changes
permissions:
contents: read
Expand Down Expand Up @@ -720,6 +727,7 @@ jobs:
# ── Copy Lint: Ban A-F Grade / Verdict UIs in /tools ────

copy-lint-tools-grade-verdicts:
if: github.event_name == 'pull_request'
needs: changes
permissions:
contents: read
Expand Down Expand Up @@ -761,6 +769,7 @@ jobs:
# ── Label PR ────────────────────────────────────────────

label:
if: github.event_name == 'pull_request'
needs: changes
permissions:
contents: read
Expand Down Expand Up @@ -789,6 +798,7 @@ jobs:
# ──────────────────────────────────────────────

supabase-patterns:
if: github.event_name == 'pull_request'
needs: changes
permissions:
contents: read
Expand Down Expand Up @@ -849,6 +859,7 @@ jobs:
# ──────────────────────────────────────────────

source-hygiene:
if: github.event_name == 'pull_request'
needs: changes
permissions:
contents: read
Expand Down Expand Up @@ -913,6 +924,7 @@ jobs:
# ──────────────────────────────────────────────

audit-trail:
if: github.event_name == 'pull_request'
needs: changes
permissions:
contents: read
Expand Down Expand Up @@ -1014,6 +1026,7 @@ jobs:
# ──────────────────────────────────────────────

sql-safety:
if: github.event_name == 'pull_request'
needs: changes
permissions:
contents: read
Expand Down Expand Up @@ -1047,6 +1060,7 @@ jobs:
# ──────────────────────────────────────────────

tenant-isolation:
if: github.event_name == 'pull_request'
needs: changes
permissions:
contents: read
Expand Down Expand Up @@ -1121,6 +1135,7 @@ jobs:
# ──────────────────────────────────────────────

react-patterns:
if: github.event_name == 'pull_request'
needs: changes
permissions:
contents: read
Expand Down
13 changes: 13 additions & 0 deletions docs/runbooks/BRANCH_PROTECTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ time out (spurious block, recoverable), while the dangerous direction —
passing an area whose real gate actually failed — only happens on
under-detection.

## Merge queue

After these workflow changes land, configure the queue as a repository ruleset
on `main`: squash merging, `ALLGREEN`, and a check timeout of 180 minutes.
Required-context workflows need `merge_group: types: [checks_requested]`
triggers, as do `backend-ci.yml` and `frontend-ci.yml` because PR Gate mirrors
their gates. Without these triggers the queue would deadlock waiting for
checks on the merge-group SHA. The workflow inventory guard enforces this.

`gh pr merge --squash --auto` enqueues the PR. To turn the queue off,
disable or delete its repository ruleset; removing workflow triggers while
the queue is enabled would leave required checks pending forever.

## Rules

### No admin-override merges
Expand Down
Loading
Loading