From 40986dad6907b89833835c1dd8e63d2b3db79e9a Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 11:37:52 -0700 Subject: [PATCH 1/9] PDP-1182: Add pull_request_target trigger to copyright-check.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable copyright-check to be used directly via org-level repository rulesets without requiring a per-repo pr-workflow.yaml caller. - Adds pull_request_target trigger (opened, edited, synchronize, reopened) - Retains workflow_call for backward compat with existing per-repo callers - Safe for fork PRs: copyrightcheck.py (from trusted pr-workflows repo) only reads fork files as text — no code execution risk - Worst case from malicious fork .copyrightconfig is check passes (policy bypass only, not security exploit); reviewed by maintainer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/copyright-check.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index e290f8e..df7854f 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -1,6 +1,15 @@ name: Copyright Validation on: + # Direct trigger for org-level repository rulesets — works for PRs from forks + # since pull_request_target runs with base repo context and write token. + # Fork code is only read as text by the trusted copyrightcheck.py script; + # no fork code is executed. Worst case from a malicious .copyrightconfig + # is the check passes (policy bypass), not code execution. + pull_request_target: + types: [opened, edited, synchronize, reopened] + + # Also support being called as a reusable workflow from individual repos workflow_call: jobs: From 71fa91d5d7bd5716b9576ec7cdb8721d2f9436f2 Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 12:07:23 -0700 Subject: [PATCH 2/9] PDP-1182: Replace git fetch/diff with GitHub API for changed files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixes SECCMP-1797 regression: git fetch failed with exit code 128 for private repos because persist-credentials:false removed the token after checkout, leaving no credentials for subsequent git fetch - Use gh api /pulls/{n}/files instead — no git credentials needed, works for public, private, and fork PRs equally - Use IFS= read -r to correctly handle filenames with spaces - Deleted files naturally filtered by [ -f target-repo/$f ] check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/copyright-check.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index df7854f..2ffd481 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -63,14 +63,15 @@ jobs: - name: Get changed files id: changed-files env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - BASE_REF: ${{ github.event.pull_request.base.ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} run: | - cd target-repo - git fetch origin "$BASE_REF" - git diff --name-only --diff-filter=AMR "$BASE_SHA" "$HEAD_SHA" | while read f; do [ -f "$f" ] && echo "$f"; done > ../files_to_check.txt - count=$(wc -l < ../files_to_check.txt | tr -d ' ') + gh api "repos/${GH_REPO}/pulls/${PR_NUMBER}/files" --paginate \ + --jq '.[].filename' | while IFS= read -r f; do + [ -f "target-repo/$f" ] && echo "$f" + done > files_to_check.txt + count=$(wc -l < files_to_check.txt | tr -d ' ') if [ "$count" -eq 0 ]; then echo "skip-validation=true" >> $GITHUB_OUTPUT; else echo "skip-validation=false" >> $GITHUB_OUTPUT; fi echo "files-count=$count" >> $GITHUB_OUTPUT From cb3507314462fda44c972294c3fc3d0b137b1cf0 Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 12:24:54 -0700 Subject: [PATCH 3/9] PDP-1182: Address PR review comments - Remove pull-requests: write permission (only issues: write needed for issue comment APIs; pull-requests: write was unnecessary scope) - Use refs/pull/N/head for fork PR checkout (more reliable than SHA fetch for pull_request_target; explicit named ref always resolvable) - Use --files-from-stdin to pass files to copyrightcheck.py (eliminates shell word-splitting and argument injection via filenames starting with '--') Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/copyright-check.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index 2ffd481..02c4771 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -18,14 +18,13 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - pull-requests: write issues: write steps: - name: Checkout PR head uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: refs/pull/${{ github.event.pull_request.number }}/head path: target-repo persist-credentials: false @@ -87,8 +86,8 @@ jobs: cfg="$CONFIG_FILE" [ -f "$script" ] || { echo "script missing"; exit 1; } chmod +x "$script" - files=$(tr '\n' ' ' < files_to_check.txt) - python3 "$script" --config "$cfg" --working-dir target-repo $files > validation_output.txt 2>&1 + python3 "$script" --config "$cfg" --working-dir target-repo \ + --files-from-stdin < files_to_check.txt > validation_output.txt 2>&1 ec=$? if [ $ec -eq 0 ]; then echo "status=success" >> $GITHUB_OUTPUT; else echo "status=failed" >> $GITHUB_OUTPUT; fi exit $ec From 9b8b737199abd63d1328e8332fd041182b73a059 Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 12:51:58 -0700 Subject: [PATCH 4/9] PDP-1182: Add pull-requests: read permission for gh api PR files endpoint The explicit permissions block restricts the GITHUB_TOKEN to only listed scopes. The 'gh api repos/.../pulls/.../files' call uses the Pull Requests API which requires pull-requests: read. Without it the call can 403 on private repos. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/copyright-check.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index 02c4771..68412a5 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -19,6 +19,7 @@ jobs: permissions: contents: read issues: write + pull-requests: read steps: - name: Checkout PR head From 7b453f94bc20aea9879f02cb0647d94f5199f521 Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 13:03:23 -0700 Subject: [PATCH 5/9] fix: use if/then/fi to avoid pipefail exit on deleted-only PRs When a PR only deletes files, '[ -f target-repo/$f ] && echo $f' returns exit code 1 (false && skipped-cmd). With bash pipefail this causes 'Get changed files' to fail before writing skip-validation=true, making all always() steps run with empty VALIDATION_STATUS. Using if/then/fi instead exits 0 when condition is false (no else), so the pipeline succeeds and the empty-file-list path works correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/copyright-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index 68412a5..45ccd3c 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -69,7 +69,7 @@ jobs: run: | gh api "repos/${GH_REPO}/pulls/${PR_NUMBER}/files" --paginate \ --jq '.[].filename' | while IFS= read -r f; do - [ -f "target-repo/$f" ] && echo "$f" + if [ -f "target-repo/$f" ]; then echo "$f"; fi done > files_to_check.txt count=$(wc -l < files_to_check.txt | tr -d ' ') if [ "$count" -eq 0 ]; then echo "skip-validation=true" >> $GITHUB_OUTPUT; else echo "skip-validation=false" >> $GITHUB_OUTPUT; fi From fd2c9827fabe9f9888e30fc2e44bbe533a3c3c4c Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 13:23:49 -0700 Subject: [PATCH 6/9] fix: set -e exits before status write; skip PR comment for pull_request_target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs fixed: 1. set -e: python3 exits 1 causing bash to exit before ec=$? and status=failed can be written to GITHUB_OUTPUT. Use &&/|| pattern so status is always written before the exit. 2. PR comment 403: org required workflows (pull_request_target via org ruleset) receive a read-only token for the triggering repo — createComment/updateComment always 403. Skip the comment step for pull_request_target and rely on Job Summary instead. workflow_call path still posts PR comments (token has write access to calling repo). Also improves the fallback error message to point to Job Summary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/copyright-check.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index 45ccd3c..b1d193a 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -88,10 +88,9 @@ jobs: [ -f "$script" ] || { echo "script missing"; exit 1; } chmod +x "$script" python3 "$script" --config "$cfg" --working-dir target-repo \ - --files-from-stdin < files_to_check.txt > validation_output.txt 2>&1 - ec=$? - if [ $ec -eq 0 ]; then echo "status=success" >> $GITHUB_OUTPUT; else echo "status=failed" >> $GITHUB_OUTPUT; fi - exit $ec + --files-from-stdin < files_to_check.txt > validation_output.txt 2>&1 \ + && echo "status=success" >> "$GITHUB_OUTPUT" \ + || { echo "status=failed" >> "$GITHUB_OUTPUT"; exit 1; } - name: Extract Markdown summary if: always() && steps.changed-files.outputs.skip-validation != 'true' @@ -104,7 +103,10 @@ jobs: - name: Post / Update PR comment with summary id: pr-comment - if: always() && steps.changed-files.outputs.skip-validation != 'true' + # workflow_call: token is scoped to the calling repo — write access works. + # pull_request_target (org ruleset): token is read-only for the triggering repo — + # createComment/updateComment will 403. Skip and rely on Job Summary instead. + if: always() && steps.changed-files.outputs.skip-validation != 'true' && github.event_name == 'workflow_call' uses: actions/github-script@v7 env: VALIDATION_STATUS: ${{ steps.validate.outputs.status }} @@ -148,7 +150,7 @@ jobs: if [ "$COMMENT_ACTION" = "updated" ] || [ "$COMMENT_ACTION" = "created" ]; then echo "::error title=Copyright Validation Failed::See the $COMMENT_ACTION PR comment for detailed results."; else - echo "::error title=Copyright Validation Failed::See the PR comment (unavailable or failed to post)."; + echo "::error title=Copyright Validation Failed::Copyright headers are missing or invalid — see the Job Summary for details."; fi exit 1 fi From 62179e690ad947d7fbe1ba15d316e106cfc3dd03 Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 13:31:25 -0700 Subject: [PATCH 7/9] fix: correct misleading 'write token' comment; derive commit SHA from checkout - Line 5 comment: 'write token' was misleading after the token was scoped down to least privilege. Updated to accurately state that GITHUB_TOKEN is explicitly scoped in the job permissions block. - COPYRIGHT_CHECK_COMMIT_SHA: moved from env: block (GitHub context) to git -C target-repo rev-parse HEAD in the run: script. This guarantees the reported SHA matches what was actually checked out, not the event payload SHA which can theoretically diverge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/copyright-check.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index b1d193a..6e45288 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -2,7 +2,8 @@ name: Copyright Validation on: # Direct trigger for org-level repository rulesets — works for PRs from forks - # since pull_request_target runs with base repo context and write token. + # since pull_request_target runs with base repo context. The GITHUB_TOKEN is + # explicitly scoped to least privilege in the job permissions block below. # Fork code is only read as text by the trusted copyrightcheck.py script; # no fork code is executed. Worst case from a malicious .copyrightconfig # is the check passes (policy bypass), not code execution. @@ -80,13 +81,13 @@ jobs: if: steps.changed-files.outputs.skip-validation != 'true' continue-on-error: true env: - COPYRIGHT_CHECK_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CONFIG_FILE: ${{ steps.setup-config.outputs.config-file }} run: | script="pr-workflows/scripts/copyrightcheck.py" cfg="$CONFIG_FILE" [ -f "$script" ] || { echo "script missing"; exit 1; } chmod +x "$script" + export COPYRIGHT_CHECK_COMMIT_SHA=$(git -C target-repo rev-parse HEAD) python3 "$script" --config "$cfg" --working-dir target-repo \ --files-from-stdin < files_to_check.txt > validation_output.txt 2>&1 \ && echo "status=success" >> "$GITHUB_OUTPUT" \ From dc32a2bda1063039b91f58e9e67b424b79651a23 Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 13:44:48 -0700 Subject: [PATCH 8/9] docs: add comment explaining why issues: write is retained for both event paths GitHub Actions has no per-event conditional permissions within a single job. Splitting into two jobs (one per event type) would require artifact sharing for summary.md/validation_output.txt and adds complexity for minimal gain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/copyright-check.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index 6e45288..36c0e86 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -19,8 +19,12 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - issues: write pull-requests: read + # issues: write is needed for the PR comment step (workflow_call path only). + # pull_request_target skips that step, but GitHub Actions has no per-event + # conditional permissions within a single job — splitting into two jobs would + # require artifact sharing and add significant complexity for minimal gain. + issues: write steps: - name: Checkout PR head From 500c96b7bbfdd01ba66a29368d322404d19ae3a2 Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 16:18:03 -0700 Subject: [PATCH 9/9] security: pin all actions to commit SHAs (SECCMP-1797) Pin actions/checkout@v4, actions/setup-python@v4, and actions/github-script@v7 to immutable commit SHAs to prevent supply chain attacks. - actions/checkout -> 34e114876b0b11c390a56381ad16ebd13914f8d5 - actions/setup-python -> 7f4fc3e22c37d6ff65e88745f38bd3157c663f7c - actions/github-script -> f28e40c7f34bde8b3046d885e986cb6290c5673b Note: reverts accidental regression introduced in prior SHA-pin attempt that inadvertently reverted pull_request_target trigger, refs/pull/N/head checkout ref, pull-requests: read permission, and --files-from-stdin argument safety. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/copyright-check.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index 36c0e86..e4460e6 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -28,14 +28,14 @@ jobs: steps: - name: Checkout PR head - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: ref: refs/pull/${{ github.event.pull_request.number }}/head path: target-repo persist-credentials: false - name: Checkout pr-workflows repo - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: repository: ${{ github.repository_owner }}/pr-workflows ref: main @@ -61,7 +61,7 @@ jobs: echo "config-file=$cfg" >> $GITHUB_OUTPUT - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4 with: python-version: '3.11' @@ -112,7 +112,7 @@ jobs: # pull_request_target (org ruleset): token is read-only for the triggering repo — # createComment/updateComment will 403. Skip and rely on Job Summary instead. if: always() && steps.changed-files.outputs.skip-validation != 'true' && github.event_name == 'workflow_call' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 env: VALIDATION_STATUS: ${{ steps.validate.outputs.status }} with: