From f56da629e05792047c513cffec9417034274f608 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 14 Oct 2026 13:24:02 +0700 Subject: [PATCH 1/3] ci: scope PR gates by source risk and cancel superseded heads Stop unconditional embedded/control/BRCB/security/C++ matrices on Workbench-only PR changes. Keep full main integration checks, avoid push+PR duplication on feature branches, scope costly Windows release packaging, and add a lightweight CI routing policy gate plus documentation. --- .github/workflows/arstack-studio-qt.yml | 3 +- .github/workflows/brcb-hard-profile-ci.yml | 16 +++++ .github/workflows/ci-routing-policy.yml | 26 +++++++ .../workflows/control-interop-harness-ci.yml | 17 +++++ .github/workflows/cpp-ci.yml | 15 ++++ .github/workflows/dynamic-rcb-trial-ci.yml | 17 +++++ .github/workflows/embedded-ci.yml | 25 +++++++ .github/workflows/ied-simulator-qt.yml | 5 ++ .../ied-simulator-release-hardening.yml | 11 ++- .github/workflows/scl-assisted-connect-ci.yml | 3 +- .../workflows/scl-assisted-workbench-ci.yml | 3 +- .github/workflows/security-evidence.yml | 13 ++++ docs/CI_WORKFLOW_ROUTING.md | 23 +++++++ scripts/check_ci_trigger_policy.py | 68 +++++++++++++++++++ 14 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci-routing-policy.yml create mode 100644 docs/CI_WORKFLOW_ROUTING.md create mode 100644 scripts/check_ci_trigger_policy.py diff --git a/.github/workflows/arstack-studio-qt.yml b/.github/workflows/arstack-studio-qt.yml index 770f9fc59..fa82bec12 100644 --- a/.github/workflows/arstack-studio-qt.yml +++ b/.github/workflows/arstack-studio-qt.yml @@ -2,6 +2,7 @@ name: ARStack Studio Qt on: push: + branches: [main] paths: - 'apps/arstack_studio/**' - 'include/ariec61850/sampled_values/**' @@ -21,7 +22,7 @@ permissions: concurrency: group: arstack-studio-qt-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: windows-launcher-parse: diff --git a/.github/workflows/brcb-hard-profile-ci.yml b/.github/workflows/brcb-hard-profile-ci.yml index 2c7bd3d8e..d1638cf18 100644 --- a/.github/workflows/brcb-hard-profile-ci.yml +++ b/.github/workflows/brcb-hard-profile-ci.yml @@ -5,10 +5,26 @@ on: branches: - main pull_request: + paths: + - 'embedded/brcb_hard_profile/**' + - 'embedded/brcb_*.cpp' + - 'embedded/mms_brcb_*.cpp' + - 'include/ariec61850/mms/**' + - 'src/mms/**' + - 'include/ariec61850/control/**' + - 'src/control/**' + - 'include/ariec61850/asn1/**' + - 'src/asn1/**' + - 'tests/fixtures/scl/**' + - '.github/workflows/brcb-hard-profile-ci.yml' permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: brcb-hard-profile: name: BRCB Hard / ${{ matrix.compiler }} diff --git a/.github/workflows/ci-routing-policy.yml b/.github/workflows/ci-routing-policy.yml new file mode 100644 index 000000000..c17463caf --- /dev/null +++ b/.github/workflows/ci-routing-policy.yml @@ -0,0 +1,26 @@ +name: CI Routing Policy +on: + pull_request: + paths: + - '.github/workflows/**' + - 'scripts/check_ci_trigger_policy.py' + - 'docs/CI_WORKFLOW_ROUTING.md' + push: + branches: [main] + paths: + - '.github/workflows/**' + - 'scripts/check_ci_trigger_policy.py' + - 'docs/CI_WORKFLOW_ROUTING.md' +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} +jobs: + routing-contract: + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - uses: actions/checkout@v4 + - name: Verify scoped PR workflow routing + run: python3 scripts/check_ci_trigger_policy.py diff --git a/.github/workflows/control-interop-harness-ci.yml b/.github/workflows/control-interop-harness-ci.yml index 9623c7fed..74f78d5ad 100644 --- a/.github/workflows/control-interop-harness-ci.yml +++ b/.github/workflows/control-interop-harness-ci.yml @@ -5,11 +5,28 @@ on: branches: - "agent/phase-4d-c5-control-interoperability" pull_request: + paths: + - 'tools/control_interop_probe/**' + - 'include/ariec61850/control/**' + - 'src/control/**' + - 'include/ariec61850/mms/**' + - 'src/mms/**' + - 'include/ariec61850/asn1/**' + - 'src/asn1/**' + - 'include/ariec61850/osi/**' + - 'src/osi/**' + - 'include/ariec61850/acse/**' + - 'src/acse/**' + - '.github/workflows/control-interop-harness-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' }} + jobs: control-interop: name: Control Interop Harness / ${{ matrix.compiler }} diff --git a/.github/workflows/cpp-ci.yml b/.github/workflows/cpp-ci.yml index 193f6e8e4..975c373de 100644 --- a/.github/workflows/cpp-ci.yml +++ b/.github/workflows/cpp-ci.yml @@ -4,11 +4,26 @@ on: push: branches: [main] pull_request: + paths: + - 'CMakeLists.txt' + - 'cmake/**' + - 'include/**' + - 'src/**' + - 'tests/**' + - 'tools/**' + - 'scripts/**' + - 'fuzz/**' + - 'embedded/**' + - '.github/workflows/cpp-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' }} + jobs: linux: name: Linux / ${{ matrix.compiler }} diff --git a/.github/workflows/dynamic-rcb-trial-ci.yml b/.github/workflows/dynamic-rcb-trial-ci.yml index 7e20d3b91..3e3933d9b 100644 --- a/.github/workflows/dynamic-rcb-trial-ci.yml +++ b/.github/workflows/dynamic-rcb-trial-ci.yml @@ -4,11 +4,28 @@ on: push: branches: ["agent/phase-4d-d2-dynamic-rcb-live-trial"] pull_request: + paths: + - 'tools/dynamic_rcb_trial/**' + - 'include/ariec61850/mms/**' + - 'src/mms/**' + - 'include/ariec61850/control/**' + - 'src/control/**' + - 'include/ariec61850/asn1/**' + - 'src/asn1/**' + - 'include/ariec61850/osi/**' + - 'src/osi/**' + - 'include/ariec61850/acse/**' + - 'src/acse/**' + - '.github/workflows/dynamic-rcb-trial-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' }} + jobs: build: strategy: diff --git a/.github/workflows/embedded-ci.yml b/.github/workflows/embedded-ci.yml index e7520114e..54ef8eae4 100644 --- a/.github/workflows/embedded-ci.yml +++ b/.github/workflows/embedded-ci.yml @@ -5,10 +5,35 @@ on: branches: - main pull_request: + paths: + - 'embedded/**' + - 'include/ariec61850/mms/**' + - 'src/mms/**' + - 'include/ariec61850/control/**' + - 'src/control/**' + - 'include/ariec61850/asn1/**' + - 'src/asn1/**' + - 'include/ariec61850/acse/**' + - 'src/acse/**' + - 'include/ariec61850/osi/**' + - 'src/osi/**' + - 'include/ariec61850/goose/**' + - 'src/goose/**' + - 'include/ariec61850/sampled_values/**' + - 'src/sampled_values/**' + - 'include/ariec61850/ethernet/**' + - 'src/ethernet/**' + - 'include/ariec61850/time_sync/**' + - 'src/time_sync/**' + - '.github/workflows/embedded-ci.yml' permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: embedded-host-sim: name: Embedded host-sim / ${{ matrix.compiler }} diff --git a/.github/workflows/ied-simulator-qt.yml b/.github/workflows/ied-simulator-qt.yml index 6b9cbf498..cd660278c 100644 --- a/.github/workflows/ied-simulator-qt.yml +++ b/.github/workflows/ied-simulator-qt.yml @@ -2,6 +2,7 @@ name: IED Simulator Qt on: push: + branches: [main] paths: - 'apps/ied_simulator/**' - 'include/ariec61850/scl/**' @@ -41,6 +42,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: build-and-smoke: runs-on: ubuntu-24.04 diff --git a/.github/workflows/ied-simulator-release-hardening.yml b/.github/workflows/ied-simulator-release-hardening.yml index 5285cdf7c..da767ee20 100644 --- a/.github/workflows/ied-simulator-release-hardening.yml +++ b/.github/workflows/ied-simulator-release-hardening.yml @@ -1,19 +1,28 @@ name: IED Simulator Release Hardening on: + workflow_dispatch: push: + branches: [main] paths: - 'apps/ied_simulator/**' - 'assets/**' - '.github/workflows/ied-simulator-release-hardening.yml' pull_request: paths: - - 'apps/ied_simulator/**' + - 'apps/ied_simulator/packaging/**' + - 'apps/ied_simulator/CMakeLists.txt' + - 'apps/ied_simulator/src/main.cpp' + - 'assets/**' - '.github/workflows/ied-simulator-release-hardening.yml' permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: linux-release-soak: runs-on: ubuntu-24.04 diff --git a/.github/workflows/scl-assisted-connect-ci.yml b/.github/workflows/scl-assisted-connect-ci.yml index cd71df3e9..9c1b9e7e3 100644 --- a/.github/workflows/scl-assisted-connect-ci.yml +++ b/.github/workflows/scl-assisted-connect-ci.yml @@ -2,6 +2,7 @@ name: SCL-Assisted Connect CI on: push: + branches: [main] paths: - 'include/ariec61850/mms/scl_assisted_connect.hpp' - 'include/ariec61850/mms/scl_association.hpp' @@ -38,7 +39,7 @@ on: concurrency: group: scl-assisted-connect-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: linux-profile: diff --git a/.github/workflows/scl-assisted-workbench-ci.yml b/.github/workflows/scl-assisted-workbench-ci.yml index 5485105f6..a04c67905 100644 --- a/.github/workflows/scl-assisted-workbench-ci.yml +++ b/.github/workflows/scl-assisted-workbench-ci.yml @@ -2,6 +2,7 @@ name: SCL-Assisted Workbench CI on: push: + branches: [main] paths: - 'apps/ied_simulator/src/MmsClientController.*' - 'apps/ied_simulator/src/MmsLiveTreeModel.*' @@ -39,7 +40,7 @@ permissions: concurrency: group: scl-assisted-workbench-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: product-path: diff --git a/.github/workflows/security-evidence.yml b/.github/workflows/security-evidence.yml index 01c3dc1e0..ec69f2782 100644 --- a/.github/workflows/security-evidence.yml +++ b/.github/workflows/security-evidence.yml @@ -4,11 +4,24 @@ on: push: branches: [main] pull_request: + paths: + - 'CMakeLists.txt' + - 'cmake/**' + - 'include/**' + - 'src/**' + - 'tests/**' + - 'tools/**' + - 'fuzz/**' + - '.github/workflows/security-evidence.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' }} + jobs: sanitizers: name: Clang / ASan + UBSan diff --git a/docs/CI_WORKFLOW_ROUTING.md b/docs/CI_WORKFLOW_ROUTING.md new file mode 100644 index 000000000..84e8ef300 --- /dev/null +++ b/docs/CI_WORKFLOW_ROUTING.md @@ -0,0 +1,23 @@ +# 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. + +## Pull request matrix + +| Changed area | Normal PR gate | Additional relevant gates | +| --- | --- | --- | +| `apps/ied_simulator/qml/**` / Workbench UI | IED Simulator Qt | SCL-assisted Workbench only for its specifically listed C++ model files | +| `apps/ied_simulator/packaging/**`, app CMake or entrypoint | IED Simulator Qt | IED Simulator Release Hardening | +| `include/**`, `src/**`, root `tests/**`, `tools/**` | C++ CI, Security and Evidence | Specialized profile gates matching the changed protocol | +| BRCB/RCB sources | C++ CI, Security and Evidence | BRCB Hard / Dynamic RCB where relevant | +| 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. +- **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. +- **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. diff --git a/scripts/check_ci_trigger_policy.py b/scripts/check_ci_trigger_policy.py new file mode 100644 index 000000000..2f0d33d5a --- /dev/null +++ b/scripts/check_ci_trigger_policy.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Fail fast if unrelated IEC 61850 workflow matrices flood a Workbench-only PR.""" +from pathlib import Path +import fnmatch +import re + +ROOT = Path(__file__).resolve().parents[1] / ".github" / "workflows" +NAME = [ + "brcb-hard-profile-ci.yml", + "control-interop-harness-ci.yml", + "cpp-ci.yml", + "dynamic-rcb-trial-ci.yml", + "embedded-ci.yml", + "security-evidence.yml", + "ied-simulator-qt.yml", + "ied-simulator-release-hardening.yml", + "scl-assisted-workbench-ci.yml", + "arstack-studio-qt.yml", + "scl-assisted-connect-ci.yml", +] +NON_GUI = set(NAME[:6]) +NORMAL_GUI = "apps/ied_simulator/qml/BrowserDataModelPane.qml" +MODEL_CPP = "apps/ied_simulator/src/MmsLiveTreeModel.cpp" +PACKAGING = "apps/ied_simulator/packaging/windows/ARStackIEC61850Workbench.nsi" + + +def event_body(source, event): + match = re.search(r"(?m)^ " + re.escape(event) + r":\s*\n", source) + if not match: + raise AssertionError(f"missing event: {event}") + rest = source[match.end():] + end = re.search(r"(?m)^ [a-z_]+:\s*(?:\n|$)|^[a-z][\w-]*:", rest) + return rest[:end.start()] if end else rest + + +def paths(source): + body = event_body(source, "pull_request") + assert re.search(r"(?m)^ paths:\s*$", body), "unscoped pull_request" + return re.findall(r"(?m)^ - ['\"]([^'\"]+)['\"]\s*$", body) + + +def matching(patterns, path): + return any(fnmatch.fnmatchcase(path, p) for p in patterns) + + +def main(): + sources = {name: (ROOT / name).read_text(encoding="utf-8") for name in NAME} + for name, source in sources.items(): + pr_paths = paths(source) + assert pr_paths, f"{name}: empty paths" + assert f".github/workflows/{name}" in pr_paths, f"{name}: workflow self-check missing" + assert re.search(r"(?m)^concurrency:", source), f"{name}: no concurrency" + assert "github.event.pull_request.number || github.ref" in source, f"{name}: wrong PR group" + assert "github.event_name == 'pull_request'" in source, f"{name}: stale runs not cancelled" + assert not matching(pr_paths, NORMAL_GUI) if name in NON_GUI else True, f"{name}: GUI triggers unrelated job" + for name in ["ied-simulator-qt.yml", "ied-simulator-release-hardening.yml", + "scl-assisted-workbench-ci.yml", "arstack-studio-qt.yml", + "scl-assisted-connect-ci.yml"]: + assert re.search(r"(?m)^ branches: \[main\]$", event_body(sources[name], "push")), name + assert matching(paths(sources["ied-simulator-qt.yml"]), NORMAL_GUI) + 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) + print("CI_ROUTING_POLICY_PASS non_gui_excluded=6 duplicate_push=0 pr_concurrency=11 release_is_scoped=true") + + +if __name__ == "__main__": + main() From 79f1d46bc7b145883ac970c11e6e55ceca51173e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 14 Oct 2026 13:38:57 +0700 Subject: [PATCH 2/3] ci: deduplicate remaining Studio, PTP, SV feature pipelines Limit four additional push gates to main and cancel superseded PR runs. Preserve on-branch ESP32-P4 test artifacts while coalescing same-branch push/PR runs through shared repository-scoped concurrency. Extend policy guard and documentation. --- .../workflows/arstack-studio-rc2-policy.yml | 3 ++- .../arstack-studio-windows-lifecycle.yml | 3 ++- .github/workflows/esp32p4-ptp-lab.yml | 4 ++-- .github/workflows/esp32p4-smv-injector.yml | 4 ++-- .github/workflows/ptp-core-ci.yml | 5 +++++ .github/workflows/smv-injector-gui.yml | 5 +++++ docs/CI_WORKFLOW_ROUTING.md | 2 ++ scripts/check_ci_trigger_policy.py | 18 ++++++++++++++++-- 8 files changed, 36 insertions(+), 8 deletions(-) diff --git a/.github/workflows/arstack-studio-rc2-policy.yml b/.github/workflows/arstack-studio-rc2-policy.yml index 9a16d418f..03f980159 100644 --- a/.github/workflows/arstack-studio-rc2-policy.yml +++ b/.github/workflows/arstack-studio-rc2-policy.yml @@ -9,6 +9,7 @@ on: - 'docs/SMV_STUDIO_RC2_ACCEPTANCE.md' - '.github/workflows/arstack-studio-rc2-policy.yml' push: + branches: [main] paths: - 'apps/arstack_studio/src/Rc2Acceptance.cpp' - 'apps/arstack_studio/rc2/**' @@ -21,7 +22,7 @@ permissions: concurrency: group: arstack-studio-rc2-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: windows-policy: diff --git a/.github/workflows/arstack-studio-windows-lifecycle.yml b/.github/workflows/arstack-studio-windows-lifecycle.yml index 04d93998e..9639712fe 100644 --- a/.github/workflows/arstack-studio-windows-lifecycle.yml +++ b/.github/workflows/arstack-studio-windows-lifecycle.yml @@ -2,6 +2,7 @@ name: ARStack Studio Windows Lifecycle on: push: + branches: [main] paths: - 'apps/arstack_studio/**' - '.github/workflows/arstack-studio-windows-lifecycle.yml' @@ -15,7 +16,7 @@ permissions: concurrency: group: arstack-studio-windows-lifecycle-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: close-exit-reopen: diff --git a/.github/workflows/esp32p4-ptp-lab.yml b/.github/workflows/esp32p4-ptp-lab.yml index 1b7c9c967..035a032b0 100644 --- a/.github/workflows/esp32p4-ptp-lab.yml +++ b/.github/workflows/esp32p4-ptp-lab.yml @@ -22,8 +22,8 @@ permissions: contents: read concurrency: - group: esp32p4-ptp-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + group: esp32p4-ptp-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: build-esp32p4-ptp: diff --git a/.github/workflows/esp32p4-smv-injector.yml b/.github/workflows/esp32p4-smv-injector.yml index f1909791d..fbd885134 100644 --- a/.github/workflows/esp32p4-smv-injector.yml +++ b/.github/workflows/esp32p4-smv-injector.yml @@ -35,8 +35,8 @@ permissions: contents: read concurrency: - group: esp32p4-smv-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + group: esp32p4-smv-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} env: ARSTACK_FIRMWARE_VERSION: '0.1.1' diff --git a/.github/workflows/ptp-core-ci.yml b/.github/workflows/ptp-core-ci.yml index 5fbd9d222..0ea016870 100644 --- a/.github/workflows/ptp-core-ci.yml +++ b/.github/workflows/ptp-core-ci.yml @@ -2,6 +2,7 @@ name: PTP Core CI on: push: + branches: [main] paths: - 'include/ariec61850/time_sync/**' - 'src/time_sync/**' @@ -39,6 +40,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: portable-ptp: strategy: diff --git a/.github/workflows/smv-injector-gui.yml b/.github/workflows/smv-injector-gui.yml index da4287d87..95038665f 100644 --- a/.github/workflows/smv-injector-gui.yml +++ b/.github/workflows/smv-injector-gui.yml @@ -2,6 +2,7 @@ name: SMV Injector GUI on: push: + branches: [main] paths: - 'apps/smv_injector_gui/**' - 'tools/smv_profile_inspect.cpp' @@ -27,6 +28,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: profile-bridge-smoke: runs-on: ubuntu-latest diff --git a/docs/CI_WORKFLOW_ROUTING.md b/docs/CI_WORKFLOW_ROUTING.md index 84e8ef300..808ed79b3 100644 --- a/docs/CI_WORKFLOW_ROUTING.md +++ b/docs/CI_WORKFLOW_ROUTING.md @@ -16,6 +16,8 @@ The CI contract is **risk-scoped PR checks, full integrated `main` checks, and m - **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. +- **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. - **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. diff --git a/scripts/check_ci_trigger_policy.py b/scripts/check_ci_trigger_policy.py index 2f0d33d5a..89ad18054 100644 --- a/scripts/check_ci_trigger_policy.py +++ b/scripts/check_ci_trigger_policy.py @@ -17,6 +17,10 @@ "scl-assisted-workbench-ci.yml", "arstack-studio-qt.yml", "scl-assisted-connect-ci.yml", + "arstack-studio-rc2-policy.yml", + "arstack-studio-windows-lifecycle.yml", + "ptp-core-ci.yml", + "smv-injector-gui.yml", ] NON_GUI = set(NAME[:6]) NORMAL_GUI = "apps/ied_simulator/qml/BrowserDataModelPane.qml" @@ -55,13 +59,23 @@ def main(): assert not matching(pr_paths, NORMAL_GUI) if name in NON_GUI else True, f"{name}: GUI triggers unrelated job" for name in ["ied-simulator-qt.yml", "ied-simulator-release-hardening.yml", "scl-assisted-workbench-ci.yml", "arstack-studio-qt.yml", - "scl-assisted-connect-ci.yml"]: + "scl-assisted-connect-ci.yml", + "arstack-studio-rc2-policy.yml", "arstack-studio-windows-lifecycle.yml", + "ptp-core-ci.yml", "smv-injector-gui.yml"]: assert re.search(r"(?m)^ branches: \[main\]$", event_body(sources[name], "push")), name assert matching(paths(sources["ied-simulator-qt.yml"]), NORMAL_GUI) 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) - print("CI_ROUTING_POLICY_PASS non_gui_excluded=6 duplicate_push=0 pr_concurrency=11 release_is_scoped=true") + # 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. + 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") if __name__ == "__main__": From cd64a284ba95276cd7dc59aa6399df6556d950be Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 14 Oct 2026 13:40:10 +0700 Subject: [PATCH 3/3] ci: normalize SCL workbench and connect PR concurrency identity Use PR-number scoped run groups consistent with routing policy acceptance; preserve main integration runs. --- .github/workflows/scl-assisted-connect-ci.yml | 2 +- .github/workflows/scl-assisted-workbench-ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scl-assisted-connect-ci.yml b/.github/workflows/scl-assisted-connect-ci.yml index 9c1b9e7e3..ce20a877c 100644 --- a/.github/workflows/scl-assisted-connect-ci.yml +++ b/.github/workflows/scl-assisted-connect-ci.yml @@ -38,7 +38,7 @@ on: - '.github/workflows/scl-assisted-connect-ci.yml' concurrency: - group: scl-assisted-connect-${{ github.ref }} + group: scl-assisted-connect-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: diff --git a/.github/workflows/scl-assisted-workbench-ci.yml b/.github/workflows/scl-assisted-workbench-ci.yml index a04c67905..bbe242e41 100644 --- a/.github/workflows/scl-assisted-workbench-ci.yml +++ b/.github/workflows/scl-assisted-workbench-ci.yml @@ -39,7 +39,7 @@ permissions: contents: read concurrency: - group: scl-assisted-workbench-${{ github.ref }} + group: scl-assisted-workbench-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: