From e4966e491f96ee9eee6fc65423564fe0e6928ea9 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 14 Oct 2026 13:57:39 +0700 Subject: [PATCH 1/3] ci: stop WIP runner flood; gate heavy draft checks, scope firmware pushes, package only on demand --- .github/workflows/arstack-studio-qt.yml | 6 +++- .github/workflows/arstack-studio-release.yml | 8 ------ .../arstack-studio-windows-lifecycle.yml | 4 ++- .github/workflows/brcb-hard-profile-ci.yml | 4 ++- .github/workflows/cpp-ci.yml | 16 ++++++++++- .github/workflows/embedded-ci.yml | 6 +++- .github/workflows/esp32p4-ptp-lab.yml | 4 +++ .github/workflows/esp32p4-smv-injector.yml | 4 +++ .../ied-simulator-release-hardening.yml | 5 +++- .../workflows/interoperability-server-ci.yml | 3 ++ .github/workflows/ptp-core-ci.yml | 7 ++++- .github/workflows/security-evidence.yml | 14 +++++++++- docs/CI_WORKFLOW_ROUTING.md | 22 +++++++++++---- scripts/check_ci_trigger_policy.py | 28 ++++++++++++++++--- 14 files changed, 105 insertions(+), 26 deletions(-) diff --git a/.github/workflows/arstack-studio-qt.yml b/.github/workflows/arstack-studio-qt.yml index fa82bec12..4c73d8205 100644 --- a/.github/workflows/arstack-studio-qt.yml +++ b/.github/workflows/arstack-studio-qt.yml @@ -10,6 +10,7 @@ on: - 'include/ariec61850/scl/**' - '.github/workflows/arstack-studio-qt.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'apps/arstack_studio/**' - 'include/ariec61850/sampled_values/**' @@ -22,10 +23,11 @@ permissions: concurrency: group: arstack-studio-qt-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} jobs: windows-launcher-parse: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: windows-latest steps: - name: Checkout @@ -54,6 +56,7 @@ jobs: Write-Host 'Windows PowerShell parse: PASS · launcher + S8B packaged ownership probe' windows-ownership-integration: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: S8B Windows ownership integration runs-on: windows-2022 steps: @@ -124,6 +127,7 @@ jobs: retention-days: 7 qt-build-smoke: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: ubuntu-latest container: ubuntu:26.04 steps: diff --git a/.github/workflows/arstack-studio-release.yml b/.github/workflows/arstack-studio-release.yml index 028bb807d..f09db9b9d 100644 --- a/.github/workflows/arstack-studio-release.yml +++ b/.github/workflows/arstack-studio-release.yml @@ -2,14 +2,6 @@ name: ARStack Studio Release on: workflow_dispatch: - pull_request: - paths: - - 'apps/arstack_studio/**' - - 'embedded/esp32p4_smv_injector/**' - - 'include/ariec61850/**' - - 'src/**' - - '.github/workflows/arstack-studio-release.yml' - - '.github/workflows/esp32p4-smv-injector.yml' push: tags: - 'v*' diff --git a/.github/workflows/arstack-studio-windows-lifecycle.yml b/.github/workflows/arstack-studio-windows-lifecycle.yml index 9639712fe..bd324d9dc 100644 --- a/.github/workflows/arstack-studio-windows-lifecycle.yml +++ b/.github/workflows/arstack-studio-windows-lifecycle.yml @@ -7,6 +7,7 @@ on: - 'apps/arstack_studio/**' - '.github/workflows/arstack-studio-windows-lifecycle.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'apps/arstack_studio/**' - '.github/workflows/arstack-studio-windows-lifecycle.yml' @@ -16,10 +17,11 @@ permissions: concurrency: group: arstack-studio-windows-lifecycle-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} jobs: close-exit-reopen: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: Close -> process exit -> immediate reopen runs-on: windows-2022 timeout-minutes: 12 diff --git a/.github/workflows/brcb-hard-profile-ci.yml b/.github/workflows/brcb-hard-profile-ci.yml index d1638cf18..8789a3b2c 100644 --- a/.github/workflows/brcb-hard-profile-ci.yml +++ b/.github/workflows/brcb-hard-profile-ci.yml @@ -5,6 +5,7 @@ on: branches: - main pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'embedded/brcb_hard_profile/**' - 'embedded/brcb_*.cpp' @@ -23,10 +24,11 @@ permissions: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} jobs: brcb-hard-profile: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: BRCB Hard / ${{ matrix.compiler }} runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/cpp-ci.yml b/.github/workflows/cpp-ci.yml index 975c373de..cbf43d6e7 100644 --- a/.github/workflows/cpp-ci.yml +++ b/.github/workflows/cpp-ci.yml @@ -3,7 +3,19 @@ name: C++ CI on: push: branches: [main] + paths: + - 'CMakeLists.txt' + - 'cmake/**' + - 'include/**' + - 'src/**' + - 'tests/**' + - 'tools/**' + - 'scripts/**' + - 'fuzz/**' + - 'embedded/**' + - '.github/workflows/cpp-ci.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'CMakeLists.txt' - 'cmake/**' @@ -22,10 +34,11 @@ permissions: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} jobs: linux: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: Linux / ${{ matrix.compiler }} runs-on: ubuntu-latest strategy: @@ -68,6 +81,7 @@ jobs: python3 scripts/run-phase4c-timeout-recovery.py --help > /dev/null windows: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: Windows / MSVC runs-on: windows-latest steps: diff --git a/.github/workflows/embedded-ci.yml b/.github/workflows/embedded-ci.yml index 54ef8eae4..2a4bced83 100644 --- a/.github/workflows/embedded-ci.yml +++ b/.github/workflows/embedded-ci.yml @@ -5,6 +5,7 @@ on: branches: - main pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'embedded/**' - 'include/ariec61850/mms/**' @@ -27,15 +28,18 @@ on: - 'src/time_sync/**' - '.github/workflows/embedded-ci.yml' + workflow_dispatch: + permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} jobs: embedded-host-sim: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: Embedded host-sim / ${{ matrix.compiler }} runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/esp32p4-ptp-lab.yml b/.github/workflows/esp32p4-ptp-lab.yml index 035a032b0..66031bc18 100644 --- a/.github/workflows/esp32p4-ptp-lab.yml +++ b/.github/workflows/esp32p4-ptp-lab.yml @@ -1,7 +1,9 @@ name: ESP32-P4 PTP Lab Profile on: + workflow_dispatch: push: + branches: [main] paths: - 'embedded/esp32p4_smv_injector/**' - 'include/ariec61850/time_sync/**' @@ -10,6 +12,7 @@ on: - 'src/ethernet/**' - '.github/workflows/esp32p4-ptp-lab.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'embedded/esp32p4_smv_injector/**' - 'include/ariec61850/time_sync/**' @@ -27,6 +30,7 @@ concurrency: jobs: build-esp32p4-ptp: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} strategy: fail-fast: false matrix: diff --git a/.github/workflows/esp32p4-smv-injector.yml b/.github/workflows/esp32p4-smv-injector.yml index fbd885134..4f96e9fd9 100644 --- a/.github/workflows/esp32p4-smv-injector.yml +++ b/.github/workflows/esp32p4-smv-injector.yml @@ -2,7 +2,9 @@ name: ESP32-P4 SMV Injector on: workflow_call: + workflow_dispatch: push: + branches: [main] paths: - 'embedded/esp32p4_smv_injector/**' - 'include/ariec61850/asn1/**' @@ -17,6 +19,7 @@ on: - 'src/sampled_values/**' - '.github/workflows/esp32p4-smv-injector.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'embedded/esp32p4_smv_injector/**' - 'include/ariec61850/asn1/**' @@ -44,6 +47,7 @@ env: jobs: build-esp32p4: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: ubuntu-latest timeout-minutes: 25 container: diff --git a/.github/workflows/ied-simulator-release-hardening.yml b/.github/workflows/ied-simulator-release-hardening.yml index da767ee20..5b20a1e68 100644 --- a/.github/workflows/ied-simulator-release-hardening.yml +++ b/.github/workflows/ied-simulator-release-hardening.yml @@ -9,6 +9,7 @@ on: - 'assets/**' - '.github/workflows/ied-simulator-release-hardening.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'apps/ied_simulator/packaging/**' - 'apps/ied_simulator/CMakeLists.txt' @@ -21,10 +22,11 @@ permissions: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} jobs: linux-release-soak: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -73,6 +75,7 @@ jobs: --cycles 12 windows-release-package: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: windows-2025 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/interoperability-server-ci.yml b/.github/workflows/interoperability-server-ci.yml index b9230a4d6..0f298820e 100644 --- a/.github/workflows/interoperability-server-ci.yml +++ b/.github/workflows/interoperability-server-ci.yml @@ -2,6 +2,7 @@ name: IEC 61850 Interoperability Server CI on: pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'tools/static_ied_server_iedsim/**' - 'tools/static_ied_server.cpp' @@ -22,6 +23,7 @@ concurrency: jobs: linux: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} strategy: matrix: compiler: [gcc, clang] @@ -45,6 +47,7 @@ jobs: run: ctest --test-dir build-interop-parity --output-on-failure windows: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: windows-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/ptp-core-ci.yml b/.github/workflows/ptp-core-ci.yml index 0ea016870..12b44f610 100644 --- a/.github/workflows/ptp-core-ci.yml +++ b/.github/workflows/ptp-core-ci.yml @@ -20,6 +20,7 @@ on: - 'tests/ptp_profile/**' - '.github/workflows/ptp-core-ci.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'include/ariec61850/time_sync/**' - 'src/time_sync/**' @@ -37,15 +38,18 @@ on: - 'tests/ptp_profile/**' - '.github/workflows/ptp-core-ci.yml' + workflow_dispatch: + permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} jobs: portable-ptp: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} strategy: fail-fast: false matrix: @@ -76,6 +80,7 @@ jobs: run: ctest --test-dir build-ptp-${{ matrix.compiler }} --output-on-failure portable-ptp-msvc: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: windows-latest steps: - name: Checkout diff --git a/.github/workflows/security-evidence.yml b/.github/workflows/security-evidence.yml index ec69f2782..906909835 100644 --- a/.github/workflows/security-evidence.yml +++ b/.github/workflows/security-evidence.yml @@ -3,7 +3,17 @@ name: Security and Evidence on: push: branches: [main] + paths: + - 'CMakeLists.txt' + - 'cmake/**' + - 'include/**' + - 'src/**' + - 'tests/**' + - 'tools/**' + - 'fuzz/**' + - '.github/workflows/security-evidence.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'CMakeLists.txt' - 'cmake/**' @@ -20,10 +30,11 @@ permissions: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} jobs: sanitizers: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: Clang / ASan + UBSan runs-on: ubuntu-latest env: @@ -45,6 +56,7 @@ jobs: run: ctest --test-dir build-sanitizers --output-on-failure fuzz-smoke: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: Clang / libFuzzer corpus smoke runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/docs/CI_WORKFLOW_ROUTING.md b/docs/CI_WORKFLOW_ROUTING.md index 808ed79b3..a05d5c197 100644 --- a/docs/CI_WORKFLOW_ROUTING.md +++ b/docs/CI_WORKFLOW_ROUTING.md @@ -1,6 +1,6 @@ # CI routing contract — ARStack61850 -The CI contract is **risk-scoped PR checks, full integrated `main` checks, and manual/on-demand release packaging**. A new feature commit must never trigger unrelated domain matrices. +The CI contract is **draft PRs do not consume heavy runner minutes, targeted ready-for-review gates, risk-scoped `main` integration, and manual/tag-only release packaging**. A new feature commit must never trigger unrelated domain matrices. ## Pull request matrix @@ -13,13 +13,23 @@ The CI contract is **risk-scoped PR checks, full integrated `main` checks, and m | control interoperability sources | C++ CI, Security and Evidence | Control Interop Harness | | embedded sources or embedded-facing protocol core | Embedded Profile CI | Other firmware / protocol-specific workflows through their own paths | -- **PR**: path-scoped. Superseded PR heads are cancelled using workflow-specific concurrency groups. -- **Feature branch push**: no duplicate workflow run when an equivalent PR run already exists. The patched workflows run on push to `main` only (historic explicit special branches remain where applicable). -- **`main`**: preserve full integration CI where configured. It is not path-trimmed to optimize feature iteration. +- **Draft PR**: expensive C++ / sanitizers / MSVC / Qt / ESP-IDF / PTP / interop jobs are **skipped**. A Draft can be updated without restarting expensive runs. **Ready PR**: path-scoped jobs run on ready_for_review and synchronize. Superseded PR heads are cancelled. +- **Feature branch push**: no repeated ESP-IDF/Windows builds. Only selected explicit historic special branches retain their own scoped push gates. Favor a Draft PR until an acceptance checkpoint, then mark Ready or manually dispatch the relevant test. +- **`main`**: production integration is path-scoped (including C++ and security) and superseded heavy runs may be cancelled as the branch advances. Keep the newest HEAD green before tagging. - **Studio / PTP / legacy SMV GUI**: feature branch push does not duplicate the PR validation; push is scoped to `main`. -- **ESP32-P4 firmware / PTP lab**: feature-branch push still builds artifacts before a PR exists. Push and PR for the *same repository+branch* use a shared concurrency key, cancelling superseded feature runs without cancelling integrated `main` evidence. -- **Windows Workbench release package**: run automatically on release-impacting PRs and integrated `main` changes, or manually with `workflow_dispatch` for an explicit bench/release candidate. Normal QML-only PR iteration should not package an installer on every commit. +- **ESP32-P4 firmware / PTP lab**: feature pushes do **not** run ESP-IDF builds. Run on scoped ready PRs, `main` changes, or intentionally from `workflow_dispatch` on the exact branch/commit for hardware testing. +- **ARStack Studio release package**: only `workflow_dispatch` for explicit test installers or `v*` tags for official release. PR changes do **not** trigger full installer packaging. ARStack Studio Qt smoke runs on relevant ready PRs. - **Before merging a release candidate**: explicitly run the Windows packaging/release acceptance workflow on the exact release candidate head, in addition to scoped PR CI. No hardware proof is implied by CI. - **Workflow files themselves**: are included in their own path filters so trigger changes receive acceptance runs. The lightweight CI Routing Policy gate checks trigger invariants. Do not broaden unrelated PR `paths` to `apps/ied_simulator/**` or remove concurrency without updating `scripts/check_ci_trigger_policy.py`. Batch related changes into one commit before asking CI to validate; prefer failed-job reruns rather than repushing formatting edits. + +## Engineering checkpoint protocol + +1. Work in Draft PR without repetitive workflow runs for each edited file. Validate formatting and small tests locally or at a deliberate checkpoint. +2. Run a **single targeted workflow_dispatch on the exact feature ref** for an early firmware artifact; explicitly name the commit SHA in test evidence. +3. Mark PR Ready only when code is coherent. Relevant path-scoped GCC/Clang/MSVC, firmware, security and Qt checks then run; avoid pointless pushes to wake runners. +4. Build the full Windows installer **only on demand or tag**, never for every protocol diff. +5. Merge only after relevant required checks at the current head and investigate missing/stale checks. Do not confuse a skipped Draft check with passed test evidence. + +A GitHub workflow record for a Draft may still show as *skipped*; it must consume **zero expensive job runners**, not requeue 15-minute builds. Manual dispatch remains available and is not skipped. diff --git a/scripts/check_ci_trigger_policy.py b/scripts/check_ci_trigger_policy.py index 89ad18054..b7934c071 100644 --- a/scripts/check_ci_trigger_policy.py +++ b/scripts/check_ci_trigger_policy.py @@ -67,15 +67,35 @@ def main(): assert matching(paths(sources["scl-assisted-workbench-ci.yml"]), MODEL_CPP) assert not matching(paths(sources["ied-simulator-release-hardening.yml"]), NORMAL_GUI) assert matching(paths(sources["ied-simulator-release-hardening.yml"]), PACKAGING) - # Firmware branch builds are intentionally preserved for board testing, but - # one branch owns a shared push/PR concurrency key so only its latest - # firmware artifact run continues. + # PTP / SMV firmware uses one source-branch key for PR runs; repetitive + # feature pushes never schedule additional ESP-IDF builds. for name in ("esp32p4-smv-injector.yml", "esp32p4-ptp-lab.yml"): fw = (ROOT / name).read_text(encoding="utf-8") assert "github.event.pull_request.head.ref || github.ref_name" in fw, name assert "github.event.pull_request.head.repo.full_name || github.repository" in fw, name assert "github.ref != 'refs/heads/main'" in fw, name - print("CI_ROUTING_POLICY_PASS non_gui_excluded=6 duplicate_feature_push=0 pr_concurrency=15 firmware_branch_artifacts=preserved") + # WIP iteration must not spend Windows/ESP-IDF/3-compiler runners. + # Ready-for-review, main integration and explicit workflow_dispatch remain. + expensive = ("esp32p4-ptp-lab.yml", "esp32p4-smv-injector.yml", "ptp-core-ci.yml", "cpp-ci.yml", "embedded-ci.yml", "security-evidence.yml", "arstack-studio-qt.yml", "interoperability-server-ci.yml", "ied-simulator-release-hardening.yml", "arstack-studio-windows-lifecycle.yml", "brcb-hard-profile-ci.yml",) + for name in expensive: + source = (ROOT / name).read_text(encoding="utf-8") + assert "ready_for_review" in event_body(source, "pull_request"), name + job_part = source.split("\njobs:\n", 1)[1] + job_names = re.findall(r"(?m)^ [a-zA-Z0-9_-]+:", job_part) + draft_gates = re.findall( + r"(?m)^ if: .*github.event.pull_request.draft.*$", + job_part, + ) + assert len(draft_gates) == len(job_names), (name, len(draft_gates), len(job_names)) + for name in ("esp32p4-smv-injector.yml", "esp32p4-ptp-lab.yml"): + firmware = (ROOT / name).read_text(encoding="utf-8") + push = event_body(firmware, "push") + assert "branches: [main]" in push, f"{name}: feature push flood" + assert "workflow_dispatch:" in firmware, f"{name}: missing manual candidate" + release = (ROOT / "arstack-studio-release.yml").read_text(encoding="utf-8") + assert not re.search(r"(?m)^ pull_request:", release), "installer on every PR" + assert "workflow_dispatch:" in release and "tags:" in release + print("CI_ROUTING_POLICY_PASS draft_runner_gates=11 firmware_feature_push=off release_pr_packaging=off manual_candidates=on") if __name__ == "__main__": From f3c14d03fc02c893872746a9fa63f2d49aed6517 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 14 Oct 2026 14:32:13 +0700 Subject: [PATCH 2/3] ci: enforce Draft skip for IED Qt and feature-push main-only --- .github/workflows/ied-simulator-qt.yml | 2 ++ scripts/check_ci_trigger_policy.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ied-simulator-qt.yml b/.github/workflows/ied-simulator-qt.yml index cd660278c..cd85a2a8a 100644 --- a/.github/workflows/ied-simulator-qt.yml +++ b/.github/workflows/ied-simulator-qt.yml @@ -21,6 +21,7 @@ on: - 'CMakeLists.txt' - '.github/workflows/ied-simulator-qt.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'apps/ied_simulator/**' - 'include/ariec61850/scl/**' @@ -48,6 +49,7 @@ concurrency: jobs: build-and-smoke: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/scripts/check_ci_trigger_policy.py b/scripts/check_ci_trigger_policy.py index b7934c071..3a4952d4a 100644 --- a/scripts/check_ci_trigger_policy.py +++ b/scripts/check_ci_trigger_policy.py @@ -76,7 +76,7 @@ def main(): assert "github.ref != 'refs/heads/main'" in fw, name # WIP iteration must not spend Windows/ESP-IDF/3-compiler runners. # Ready-for-review, main integration and explicit workflow_dispatch remain. - expensive = ("esp32p4-ptp-lab.yml", "esp32p4-smv-injector.yml", "ptp-core-ci.yml", "cpp-ci.yml", "embedded-ci.yml", "security-evidence.yml", "arstack-studio-qt.yml", "interoperability-server-ci.yml", "ied-simulator-release-hardening.yml", "arstack-studio-windows-lifecycle.yml", "brcb-hard-profile-ci.yml",) + expensive = ("esp32p4-ptp-lab.yml", "esp32p4-smv-injector.yml", "ptp-core-ci.yml", "cpp-ci.yml", "embedded-ci.yml", "security-evidence.yml", "arstack-studio-qt.yml", "interoperability-server-ci.yml", "ied-simulator-release-hardening.yml", "ied-simulator-qt.yml", "arstack-studio-windows-lifecycle.yml", "brcb-hard-profile-ci.yml",) for name in expensive: source = (ROOT / name).read_text(encoding="utf-8") assert "ready_for_review" in event_body(source, "pull_request"), name @@ -95,7 +95,7 @@ def main(): release = (ROOT / "arstack-studio-release.yml").read_text(encoding="utf-8") assert not re.search(r"(?m)^ pull_request:", release), "installer on every PR" assert "workflow_dispatch:" in release and "tags:" in release - print("CI_ROUTING_POLICY_PASS draft_runner_gates=11 firmware_feature_push=off release_pr_packaging=off manual_candidates=on") + print("CI_ROUTING_POLICY_PASS draft_runner_gates=12 firmware_feature_push=off release_pr_packaging=off manual_candidates=on") if __name__ == "__main__": From 74ad858f601fa3389bece0b8a3b2a7daa67c6ff8 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 04:55:40 +0700 Subject: [PATCH 3/3] ci(policy): allow only bounded opt-in M3 phase checkpoint on Draft --- scripts/check_ci_trigger_policy.py | 41 +++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/scripts/check_ci_trigger_policy.py b/scripts/check_ci_trigger_policy.py index efe953138..21df70d15 100644 --- a/scripts/check_ci_trigger_policy.py +++ b/scripts/check_ci_trigger_policy.py @@ -92,14 +92,41 @@ def main(): assert "ready_for_review" in trigger, f"{name}: no ready transition" assert "paths:" in trigger, f"{name}: unscoped PR" jobs = source.split("\njobs:\n", 1)[1] - names = re.findall(r"(?m)^ [a-z0-9_-]+:\s*$", jobs) - guarded = re.findall( - r"(?m)^ [a-z0-9_-]+:\s*\n" + re.escape(gate) + r"\s*$", - jobs, - ) - assert names and len(guarded) == len(names), ( - name, len(guarded), len(names) + # Validate each top-level job, not just the number of guarded jobs. + # The sole Draft exception is the explicit M3 opt-in SOURCE checkpoint. + boundaries = list(re.finditer(r"(?m)^ ([a-z0-9_-]+):[ ]*$", jobs)) + assert boundaries, f"{name}: no jobs" + phase_gate = ( + " if: ${{ github.event_name == 'pull_request' && " + "github.event.pull_request.draft && " + "contains(github.event.pull_request.body, '[run-phase-guard]') }}" ) + for index, boundary in enumerate(boundaries): + job_name = boundary.group(1) + block_end = ( + boundaries[index + 1].start() + if index + 1 < len(boundaries) else len(jobs) + ) + block = jobs[boundary.end():block_end] + if_lines = re.findall(r"(?m)^ if: .+$", block) + if if_lines == [gate]: + continue + if name == "esp32p4-ptp-lab.yml" and job_name == "phase-guard-checkpoint": + assert if_lines == [phase_gate], (name, job_name, "unsafe opt-in") + assert not re.search(r"(?m)^ (?:strategy|uses):", block), ( + name, job_name, "checkpoint may not be a matrix/reusable job" + ) + assert "matrix:" not in block, (name, job_name, "matrix flood") + assert "Portable strict phase regression" in block, ( + name, job_name, "portable tests missing" + ) + assert "Compile guarded ESP32-P4 SOURCE firmware" in block, ( + name, job_name, "SOURCE build missing" + ) + continue + raise AssertionError( + f"{name}: job {job_name} lacks Draft gate (got {if_lines})" + ) for name in ("esp32p4-smv-injector.yml", "esp32p4-ptp-lab.yml"): source = (ROOT / name).read_text(encoding="utf-8")