diff --git a/.github/workflows/arstack-studio-qt.yml b/.github/workflows/arstack-studio-qt.yml index fa82bec12..b9cd57e6d 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/**' @@ -26,6 +27,7 @@ concurrency: 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-rc2-policy.yml b/.github/workflows/arstack-studio-rc2-policy.yml index 03f980159..1574c72ac 100644 --- a/.github/workflows/arstack-studio-rc2-policy.yml +++ b/.github/workflows/arstack-studio-rc2-policy.yml @@ -2,6 +2,7 @@ name: ARStack Studio RC2 Acceptance Policy on: pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'apps/arstack_studio/src/Rc2Acceptance.cpp' - 'apps/arstack_studio/rc2/**' @@ -26,6 +27,7 @@ concurrency: jobs: windows-policy: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: RC2 Windows evidence gate runs-on: windows-2022 steps: @@ -80,6 +82,7 @@ jobs: retention-days: 14 linux-policy: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: RC2 Linux policy regression runs-on: ubuntu-latest container: ubuntu:26.04 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..6f080900e 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' @@ -20,6 +21,7 @@ concurrency: 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..dd9a2601c 100644 --- a/.github/workflows/brcb-hard-profile-ci.yml +++ b/.github/workflows/brcb-hard-profile-ci.yml @@ -4,7 +4,20 @@ on: push: branches: - main + 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' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'embedded/brcb_hard_profile/**' - 'embedded/brcb_*.cpp' @@ -27,6 +40,7 @@ concurrency: 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/control-hard-profile-ci.yml b/.github/workflows/control-hard-profile-ci.yml index b3894e862..b511a6223 100644 --- a/.github/workflows/control-hard-profile-ci.yml +++ b/.github/workflows/control-hard-profile-ci.yml @@ -4,7 +4,14 @@ on: push: branches: - main + paths: + - 'include/ariec61850/control/**' + - 'src/control/**' + - 'embedded/control_guarded_hard_profile_smoke.cpp' + - 'embedded/control_hard_profile/**' + - '.github/workflows/control-hard-profile-ci.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'include/ariec61850/control/**' - 'src/control/**' @@ -15,8 +22,13 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: control-hard-profile: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: Control Hard / ${{ matrix.compiler }} runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/control-interop-harness-ci.yml b/.github/workflows/control-interop-harness-ci.yml index 74f78d5ad..a5ddc0b8c 100644 --- a/.github/workflows/control-interop-harness-ci.yml +++ b/.github/workflows/control-interop-harness-ci.yml @@ -5,6 +5,7 @@ on: branches: - "agent/phase-4d-c5-control-interoperability" pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'tools/control_interop_probe/**' - 'include/ariec61850/control/**' @@ -29,6 +30,7 @@ concurrency: jobs: control-interop: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: Control Interop Harness / ${{ matrix.compiler }} runs-on: ${{ matrix.os }} strategy: diff --git a/.github/workflows/control-mms-profile-ci.yml b/.github/workflows/control-mms-profile-ci.yml index 86ef42d47..d39b3a7a2 100644 --- a/.github/workflows/control-mms-profile-ci.yml +++ b/.github/workflows/control-mms-profile-ci.yml @@ -4,7 +4,19 @@ on: push: branches: - main + paths: + - 'include/ariec61850/control/**' + - 'src/control/**' + - 'tests/test_control_mms_structure.cpp' + - 'tests/control_mms_profile/**' + - 'include/ariec61850/mms/data_value.hpp' + - 'include/ariec61850/mms/services.hpp' + - 'src/mms/data_value.cpp' + - 'src/mms/data_codec.cpp' + - 'src/mms/utc_time.cpp' + - '.github/workflows/control-mms-profile-ci.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'include/ariec61850/control/**' - 'src/control/**' @@ -20,8 +32,13 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: control-mms-profile: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: Control MMS / ${{ matrix.compiler }} runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/control-session-profile-ci.yml b/.github/workflows/control-session-profile-ci.yml index b1936b143..eefab7b92 100644 --- a/.github/workflows/control-session-profile-ci.yml +++ b/.github/workflows/control-session-profile-ci.yml @@ -4,7 +4,16 @@ on: push: branches: - main + paths: + - 'include/ariec61850/control/**' + - 'src/control/**' + - 'include/ariec61850/mms/association_runtime.hpp' + - 'src/mms/association_runtime_control_wait.cpp' + - 'tests/test_control_session.cpp' + - 'tests/control_session_profile/**' + - '.github/workflows/control-session-profile-ci.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'include/ariec61850/control/**' - 'src/control/**' @@ -17,8 +26,13 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: control-session-profile: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: Control Session / ${{ matrix.compiler }} runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/control-termination-profile-ci.yml b/.github/workflows/control-termination-profile-ci.yml index 56e0730a2..c67e942fc 100644 --- a/.github/workflows/control-termination-profile-ci.yml +++ b/.github/workflows/control-termination-profile-ci.yml @@ -4,7 +4,17 @@ on: push: branches: - main + paths: + - 'include/ariec61850/control/**' + - 'src/control/**' + - 'tests/test_command_termination.cpp' + - 'tests/control_termination_profile/**' + - 'include/ariec61850/mms/data_value.hpp' + - 'include/ariec61850/mms/reporting.hpp' + - 'src/mms/data_value.cpp' + - '.github/workflows/control-termination-profile-ci.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'include/ariec61850/control/**' - 'src/control/**' @@ -18,8 +28,13 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: control-termination-profile: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: Control Termination / ${{ matrix.compiler }} runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/cpp-ci.yml b/.github/workflows/cpp-ci.yml index 975c373de..de480af16 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/**' @@ -26,6 +38,7 @@ concurrency: 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/dynamic-rcb-trial-ci.yml b/.github/workflows/dynamic-rcb-trial-ci.yml index 3e3933d9b..8252f4d48 100644 --- a/.github/workflows/dynamic-rcb-trial-ci.yml +++ b/.github/workflows/dynamic-rcb-trial-ci.yml @@ -4,6 +4,7 @@ on: push: branches: ["agent/phase-4d-d2-dynamic-rcb-live-trial"] pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'tools/dynamic_rcb_trial/**' - 'include/ariec61850/mms/**' @@ -28,6 +29,7 @@ concurrency: jobs: build: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} strategy: matrix: include: diff --git a/.github/workflows/embedded-ci.yml b/.github/workflows/embedded-ci.yml index 54ef8eae4..80591ab54 100644 --- a/.github/workflows/embedded-ci.yml +++ b/.github/workflows/embedded-ci.yml @@ -4,7 +4,29 @@ on: push: branches: - main + 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' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'embedded/**' - 'include/ariec61850/mms/**' @@ -36,6 +58,7 @@ concurrency: 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-qt.yml b/.github/workflows/ied-simulator-qt.yml index dbe621bb7..65f47d09f 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/.github/workflows/ied-simulator-release-hardening.yml b/.github/workflows/ied-simulator-release-hardening.yml index 87d9b4fda..b22054c07 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' @@ -25,6 +26,7 @@ concurrency: 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..fc557c91e 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' @@ -12,7 +13,16 @@ on: - 'src/osi/**' - 'src/acse/**' - 'src/mms/**' - - 'include/ariec61850/**' + - 'include/ariec61850/asn1/**' + - 'include/ariec61850/acse/**' + - 'include/ariec61850/osi/**' + - 'include/ariec61850/mms/**' + - 'include/ariec61850/scl/**' + - 'include/ariec61850/control/**' + - 'include/ariec61850/ethernet/**' + - 'include/ariec61850/goose/**' + - 'include/ariec61850/sampled_values/**' + - 'include/ariec61850/capture/**' - '.github/workflows/interoperability-server-ci.yml' workflow_dispatch: @@ -22,6 +32,7 @@ concurrency: jobs: linux: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} strategy: matrix: compiler: [gcc, clang] @@ -45,6 +56,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/mms-r12-server-ci.yml b/.github/workflows/mms-r12-server-ci.yml index 23f89af9d..17dc7e782 100644 --- a/.github/workflows/mms-r12-server-ci.yml +++ b/.github/workflows/mms-r12-server-ci.yml @@ -2,6 +2,7 @@ name: MMS R1-R2 Server CI on: pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'include/ariec61850/acse/**' - 'include/ariec61850/mms/**' @@ -19,8 +20,13 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: linux: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} name: Linux ${{ matrix.compiler }} runs-on: ubuntu-latest strategy: @@ -52,6 +58,7 @@ jobs: ./build-mms-r12/ariec61850_mms_urcb_server --help 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/ptp-core-ci.yml b/.github/workflows/ptp-core-ci.yml index 0ea016870..cfbd312be 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,6 +38,8 @@ on: - 'tests/ptp_profile/**' - '.github/workflows/ptp-core-ci.yml' + workflow_dispatch: + permissions: contents: read @@ -46,6 +49,7 @@ concurrency: 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/scl-assisted-connect-ci.yml b/.github/workflows/scl-assisted-connect-ci.yml index ce20a877c..aa3bc6cb7 100644 --- a/.github/workflows/scl-assisted-connect-ci.yml +++ b/.github/workflows/scl-assisted-connect-ci.yml @@ -20,6 +20,7 @@ on: - 'tests/scl_assisted_profile/**' - '.github/workflows/scl-assisted-connect-ci.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'include/ariec61850/mms/scl_assisted_connect.hpp' - 'include/ariec61850/mms/scl_association.hpp' @@ -43,6 +44,7 @@ concurrency: jobs: linux-profile: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/scl-assisted-workbench-ci.yml b/.github/workflows/scl-assisted-workbench-ci.yml index bbe242e41..96ad0fbf3 100644 --- a/.github/workflows/scl-assisted-workbench-ci.yml +++ b/.github/workflows/scl-assisted-workbench-ci.yml @@ -19,6 +19,7 @@ on: - 'tests/fixtures/scl/client-workbench-incompatible.scd' - '.github/workflows/scl-assisted-workbench-ci.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'apps/ied_simulator/src/MmsClientController.*' - 'apps/ied_simulator/src/MmsLiveTreeModel.*' @@ -44,6 +45,7 @@ concurrency: jobs: product-path: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/security-evidence.yml b/.github/workflows/security-evidence.yml index ec69f2782..49d2e8546 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/**' @@ -24,6 +34,7 @@ concurrency: 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/.github/workflows/smv-injector-gui.yml b/.github/workflows/smv-injector-gui.yml index 95038665f..772435759 100644 --- a/.github/workflows/smv-injector-gui.yml +++ b/.github/workflows/smv-injector-gui.yml @@ -14,6 +14,7 @@ on: - 'tests/fixtures/scl/**' - '.github/workflows/smv-injector-gui.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'apps/smv_injector_gui/**' - 'tools/smv_profile_inspect.cpp' @@ -34,6 +35,7 @@ concurrency: jobs: profile-bridge-smoke: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: - name: Checkout @@ -155,6 +157,7 @@ jobs: grep -q '"deviceSupport":"ready"' /tmp/api.json windows-launcher-smoke: + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: windows-latest steps: - name: Checkout diff --git a/docs/CI_WORKFLOW_ROUTING.md b/docs/CI_WORKFLOW_ROUTING.md index 808ed79b3..7c5263966 100644 --- a/docs/CI_WORKFLOW_ROUTING.md +++ b/docs/CI_WORKFLOW_ROUTING.md @@ -1,25 +1,67 @@ -# CI routing contract — ARStack61850 +# ARStack61850 — CI routing and runner-budget contract -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. +**Purpose:** fast iterations without sacrificing reliable release validation. +A normal WIP/experimental feature push must NOT start a firmware matrix, +Windows package, sanitizer run, or broad unrelated subsystem regression. -## Pull request matrix +## Routing by lifecycle -| Changed area | Normal PR gate | Additional relevant gates | +| Event | What runs | What must **not** run | | --- | --- | --- | -| `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. -- **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. - -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. +| Draft PR, opened/synchronized | Lightweight routing policy and deliberately opted-in checkpoint jobs | Heavy ESP-IDF, Windows Qt/installer, GCC+Clang+MSVC, sanitizers, control/MMS matrices | +| Explicit single checkpoint | Engineer selects the exact workflow/ref via `workflow_dispatch`, or a temporary opt-in one-job gate | Unrelated workflows and full platform/release packaging | +| PR `ready_for_review`, later synchronize | All *relevant* path-filtered CI, including specialized required regression | Unrelated subsystem matrices | +| `main` integration push | Relevant path-scoped integration tests (including security, control, firmware) | All-workflows-on-every-commit | +| Tagged `v*` release or manual release dispatch | ARStack Studio Windows installer + release firmware as requested | Installer on ordinary PR commits | + +Every workflow keeps its own **`paths`** filters for ready PR checks. +When a Draft PR is updated, GitHub may show a workflow **skipped** record; +that record is not a passed test and must not allocate the heavy job runner. +We cannot prevent the GitHub PR event itself without also losing up-to-date +Ready-PR checks on `synchronize`. + +## Firmware and PTP–SMV strategy + +- **ESP32-P4 PTP Lab Profile** and **ESP32-P4 SMV Injector**: + feature-branch `push` events are disabled; automatic `push` is scoped to + `main` plus relevant paths. `workflow_dispatch` is supported on exact + feature refs to create a test firmware artifact intentionally. +- `workflow_call` in the existing SV firmware workflow remains available + to the manually/tag-triggered Studio release process. +- Draft PTP/SV development can use a **single explicit target** in the + existing workflow (e.g. M2/M3 phase regressions plus guarded SOURCE build), + then remove the opt-in after receiving exact-SHA evidence. +- The default firmware must never be made falsely green by treating a + **skipped** experimental test as proof of PTP synchronization or IED lock. + +## Studio, IED Simulator and protocol tests + +- Studio installer (`arstack-studio-release.yml`) triggers only from a + version tag or explicit `workflow_dispatch`, not normal PRs. +- Studio Qt, Simulator Qt and Windows lifecycle jobs remain path-scoped for + Ready PRs but skip expensive jobs on Draft. +- Interoperability server no longer matches *all* IEC61850 headers: + a standalone PTP time-sync change does not invoke MMS-server CI. +- Core C++, embedded and security jobs use existing PR affected-file paths + on `main` too. Control/BRCB specialized CI uses its own affected paths + on `main`, instead of rebuilding every Control variant for unrelated UI. +- Superseded PR heads are cancelled via workflow-specific `concurrency` + groups. Avoid workflows that cause additional commits or dispatch siblings + merely to perform ordinary validation. + +## Working discipline + +1. Implement a coherent milestone in one local/remote batch. Avoid push after + every line edit, generated formatting change, or README adjustment. +2. Check what can be tested without hardware first (portable CTest/lint). +3. Choose **one** targeted CI checkpoint on the selected feature SHA. +4. Resolve failures, then mark Ready when normal path-scoped checks should run. +5. Before integration/release, insist on relevant **non-skipped** exact-head + results; select the release workflow explicitly if an installer is needed. +6. A CI routing policy modification is always checked by the cheap + `ci-routing-policy.yml` job, which runs + `scripts/check_ci_trigger_policy.py`. + +**Cost/accountability:** A skipped Draft run is acceptable metadata. +A repeated 25-minute ESP-IDF image pull, full Windows package, irrelevant +protocol matrix or repeated action per WIP commit is not. diff --git a/scripts/check_ci_trigger_policy.py b/scripts/check_ci_trigger_policy.py index 89ad18054..21df70d15 100644 --- a/scripts/check_ci_trigger_policy.py +++ b/scripts/check_ci_trigger_policy.py @@ -67,15 +67,105 @@ 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. + # Real routing guarantees: no feature-push firmware fanout, no + # PR-triggered installer, no Draft PR runner minutes for heavy matrices. + heavy_pr = ( + "esp32p4-ptp-lab.yml", "esp32p4-smv-injector.yml", + "cpp-ci.yml", "embedded-ci.yml", "security-evidence.yml", + "ptp-core-ci.yml", "arstack-studio-qt.yml", + "arstack-studio-windows-lifecycle.yml", "ied-simulator-qt.yml", + "ied-simulator-release-hardening.yml", "brcb-hard-profile-ci.yml", + "control-hard-profile-ci.yml", "control-mms-profile-ci.yml", + "control-session-profile-ci.yml", "control-termination-profile-ci.yml", + "interoperability-server-ci.yml", "scl-assisted-connect-ci.yml", + "scl-assisted-workbench-ci.yml", "smv-injector-gui.yml", + "arstack-studio-rc2-policy.yml", "mms-r12-server-ci.yml", + "control-interop-harness-ci.yml", "dynamic-rcb-trial-ci.yml", + ) + gate = ( + " if: ${{ github.event_name != 'pull_request' || " + "!github.event.pull_request.draft }}" + ) + for name in heavy_pr: + source = (ROOT / name).read_text(encoding="utf-8") + trigger = event_body(source, "pull_request") + 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] + # 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"): - 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") + source = (ROOT / name).read_text(encoding="utf-8") + push = event_body(source, "push") + assert "branches: [main]" in push, f"{name}: feature push flood" + assert "paths:" in push, f"{name}: unscoped main build" + assert re.search(r"(?m)^ workflow_dispatch:\s*$", source), ( + name, "missing on-demand board artifact" + ) + + main_path_scoped = ( + "cpp-ci.yml", "security-evidence.yml", "embedded-ci.yml", + "brcb-hard-profile-ci.yml", "control-hard-profile-ci.yml", + "control-mms-profile-ci.yml", "control-session-profile-ci.yml", + "control-termination-profile-ci.yml", + ) + for name in main_path_scoped: + source = (ROOT / name).read_text(encoding="utf-8") + push = event_body(source, "push") + assert "paths:" in push, f"{name}: full suite on every main push" + + release = (ROOT / "arstack-studio-release.yml").read_text(encoding="utf-8") + assert not re.search(r"(?m)^ pull_request:", release), ( + "Studio installer packaging must be manually dispatched or tagged" + ) + assert "workflow_dispatch:" in release and "tags:" in release + interop = (ROOT / "interoperability-server-ci.yml").read_text( + encoding="utf-8" + ) + assert not matching( + paths(interop), "include/ariec61850/time_sync/ptp_runtime.hpp" + ), "PTP-only headers must not trigger MMS server CI" + + print( + "CI_ROUTING_POLICY_PASS: " + "draft_gate_workflows=23; feature_firmware_push=off; " + "main_path_scoped=8; installer_pr=off; " + "targeted_manual_firmware=on" + ) if __name__ == "__main__":