From 06a112faca12ff0e0321af03754ab6d97f89f318 Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 20 May 2026 15:50:14 +0800 Subject: [PATCH 1/9] ci: add Integration Test Single Node (Full) workflow Pulls troninfra/troninfra-ci:latest from DockerHub on every PR/dispatch and runs the integration test suite against the FullNode.jar built from the PR. Uses JDK 8 inside the container so FullNode runs on the production runtime; JAVA_HOME_17 keeps Gradle on JDK 17 for the test tooling. Image is built and published from a separate repo (tron_integration-test/java-tron_integration-test) as part of that project's release pipeline (vX.Y.Z tag push). --- .../integration-test-single-node.yml | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/integration-test-single-node.yml diff --git a/.github/workflows/integration-test-single-node.yml b/.github/workflows/integration-test-single-node.yml new file mode 100644 index 00000000000..874ffb82cdd --- /dev/null +++ b/.github/workflows/integration-test-single-node.yml @@ -0,0 +1,78 @@ +name: Integration Test Single Node (Full) + +on: + pull_request: + branches: [ 'develop', 'release_**' ] + types: [ opened, synchronize, reopened ] + paths-ignore: [ '**/*.md', '.gitignore', '**/.gitignore', '.editorconfig', + '.gitattributes', 'docs/**', 'CHANGELOG', '.github/ISSUE_TEMPLATE/**', + '.github/PULL_REQUEST_TEMPLATE/**', '.github/CODEOWNERS' ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + integration: + name: Integration Test Single Node Full (JDK 8 / x86_64) + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Checkout java-tron + uses: actions/checkout@v5 + + - name: Set up JDK 8 + uses: actions/setup-java@v5 + with: + java-version: '8' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-integration-test-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + restore-keys: ${{ runner.os }}-gradle-integration-test- + + - name: Build FullNode.jar + run: ./gradlew clean build -x test --no-daemon + + - name: Pull integration-test image + run: docker pull troninfra/troninfra-ci:latest + + - name: Run integration tests + run: | + # JAVA_HOME=JDK 8 so FullNode runs on the same JVM family as + # production (a few assertions check `java.version` starts with + # "1.8"). JAVA_HOME_17 keeps Gradle on JDK 17 for the test + # tooling, which requires Java 17. + docker run --name integration-test \ + -e FULLNODE_JAR=/javatron/FullNode.jar \ + -e JAVA_HOME=/usr/lib/jvm/temurin-8 \ + -e JAVA_HOME_17=/opt/java/openjdk \ + -v "${{ github.workspace }}/build/libs/FullNode.jar:/javatron/FullNode.jar:ro" \ + troninfra/troninfra-ci:latest \ + --clean + + - name: Extract test reports from container + if: always() + run: | + mkdir -p integration-reports + docker cp integration-test:/app/build/reports/. integration-reports/reports/ 2>/dev/null || true + docker cp integration-test:/app/build/test-results/. integration-reports/test-results/ 2>/dev/null || true + docker cp integration-test:/app/build/test-output.log integration-reports/ 2>/dev/null || true + docker cp integration-test:/app/node/node.log integration-reports/ 2>/dev/null || true + docker cp integration-test:/app/node/data/logs/tron.log integration-reports/ 2>/dev/null || true + docker rm -f integration-test 2>/dev/null || true + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v6 + with: + name: integration-test-report + path: integration-reports/ + if-no-files-found: warn From 580f4babf7ea6e8682a8b731fed0be94d86fea11 Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 20 May 2026 15:50:14 +0800 Subject: [PATCH 2/9] ci: add Integration Test Multinode (Full) workflow Mirrors the single-node workflow but starts a 3-witness docker-compose stack via the integration-test image and runs the multinode test target against it. HOST_WORKDIR is set so the script's path-alignment logic resolves compose configs to host paths usable by the host docker daemon (DinD via socket mount). Builds the PR's FullNode.jar, wraps it in a local docker image that inherits from tronprotocol/java-tron:latest, and injects it into the multinode stack via TRON_IMAGE. --- .../workflows/integration-test-multinode.yml | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 .github/workflows/integration-test-multinode.yml diff --git a/.github/workflows/integration-test-multinode.yml b/.github/workflows/integration-test-multinode.yml new file mode 100644 index 00000000000..3d8ac20d507 --- /dev/null +++ b/.github/workflows/integration-test-multinode.yml @@ -0,0 +1,117 @@ +name: Integration Test Multinode (Full) + +on: + pull_request: + branches: [ 'develop', 'release_**' ] + types: [ opened, synchronize, reopened ] + paths-ignore: [ '**/*.md', '.gitignore', '**/.gitignore', '.editorconfig', + '.gitattributes', 'docs/**', 'CHANGELOG', '.github/ISSUE_TEMPLATE/**', + '.github/PULL_REQUEST_TEMPLATE/**', '.github/CODEOWNERS' ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + multinode-full: + name: Integration Test Multinode Full (JDK 8 / x86_64) + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Checkout java-tron + uses: actions/checkout@v5 + + - name: Set up JDK 8 + uses: actions/setup-java@v5 + with: + java-version: '8' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-multinode-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + restore-keys: ${{ runner.os }}-gradle-multinode- + + - name: Build FullNode.jar + run: ./gradlew clean build -x test --no-daemon + + - name: Build local java-tron Docker image (wraps PR-built FullNode.jar) + run: | + mkdir -p /tmp/tron-image + cp build/libs/FullNode.jar /tmp/tron-image/ + cat > /tmp/tron-image/Dockerfile <<'EOF' + FROM tronprotocol/java-tron:latest + COPY FullNode.jar /java-tron/lib/FullNode.jar + EOF + docker build -t java-tron-local:pr /tmp/tron-image + + - name: Pull integration-test image + run: docker pull troninfra/troninfra-ci:latest + + - name: Extract compose configs to host (for DinD path-alignment) + run: | + # start-multinode.sh builds HOST_COMPOSE_DIR as: + # ${HOST_WORKDIR}/docker/multi-node + # so the files must live at $HOST_WORKDIR/docker/multi-node/ on the + # host. Set HOST_WORKDIR to the workspace root and extract + # /app/docker/ 1:1 into workspace/docker/ — the subdirectories + # (multi-node/, single-node/) don't collide with java-tron's own + # docker/ files. + docker create --name it-extract troninfra/troninfra-ci:latest + docker cp it-extract:/app/docker/. "${{ github.workspace }}/docker/" + docker rm -f it-extract + + - name: Run multinode full tests + run: | + # --network host: multinode tests talk to nodes via 127.0.0.1:50051 etc. + # DinD socket + HOST_WORKDIR path-alignment lets the container orchestrate + # the 3-witness compose stack via the host daemon. + # Don't override --workdir so the container's default /app entrypoint works. + docker run --name integration-multinode \ + --network host \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "${{ github.workspace }}:${{ github.workspace }}" \ + -v "${{ github.workspace }}/docker/multi-node:/app/docker/multi-node" \ + -e HOST_WORKDIR="${{ github.workspace }}" \ + -e TRON_IMAGE=java-tron-local:pr \ + -e JAVA_HOME=/usr/lib/jvm/temurin-8 \ + -e JAVA_HOME_17=/opt/java/openjdk \ + troninfra/troninfra-ci:latest \ + --multinode --clean + + - name: Extract test reports from container + if: always() + run: | + mkdir -p integration-reports + docker cp integration-multinode:/app/build/reports/. integration-reports/reports/ 2>/dev/null || true + docker cp integration-multinode:/app/build/test-results/. integration-reports/test-results/ 2>/dev/null || true + docker cp integration-multinode:/app/build/test-output.log integration-reports/ 2>/dev/null || true + + - name: Collect witness node logs + if: always() + run: | + mkdir -p integration-reports/node-logs + for c in tron-mn-node1 tron-mn-node2 tron-mn-node3 tron-mn-mongodb; do + docker logs "$c" > "integration-reports/node-logs/${c}.log" 2>&1 || true + done + + - name: Tear down compose stack + if: always() + run: | + docker rm -f tron-mn-node1 tron-mn-node2 tron-mn-node3 tron-mn-mongodb 2>/dev/null || true + docker network rm multi-node_tron-net 2>/dev/null || true + docker rm -f integration-multinode 2>/dev/null || true + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v6 + with: + name: integration-multinode-report + path: integration-reports/ + if-no-files-found: warn From d6711462275da35a03a65b8d4666d55eece1c9ae Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 20 May 2026 16:14:50 +0800 Subject: [PATCH 3/9] ci: disable System Test auto-triggers (superseded by Integration Test) Both Integration Test workflows (Single Node / Multinode, Full) cover strictly more than the existing System Test runs. Drop System Test from the PR + push auto-trigger set so every commit doesn't pay the ~7 min runner cost twice for overlapping coverage. workflow_dispatch is kept so a regression suspect can still run stest manually from the Actions UI if needed. To re-enable for routine PR runs, uncomment the push + pull_request blocks at the top of the file. --- .github/workflows/system-test.yml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/system-test.yml b/.github/workflows/system-test.yml index f6184fb0efc..45f545cdc21 100644 --- a/.github/workflows/system-test.yml +++ b/.github/workflows/system-test.yml @@ -1,17 +1,23 @@ name: System Test +# Auto-triggers disabled: superseded by Integration Test (Single Node / Full) +# and Integration Test (Multinode / Full). Kept reachable via manual dispatch +# in case a regression suspects need a stest run against tronprotocol/system-test. +# To re-enable for routine PR runs, restore the push + pull_request blocks below. on: - push: - branches: [ 'master', 'release_**' ] - pull_request: - branches: [ 'develop', 'release_**' ] - types: [ opened, synchronize, reopened ] - paths-ignore: [ '**/*.md', '.gitignore', '**/.gitignore', '.editorconfig', - '.gitattributes', 'docs/**', 'CHANGELOG', '.github/ISSUE_TEMPLATE/**', - '.github/PULL_REQUEST_TEMPLATE/**', '.github/CODEOWNERS' ] + workflow_dispatch: + # push: + # branches: [ 'master', 'release_**' ] + # pull_request: + # branches: [ 'develop', 'release_**' ] + # types: [ opened, synchronize, reopened ] + # paths-ignore: [ '**/*.md', '.gitignore', '**/.gitignore', '.editorconfig', + # '.gitattributes', 'docs/**', 'CHANGELOG', '.github/ISSUE_TEMPLATE/**', + # '.github/PULL_REQUEST_TEMPLATE/**', '.github/CODEOWNERS' ] concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + # Only workflow_dispatch is enabled, so no PR context to key on — group by ref only. + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: From 196db87cd7e491d0ff51cef76e63d4163d6bea23 Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 20 May 2026 17:15:10 +0800 Subject: [PATCH 4/9] ci: delete System Test workflow (superseded by Integration Test) --- .github/workflows/system-test.yml | 101 ------------------------------ 1 file changed, 101 deletions(-) delete mode 100644 .github/workflows/system-test.yml diff --git a/.github/workflows/system-test.yml b/.github/workflows/system-test.yml deleted file mode 100644 index 45f545cdc21..00000000000 --- a/.github/workflows/system-test.yml +++ /dev/null @@ -1,101 +0,0 @@ -name: System Test - -# Auto-triggers disabled: superseded by Integration Test (Single Node / Full) -# and Integration Test (Multinode / Full). Kept reachable via manual dispatch -# in case a regression suspects need a stest run against tronprotocol/system-test. -# To re-enable for routine PR runs, restore the push + pull_request blocks below. -on: - workflow_dispatch: - # push: - # branches: [ 'master', 'release_**' ] - # pull_request: - # branches: [ 'develop', 'release_**' ] - # types: [ opened, synchronize, reopened ] - # paths-ignore: [ '**/*.md', '.gitignore', '**/.gitignore', '.editorconfig', - # '.gitattributes', 'docs/**', 'CHANGELOG', '.github/ISSUE_TEMPLATE/**', - # '.github/PULL_REQUEST_TEMPLATE/**', '.github/CODEOWNERS' ] - -concurrency: - # Only workflow_dispatch is enabled, so no PR context to key on — group by ref only. - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - system-test: - name: System Test (JDK 8 / x86_64) - runs-on: ubuntu-latest - timeout-minutes: 60 - - steps: - - name: Set up JDK 8 - uses: actions/setup-java@v5 - with: - java-version: '8' - distribution: 'temurin' - - - name: Clone system-test - uses: actions/checkout@v5 - with: - repository: tronprotocol/system-test - ref: release_workflow - path: system-test - - - name: Checkout java-tron - uses: actions/checkout@v5 - with: - path: java-tron - - - name: Cache Gradle packages - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-system-test-${{ hashFiles('java-tron/**/*.gradle', 'java-tron/**/gradle-wrapper.properties') }} - restore-keys: ${{ runner.os }}-gradle-system-test- - - - name: Build java-tron - working-directory: java-tron - run: ./gradlew clean build -x test --no-daemon - - - name: Copy config and start FullNode - run: | - cp system-test/testcase/src/test/resources/config-system-test.conf java-tron/ - cd java-tron - nohup java -jar build/libs/FullNode.jar --witness -c config-system-test.conf > fullnode.log 2>&1 & - echo "FullNode started, waiting for it to be ready..." - - MAX_ATTEMPTS=60 - INTERVAL=5 - for i in $(seq 1 $MAX_ATTEMPTS); do - if curl -s --fail "http://localhost:8090/wallet/getblockbynum?num=1" > /dev/null 2>&1; then - echo "FullNode is ready! (attempt $i)" - exit 0 - fi - echo "Waiting... (attempt $i/$MAX_ATTEMPTS)" - sleep $INTERVAL - done - - echo "FullNode failed to start within $((MAX_ATTEMPTS * INTERVAL)) seconds." - echo "=== FullNode log (last 50 lines) ===" - tail -50 fullnode.log || true - exit 1 - - - name: Run system tests - working-directory: system-test - run: | - if [ ! -f solcDIR/solc-linux-0.8.6 ]; then - echo "ERROR: solc binary not found at solcDIR/solc-linux-0.8.6" - exit 1 - fi - cp solcDIR/solc-linux-0.8.6 solcDIR/solc - ./gradlew clean --no-daemon - ./gradlew --info stest --no-daemon - - - name: Upload FullNode log - if: always() - uses: actions/upload-artifact@v6 - with: - name: fullnode-log - path: java-tron/fullnode.log - if-no-files-found: warn From 8ce5c51439527370dba383596d1918955a0efdb0 Mon Sep 17 00:00:00 2001 From: warku123 Date: Thu, 21 May 2026 16:56:21 +0800 Subject: [PATCH 5/9] DEMO: break getMaintenanceTimeInterval to prove CI gating Hardcodes getMaintenanceTimeInterval to return 6000L instead of reading from DB. The Integration Test workflows added in this PR should catch this protocol-level regression and turn CI red. REVERT BEFORE MERGE. --- .../java/org/tron/core/store/DynamicPropertiesStore.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java index 0f74f20d379..f707adf3117 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -1156,11 +1156,9 @@ public void saveMaintenanceTimeInterval(long timeInterval) { } public long getMaintenanceTimeInterval() { - return Optional.ofNullable(getUnchecked(MAINTENANCE_TIME_INTERVAL)) - .map(BytesCapsule::getData) - .map(ByteArray::toLong) - .orElseThrow( - () -> new IllegalArgumentException("not found MAINTENANCE_TIME_INTERVAL")); + // DEMO BREAK — REVERT BEFORE MERGE. Hardcodes a wrong value to prove + // the new Integration Test workflow catches protocol-level regressions. + return 6000L; } public void saveAccountUpgradeCost(long accountUpgradeCost) { From 758ad78a1bf1bd91d9e448ab23600a0ed04ca336 Mon Sep 17 00:00:00 2001 From: warku123 Date: Thu, 21 May 2026 17:28:12 +0800 Subject: [PATCH 6/9] Revert "DEMO: break getMaintenanceTimeInterval to prove CI gating" This reverts commit 8ce5c51439527370dba383596d1918955a0efdb0. --- .../java/org/tron/core/store/DynamicPropertiesStore.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java index f707adf3117..0f74f20d379 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -1156,9 +1156,11 @@ public void saveMaintenanceTimeInterval(long timeInterval) { } public long getMaintenanceTimeInterval() { - // DEMO BREAK — REVERT BEFORE MERGE. Hardcodes a wrong value to prove - // the new Integration Test workflow catches protocol-level regressions. - return 6000L; + return Optional.ofNullable(getUnchecked(MAINTENANCE_TIME_INTERVAL)) + .map(BytesCapsule::getData) + .map(ByteArray::toLong) + .orElseThrow( + () -> new IllegalArgumentException("not found MAINTENANCE_TIME_INTERVAL")); } public void saveAccountUpgradeCost(long accountUpgradeCost) { From 703c087bfb59a667eb038cc236d915ec0a2aacda Mon Sep 17 00:00:00 2001 From: warku123 Date: Thu, 21 May 2026 17:32:52 +0800 Subject: [PATCH 7/9] DEMO: break multinode witness rotation to prove multinode-specific CI gating Truncates ConsensusDelegate.getActiveWitnesses() to return only the first witness. Single-node setups have 1 witness so this is effectively a no-op there; multinode setups (3 witnesses) lose rotation entirely, which the Integration Test (Multinode Full) workflow should catch while the Single Node workflow stays green. REVERT BEFORE MERGE. --- .../main/java/org/tron/consensus/ConsensusDelegate.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/consensus/src/main/java/org/tron/consensus/ConsensusDelegate.java b/consensus/src/main/java/org/tron/consensus/ConsensusDelegate.java index ef3a35d1ec7..1be88ff81d6 100644 --- a/consensus/src/main/java/org/tron/consensus/ConsensusDelegate.java +++ b/consensus/src/main/java/org/tron/consensus/ConsensusDelegate.java @@ -85,7 +85,12 @@ public void saveActiveWitnesses(List addresses) { } public List getActiveWitnesses() { - return witnessScheduleStore.getActiveWitnesses(); + // DEMO BREAK — REVERT BEFORE MERGE. Truncate active witnesses to just the + // first one. Single-node setups (1 witness) are unaffected; multinode + // (3 witnesses) loses witness rotation, exposing the multinode workflow's + // unique value over single-node. + List all = witnessScheduleStore.getActiveWitnesses(); + return all.isEmpty() ? all : all.subList(0, 1); } public AccountCapsule getAccount(byte[] address) { From ccbc8f07376a9fc7457f266f1bcb217ab7ad54d1 Mon Sep 17 00:00:00 2001 From: warku123 Date: Thu, 21 May 2026 18:07:06 +0800 Subject: [PATCH 8/9] Revert "DEMO: break multinode witness rotation to prove multinode-specific CI gating" This reverts commit 703c087bfb59a667eb038cc236d915ec0a2aacda. --- .../main/java/org/tron/consensus/ConsensusDelegate.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/consensus/src/main/java/org/tron/consensus/ConsensusDelegate.java b/consensus/src/main/java/org/tron/consensus/ConsensusDelegate.java index 1be88ff81d6..ef3a35d1ec7 100644 --- a/consensus/src/main/java/org/tron/consensus/ConsensusDelegate.java +++ b/consensus/src/main/java/org/tron/consensus/ConsensusDelegate.java @@ -85,12 +85,7 @@ public void saveActiveWitnesses(List addresses) { } public List getActiveWitnesses() { - // DEMO BREAK — REVERT BEFORE MERGE. Truncate active witnesses to just the - // first one. Single-node setups (1 witness) are unaffected; multinode - // (3 witnesses) loses witness rotation, exposing the multinode workflow's - // unique value over single-node. - List all = witnessScheduleStore.getActiveWitnesses(); - return all.isEmpty() ? all : all.subList(0, 1); + return witnessScheduleStore.getActiveWitnesses(); } public AccountCapsule getAccount(byte[] address) { From eed9dad7d3f7dd2e72a37809899d755c6c5c9f0c Mon Sep 17 00:00:00 2001 From: warku123 Date: Thu, 21 May 2026 18:39:30 +0800 Subject: [PATCH 9/9] ci: sync pr-cancel workflow list with integration test rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit system-test.yml is removed in this PR and replaced by two new workflows (integration-test-single-node.yml + integration-test-multinode.yml). Update pr-cancel.yml's hard-coded workflow list to match: drop system-test.yml, add the two new ones. Also wrap each workflow iteration in try/catch so that a missing or renamed workflow file no longer takes down the whole cancel job — the remaining workflows still get processed and a clear "Skipping ..." line is logged for the offending one. Closes the resilience gap flagged in code review. --- .github/workflows/pr-cancel.yml | 66 +++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/.github/workflows/pr-cancel.yml b/.github/workflows/pr-cancel.yml index bbd0e68c235..3213026d3f9 100644 --- a/.github/workflows/pr-cancel.yml +++ b/.github/workflows/pr-cancel.yml @@ -13,43 +13,55 @@ jobs: if: github.event.pull_request.merged == false runs-on: ubuntu-latest steps: - - name: Cancel PR Build and System Test + - name: Cancel PR Build and Integration Tests uses: actions/github-script@v8 with: script: | - const workflows = ['pr-build.yml', 'system-test.yml', 'codeql.yml']; + const workflows = [ + 'pr-build.yml', + 'codeql.yml', + 'integration-test-single-node.yml', + 'integration-test-multinode.yml', + ]; const headSha = context.payload.pull_request.head.sha; const prNumber = context.payload.pull_request.number; for (const workflowId of workflows) { - for (const status of ['in_progress', 'queued']) { - const runs = await github.paginate( - github.rest.actions.listWorkflowRuns, - { - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: workflowId, - status, - event: 'pull_request', - per_page: 100, - }, - (response) => response.data.workflow_runs - ); - - for (const run of runs) { - if (!run) { - continue; - } - const prs = Array.isArray(run.pull_requests) ? run.pull_requests : []; - const isTargetPr = prs.length === 0 || prs.some((pr) => pr.number === prNumber); - if (run.head_sha === headSha && isTargetPr) { - await github.rest.actions.cancelWorkflowRun({ + // Wrap each workflow iteration so a missing / renamed file + // doesn't take down the whole cancel job — other workflows + // in the list still get processed. + try { + for (const status of ['in_progress', 'queued']) { + const runs = await github.paginate( + github.rest.actions.listWorkflowRuns, + { owner: context.repo.owner, repo: context.repo.repo, - run_id: run.id, - }); - console.log(`Cancelled ${workflowId} run #${run.id} (${status})`); + workflow_id: workflowId, + status, + event: 'pull_request', + per_page: 100, + }, + (response) => response.data.workflow_runs + ); + + for (const run of runs) { + if (!run) { + continue; + } + const prs = Array.isArray(run.pull_requests) ? run.pull_requests : []; + const isTargetPr = prs.length === 0 || prs.some((pr) => pr.number === prNumber); + if (run.head_sha === headSha && isTargetPr) { + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: run.id, + }); + console.log(`Cancelled ${workflowId} run #${run.id} (${status})`); + } } } + } catch (err) { + console.log(`Skipping ${workflowId}: ${err.message}`); } }