From 0894d796b4fc8a705495cdf5132ab84a178bdc05 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Mar 2026 10:23:03 +0000 Subject: [PATCH 1/4] ci: use pull_request_target to fix "Resource not accessible by integration" The pull_request trigger gives GITHUB_TOKEN read-only permissions in certain contexts (fork PRs, restrictive repo/org settings), causing peter-evans/create-or-update-comment and gh api DELETE calls to fail. Switch both verify_data_Integrity.yml and visualize_stopping_patterns.yml to pull_request_target, which runs with the base repo's token permissions. Explicitly checkout the PR head SHA so the correct code is validated. https://claude.ai/code/session_01Lr3k5y8UYcH26a8hndo8ek --- .github/workflows/verify_data_Integrity.yml | 10 ++++++---- .github/workflows/visualize_stopping_patterns.yml | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/verify_data_Integrity.yml b/.github/workflows/verify_data_Integrity.yml index 25bb32e5..3bf00876 100644 --- a/.github/workflows/verify_data_Integrity.yml +++ b/.github/workflows/verify_data_Integrity.yml @@ -1,6 +1,6 @@ on: workflow_dispatch: - pull_request: + pull_request_target: paths: - "data/*.csv" push: @@ -20,6 +20,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Run data validator id: validate @@ -31,7 +33,7 @@ jobs: fi - name: Find existing comment - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request_target' uses: peter-evans/find-comment@v3 id: find_comment with: @@ -40,7 +42,7 @@ jobs: body-includes: "" - name: Post or update validation failure comment - if: github.event_name == 'pull_request' && steps.validate.outputs.result == 'failure' + if: github.event_name == 'pull_request_target' && steps.validate.outputs.result == 'failure' uses: peter-evans/create-or-update-comment@v4 with: issue-number: ${{ github.event.pull_request.number }} @@ -49,7 +51,7 @@ jobs: edit-mode: replace - name: Delete comment if validation passed - if: github.event_name == 'pull_request' && steps.validate.outputs.result == 'success' && steps.find_comment.outputs.comment-id != '' + if: github.event_name == 'pull_request_target' && steps.validate.outputs.result == 'success' && steps.find_comment.outputs.comment-id != '' run: gh api repos/${{ github.repository }}/issues/comments/${{ steps.find_comment.outputs.comment-id }} -X DELETE env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/visualize_stopping_patterns.yml b/.github/workflows/visualize_stopping_patterns.yml index cb58af22..3eebf3f5 100644 --- a/.github/workflows/visualize_stopping_patterns.yml +++ b/.github/workflows/visualize_stopping_patterns.yml @@ -1,7 +1,7 @@ name: Visualize Stopping Patterns on: - pull_request: + pull_request_target: types: [opened, synchronize, reopened] paths: - "data/*.csv" @@ -18,6 +18,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: + ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Fetch base branch From bb5386adfa22c7a73677ce5a5c5ff3e5cffaf5cb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Mar 2026 10:38:38 +0000 Subject: [PATCH 2/4] ci: fix privilege escalation by splitting into two-workflow pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both verify_data_Integrity.yml and visualize_stopping_patterns.yml used pull_request_target while checking out and executing untrusted PR code (cargo run, python3) with write permissions — a known code-execution escalation path. Fix by splitting each into two workflows: - Runner workflow: triggered by pull_request (read-only token), executes untrusted PR code, uploads results as artifacts - Comment workflow: triggered by workflow_run (write permissions), only downloads trusted artifact data and manages PR comments This ensures untrusted code never runs with elevated permissions. https://claude.ai/code/session_01Lr3k5y8UYcH26a8hndo8ek --- .github/workflows/verify_data_Integrity.yml | 41 ++++++-------- .../verify_data_integrity_comment.yml | 53 +++++++++++++++++++ .../workflows/visualize_stopping_patterns.yml | 40 ++++++-------- .../visualize_stopping_patterns_comment.yml | 53 +++++++++++++++++++ 4 files changed, 136 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/verify_data_integrity_comment.yml create mode 100644 .github/workflows/visualize_stopping_patterns_comment.yml diff --git a/.github/workflows/verify_data_Integrity.yml b/.github/workflows/verify_data_Integrity.yml index 3bf00876..beca87b8 100644 --- a/.github/workflows/verify_data_Integrity.yml +++ b/.github/workflows/verify_data_Integrity.yml @@ -1,6 +1,6 @@ on: workflow_dispatch: - pull_request_target: + pull_request: paths: - "data/*.csv" push: @@ -11,8 +11,6 @@ name: Verify station data integrity permissions: contents: read - pull-requests: write - issues: write jobs: verify_migration_data: @@ -20,8 +18,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Run data validator id: validate @@ -32,29 +28,22 @@ jobs: echo "result=failure" >> "$GITHUB_OUTPUT" fi - - name: Find existing comment - if: github.event_name == 'pull_request_target' - uses: peter-evans/find-comment@v3 - id: find_comment - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: "github-actions[bot]" - body-includes: "" + - name: Save metadata for comment workflow + if: always() && github.event_name == 'pull_request' + run: | + mkdir -p /tmp/validation-artifacts + echo "${{ github.event.pull_request.number }}" > /tmp/validation-artifacts/pr_number + echo "${{ steps.validate.outputs.result }}" > /tmp/validation-artifacts/result + if [ -f /tmp/validation_report.md ]; then + cp /tmp/validation_report.md /tmp/validation-artifacts/ + fi - - name: Post or update validation failure comment - if: github.event_name == 'pull_request_target' && steps.validate.outputs.result == 'failure' - uses: peter-evans/create-or-update-comment@v4 + - name: Upload validation artifacts + if: always() && github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 with: - issue-number: ${{ github.event.pull_request.number }} - comment-id: ${{ steps.find_comment.outputs.comment-id }} - body-path: /tmp/validation_report.md - edit-mode: replace - - - name: Delete comment if validation passed - if: github.event_name == 'pull_request_target' && steps.validate.outputs.result == 'success' && steps.find_comment.outputs.comment-id != '' - run: gh api repos/${{ github.repository }}/issues/comments/${{ steps.find_comment.outputs.comment-id }} -X DELETE - env: - GH_TOKEN: ${{ github.token }} + name: validation-result + path: /tmp/validation-artifacts/ - name: Fail job if validation failed if: steps.validate.outputs.result == 'failure' diff --git a/.github/workflows/verify_data_integrity_comment.yml b/.github/workflows/verify_data_integrity_comment.yml new file mode 100644 index 00000000..70f0746b --- /dev/null +++ b/.github/workflows/verify_data_integrity_comment.yml @@ -0,0 +1,53 @@ +name: Post data validation comment + +on: + workflow_run: + workflows: ["Verify station data integrity"] + types: [completed] + +permissions: + pull-requests: write + issues: write + +jobs: + comment: + name: Post validation result comment + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: validation-result + path: /tmp/validation-artifacts + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Read metadata + id: meta + run: | + echo "pr_number=$(cat /tmp/validation-artifacts/pr_number)" >> "$GITHUB_OUTPUT" + echo "result=$(cat /tmp/validation-artifacts/result)" >> "$GITHUB_OUTPUT" + + - name: Find existing comment + uses: peter-evans/find-comment@v3 + id: find_comment + with: + issue-number: ${{ steps.meta.outputs.pr_number }} + comment-author: "github-actions[bot]" + body-includes: "" + + - name: Post or update validation failure comment + if: steps.meta.outputs.result == 'failure' + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ steps.meta.outputs.pr_number }} + comment-id: ${{ steps.find_comment.outputs.comment-id }} + body-path: /tmp/validation-artifacts/validation_report.md + edit-mode: replace + + - name: Delete comment if validation passed + if: steps.meta.outputs.result == 'success' && steps.find_comment.outputs.comment-id != '' + run: gh api repos/${{ github.repository }}/issues/comments/${{ steps.find_comment.outputs.comment-id }} -X DELETE + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/visualize_stopping_patterns.yml b/.github/workflows/visualize_stopping_patterns.yml index 3eebf3f5..984bd293 100644 --- a/.github/workflows/visualize_stopping_patterns.yml +++ b/.github/workflows/visualize_stopping_patterns.yml @@ -1,15 +1,13 @@ name: Visualize Stopping Patterns on: - pull_request_target: + pull_request: types: [opened, synchronize, reopened] paths: - "data/*.csv" permissions: contents: read - pull-requests: write - issues: write jobs: visualize: @@ -18,7 +16,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Fetch base branch @@ -35,26 +32,19 @@ jobs: BASE_REF="origin/${{ github.event.pull_request.base.ref }}" \ python3 .github/scripts/visualize_stopping_patterns.py - - name: Find existing comment + - name: Save metadata for comment workflow if: always() - uses: peter-evans/find-comment@v3 - id: find_comment - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: "github-actions[bot]" - body-includes: "" - - - name: Post or update comment - if: steps.visualize.outputs.has_changes == 'true' - uses: peter-evans/create-or-update-comment@v4 + run: | + mkdir -p /tmp/visualize-artifacts + echo "${{ github.event.pull_request.number }}" > /tmp/visualize-artifacts/pr_number + echo "${{ steps.visualize.outputs.has_changes }}" > /tmp/visualize-artifacts/has_changes + if [ -f /tmp/visualization_comment.md ]; then + cp /tmp/visualization_comment.md /tmp/visualize-artifacts/ + fi + + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 with: - issue-number: ${{ github.event.pull_request.number }} - comment-id: ${{ steps.find_comment.outputs.comment-id }} - body-path: /tmp/visualization_comment.md - edit-mode: replace - - - name: Delete comment if no changes - if: steps.visualize.outputs.has_changes == 'false' && steps.find_comment.outputs.comment-id != '' - run: gh api repos/${{ github.repository }}/issues/comments/${{ steps.find_comment.outputs.comment-id }} -X DELETE - env: - GH_TOKEN: ${{ github.token }} + name: visualization-result + path: /tmp/visualize-artifacts/ diff --git a/.github/workflows/visualize_stopping_patterns_comment.yml b/.github/workflows/visualize_stopping_patterns_comment.yml new file mode 100644 index 00000000..4157d3e3 --- /dev/null +++ b/.github/workflows/visualize_stopping_patterns_comment.yml @@ -0,0 +1,53 @@ +name: Post stopping pattern visualization comment + +on: + workflow_run: + workflows: ["Visualize Stopping Patterns"] + types: [completed] + +permissions: + pull-requests: write + issues: write + +jobs: + comment: + name: Post visualization comment + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: visualization-result + path: /tmp/visualize-artifacts + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Read metadata + id: meta + run: | + echo "pr_number=$(cat /tmp/visualize-artifacts/pr_number)" >> "$GITHUB_OUTPUT" + echo "has_changes=$(cat /tmp/visualize-artifacts/has_changes)" >> "$GITHUB_OUTPUT" + + - name: Find existing comment + uses: peter-evans/find-comment@v3 + id: find_comment + with: + issue-number: ${{ steps.meta.outputs.pr_number }} + comment-author: "github-actions[bot]" + body-includes: "" + + - name: Post or update comment + if: steps.meta.outputs.has_changes == 'true' + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ steps.meta.outputs.pr_number }} + comment-id: ${{ steps.find_comment.outputs.comment-id }} + body-path: /tmp/visualize-artifacts/visualization_comment.md + edit-mode: replace + + - name: Delete comment if no changes + if: steps.meta.outputs.has_changes == 'false' && steps.find_comment.outputs.comment-id != '' + run: gh api repos/${{ github.repository }}/issues/comments/${{ steps.find_comment.outputs.comment-id }} -X DELETE + env: + GH_TOKEN: ${{ github.token }} From c3357ba9ca5a8a157750b94def0af834358586e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Mar 2026 10:49:26 +0000 Subject: [PATCH 3/4] ci: add actions: read permission for cross-run artifact download actions/download-artifact@v4 with run-id requires actions: read to access artifacts from the triggering workflow run. https://claude.ai/code/session_01Lr3k5y8UYcH26a8hndo8ek --- .github/workflows/verify_data_integrity_comment.yml | 1 + .github/workflows/visualize_stopping_patterns_comment.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/verify_data_integrity_comment.yml b/.github/workflows/verify_data_integrity_comment.yml index 70f0746b..c138e9c8 100644 --- a/.github/workflows/verify_data_integrity_comment.yml +++ b/.github/workflows/verify_data_integrity_comment.yml @@ -6,6 +6,7 @@ on: types: [completed] permissions: + actions: read pull-requests: write issues: write diff --git a/.github/workflows/visualize_stopping_patterns_comment.yml b/.github/workflows/visualize_stopping_patterns_comment.yml index 4157d3e3..96532f30 100644 --- a/.github/workflows/visualize_stopping_patterns_comment.yml +++ b/.github/workflows/visualize_stopping_patterns_comment.yml @@ -6,6 +6,7 @@ on: types: [completed] permissions: + actions: read pull-requests: write issues: write From 6280bac7292387ecb5fdb27cb0c14209e9bf4dc8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Mar 2026 11:06:24 +0000 Subject: [PATCH 4/4] ci: use trusted event context for PR number and harden metadata reads - Use github.event.workflow_run.pull_requests[0].number instead of reading PR number from artifact files (avoids trusting artifact data) - Add conclusion == 'success' gate to visualization comment workflow so it only posts when the source workflow succeeded - Add existence/non-empty checks for metadata files with clear error messages or safe defaults https://claude.ai/code/session_01Lr3k5y8UYcH26a8hndo8ek --- .../workflows/verify_data_integrity_comment.yml | 13 +++++++++---- .../visualize_stopping_patterns_comment.yml | 17 ++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/workflows/verify_data_integrity_comment.yml b/.github/workflows/verify_data_integrity_comment.yml index c138e9c8..5c5e8868 100644 --- a/.github/workflows/verify_data_integrity_comment.yml +++ b/.github/workflows/verify_data_integrity_comment.yml @@ -27,14 +27,19 @@ jobs: - name: Read metadata id: meta run: | - echo "pr_number=$(cat /tmp/validation-artifacts/pr_number)" >> "$GITHUB_OUTPUT" - echo "result=$(cat /tmp/validation-artifacts/result)" >> "$GITHUB_OUTPUT" + echo "pr_number=${{ github.event.workflow_run.pull_requests[0].number }}" >> "$GITHUB_OUTPUT" + if [ -s /tmp/validation-artifacts/result ]; then + echo "result=$(cat /tmp/validation-artifacts/result)" >> "$GITHUB_OUTPUT" + else + echo "::error::Missing or empty result metadata" + exit 1 + fi - name: Find existing comment uses: peter-evans/find-comment@v3 id: find_comment with: - issue-number: ${{ steps.meta.outputs.pr_number }} + issue-number: ${{ github.event.workflow_run.pull_requests[0].number }} comment-author: "github-actions[bot]" body-includes: "" @@ -42,7 +47,7 @@ jobs: if: steps.meta.outputs.result == 'failure' uses: peter-evans/create-or-update-comment@v4 with: - issue-number: ${{ steps.meta.outputs.pr_number }} + issue-number: ${{ github.event.workflow_run.pull_requests[0].number }} comment-id: ${{ steps.find_comment.outputs.comment-id }} body-path: /tmp/validation-artifacts/validation_report.md edit-mode: replace diff --git a/.github/workflows/visualize_stopping_patterns_comment.yml b/.github/workflows/visualize_stopping_patterns_comment.yml index 96532f30..2fe0bea3 100644 --- a/.github/workflows/visualize_stopping_patterns_comment.yml +++ b/.github/workflows/visualize_stopping_patterns_comment.yml @@ -14,7 +14,9 @@ jobs: comment: name: Post visualization comment runs-on: ubuntu-latest - if: github.event.workflow_run.event == 'pull_request' + if: >- + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'pull_request' steps: - name: Download artifacts uses: actions/download-artifact@v4 @@ -27,14 +29,19 @@ jobs: - name: Read metadata id: meta run: | - echo "pr_number=$(cat /tmp/visualize-artifacts/pr_number)" >> "$GITHUB_OUTPUT" - echo "has_changes=$(cat /tmp/visualize-artifacts/has_changes)" >> "$GITHUB_OUTPUT" + echo "pr_number=${{ github.event.workflow_run.pull_requests[0].number }}" >> "$GITHUB_OUTPUT" + if [ -s /tmp/visualize-artifacts/has_changes ]; then + echo "has_changes=$(cat /tmp/visualize-artifacts/has_changes)" >> "$GITHUB_OUTPUT" + else + echo "::warning::has_changes metadata missing or empty, defaulting to false" + echo "has_changes=false" >> "$GITHUB_OUTPUT" + fi - name: Find existing comment uses: peter-evans/find-comment@v3 id: find_comment with: - issue-number: ${{ steps.meta.outputs.pr_number }} + issue-number: ${{ github.event.workflow_run.pull_requests[0].number }} comment-author: "github-actions[bot]" body-includes: "" @@ -42,7 +49,7 @@ jobs: if: steps.meta.outputs.has_changes == 'true' uses: peter-evans/create-or-update-comment@v4 with: - issue-number: ${{ steps.meta.outputs.pr_number }} + issue-number: ${{ github.event.workflow_run.pull_requests[0].number }} comment-id: ${{ steps.find_comment.outputs.comment-id }} body-path: /tmp/visualize-artifacts/visualization_comment.md edit-mode: replace