From 9e4882d48809be49fb7a82cc2d0b02e09f3e6ef5 Mon Sep 17 00:00:00 2001 From: Eugene Vyborov Date: Mon, 15 Jun 2026 18:11:46 +0100 Subject: [PATCH] fix(ci): gate required schema-parity & verify-non-root via changes job (#1222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both checks were required on `dev` but path-filtered via `on.pull_request.paths`, so they never posted a status on PRs that don't touch DB/docker paths — leaving the required context "expected" forever and freezing the entire dev merge queue (admin override blocked too via enforce_admins). Move the path filter from the workflow trigger to a cheap `changes` detector (dorny/paths-filter) + job-level `if:`. A job skipped via `if:` still posts a check run (conclusion: skipped), which branch protection counts as passing — so the required context is always present, while the heavy job runs only when the relevant surface changes. Intent preserved: schema-parity still blocks real schema drift on `src/backend/db/**`; verify-non-root still blocks root/socket regressions on `docker/**`. Self-merging: this PR edits both workflow files (each filter includes its own path), so both real jobs run here and post all four required contexts — no branch-protection change or admin override needed. Fixes #1222 Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/container-security.yml | 51 +++++++++++++------ .github/workflows/schema-parity.yml | 62 ++++++++++++++++-------- 2 files changed, 77 insertions(+), 36 deletions(-) diff --git a/.github/workflows/container-security.yml b/.github/workflows/container-security.yml index 31d6d35f..018e526c 100644 --- a/.github/workflows/container-security.yml +++ b/.github/workflows/container-security.yml @@ -4,10 +4,11 @@ name: container-security # # These checks used to live in `frontend-e2e.yml`, which is `ui`-label # gated and therefore skipped on backend infrastructure PRs — the exact -# PRs that can break them. This workflow runs unconditionally on any PR -# (and push to dev/main) that touches the relevant Docker / compose / -# bootstrap surface, so the guard executes whenever it could possibly -# regress. +# PRs that can break them. This workflow runs on every PR (and push to +# dev/main); the heavy `verify-non-root` job is gated by the `changes` +# detector so it executes whenever the Docker / compose / bootstrap +# surface changes and is SKIPPED (→ passing required check) otherwise. +# That keeps it a safe required status check — see #1222. # # Scope: # - `verify-non-root`: every Trinity-built service that holds platform @@ -25,19 +26,7 @@ name: container-security on: push: branches: [dev, main] - paths: - - 'docker/**' - - 'docker-compose*.yml' - - 'scripts/deploy/start.sh' - - 'src/mcp-server/Dockerfile' - - '.github/workflows/container-security.yml' pull_request: - paths: - - 'docker/**' - - 'docker-compose*.yml' - - 'scripts/deploy/start.sh' - - 'src/mcp-server/Dockerfile' - - '.github/workflows/container-security.yml' workflow_dispatch: # Least-privilege GITHUB_TOKEN: only `contents: read` is needed for @@ -48,7 +37,37 @@ permissions: contents: read jobs: + # Cheap path detector — runs on every PR/push so the required `verify-non-root` + # context is ALWAYS produced. The heavy stack-boot job below is gated on it, so + # an unrelated PR skips it (→ passing required check) rather than leaving the + # context "expected" forever (the #1222 freeze). + changes: + runs-on: ubuntu-latest + # paths-filter resolves the PR's changed-file list via the API; the heavy + # verify-non-root job keeps the workflow-level least-privilege contents: read. + permissions: + contents: read + pull-requests: read + outputs: + docker: ${{ steps.filter.outputs.docker }} + steps: + - uses: actions/checkout@v6 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + docker: + - 'docker/**' + - 'docker-compose*.yml' + - 'scripts/deploy/start.sh' + - 'src/mcp-server/Dockerfile' + - '.github/workflows/container-security.yml' + verify-non-root: + needs: changes + # Skipped (→ reported as a passing required check) when no docker/compose + # paths changed; boots the stack and verifies UIDs for real when they did. + if: needs.changes.outputs.docker == 'true' runs-on: ubuntu-latest timeout-minutes: 20 diff --git a/.github/workflows/schema-parity.yml b/.github/workflows/schema-parity.yml index 565056bd..260338dd 100644 --- a/.github/workflows/schema-parity.yml +++ b/.github/workflows/schema-parity.yml @@ -6,34 +6,56 @@ name: Schema Parity Gate # SQLite snapshots (schema-only vs full init_database lifecycle) and diffs # tables/columns/indexes/triggers. # -# MAINTAINER NOTE: do NOT add this job to required-status-checks in branch -# protection until it has been green for ~1 week of normal PR traffic. -# Otherwise any path-filter false-negative (e.g. a future module move that -# bypasses the filter) would brick PRs that don't touch DB files. The -# workflow self-skips on unrelated PRs (no path match -> no run -> no -# required check needed) so leaving it optional is safe. +# REQUIRED-CHECK SAFE (#1222): path filtering lives on the `changes` job below +# (job-level `if:`), NOT on `on.pull_request.paths`. The workflow runs on every +# PR, so the `schema-parity` context is always produced: on an unrelated PR the +# job is SKIPPED, which GitHub counts as a passing required check; when DB paths +# change it runs for real and can block. This supersedes the earlier "do not +# make this required" note — a required check filtered via `on.paths` posts NO +# status on unrelated PRs, which froze the entire dev merge queue (#1222). on: pull_request: - paths: - - 'src/backend/db/**' - - 'src/backend/database.py' - - 'src/backend/utils/helpers.py' - - 'tests/unit/test_schema_parity.py' - - 'tests/requirements-test.txt' - - '.github/workflows/schema-parity.yml' push: branches: [main, dev] - paths: - - 'src/backend/db/**' - - 'src/backend/database.py' - - 'src/backend/utils/helpers.py' - - 'tests/unit/test_schema_parity.py' - - 'tests/requirements-test.txt' - - '.github/workflows/schema-parity.yml' + workflow_dispatch: + +permissions: + contents: read jobs: + # Cheap path detector — runs on every PR/push so the required `schema-parity` + # context is ALWAYS produced. The heavy job below is gated on its output, so an + # unrelated PR skips it (→ passing required check) instead of leaving the + # context "expected" forever (the #1222 freeze). + changes: + runs-on: ubuntu-latest + # paths-filter resolves the PR's changed-file list via the API; the heavy + # schema-parity job keeps the workflow-level least-privilege contents: read. + permissions: + contents: read + pull-requests: read + outputs: + db: ${{ steps.filter.outputs.db }} + steps: + - uses: actions/checkout@v6 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + db: + - 'src/backend/db/**' + - 'src/backend/database.py' + - 'src/backend/utils/helpers.py' + - 'tests/unit/test_schema_parity.py' + - 'tests/requirements-test.txt' + - '.github/workflows/schema-parity.yml' + schema-parity: + needs: changes + # Skipped (→ reported as a passing required check) when no DB paths changed; + # runs for real and can block when they did. See #1222. + if: needs.changes.outputs.db == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6