11name : Copyright Validation
22
33on :
4+ # Direct trigger for org-level repository rulesets — works for PRs from forks
5+ # since pull_request_target runs with base repo context. The GITHUB_TOKEN is
6+ # explicitly scoped to least privilege in the job permissions block below.
7+ # Fork code is only read as text by the trusted copyrightcheck.py script;
8+ # no fork code is executed. Worst case from a malicious .copyrightconfig
9+ # is the check passes (policy bypass), not code execution.
10+ pull_request_target :
11+ types : [opened, edited, synchronize, reopened]
12+
13+ # Also support being called as a reusable workflow from individual repos
414 workflow_call :
515
616jobs :
919 runs-on : ubuntu-latest
1020 permissions :
1121 contents : read
12- pull-requests : write
22+ pull-requests : read
23+ # issues: write is needed for the PR comment step (workflow_call path only).
24+ # pull_request_target skips that step, but GitHub Actions has no per-event
25+ # conditional permissions within a single job — splitting into two jobs would
26+ # require artifact sharing and add significant complexity for minimal gain.
1327 issues : write
1428
1529 steps :
1630 - name : Checkout PR head
17- uses : actions/checkout@v4
31+ uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
1832 with :
19- ref : ${{ github.event.pull_request.head.sha }}
33+ ref : refs/pull/ ${{ github.event.pull_request.number }}/head
2034 path : target-repo
2135 persist-credentials : false
2236
2337 - name : Checkout pr-workflows repo
24- uses : actions/checkout@v4
38+ uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
2539 with :
2640 repository : ${{ github.repository_owner }}/pr-workflows
2741 ref : main
@@ -47,21 +61,22 @@ jobs:
4761 echo "config-file=$cfg" >> $GITHUB_OUTPUT
4862
4963 - name : Set up Python
50- uses : actions/setup-python@v4
64+ uses : actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4
5165 with :
5266 python-version : ' 3.11'
5367
5468 - name : Get changed files
5569 id : changed-files
5670 env :
57- BASE_SHA : ${{ github.event.pull_request.base.sha }}
58- HEAD_SHA : ${{ github.event.pull_request.head.sha }}
59- BASE_REF : ${{ github.event.pull_request.base.ref }}
71+ PR_NUMBER : ${{ github.event.pull_request.number }}
72+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
73+ GH_REPO : ${{ github.repository }}
6074 run : |
61- cd target-repo
62- git fetch origin "$BASE_REF"
63- git diff --name-only --diff-filter=AMR "$BASE_SHA" "$HEAD_SHA" | while read f; do [ -f "$f" ] && echo "$f"; done > ../files_to_check.txt
64- count=$(wc -l < ../files_to_check.txt | tr -d ' ')
75+ gh api "repos/${GH_REPO}/pulls/${PR_NUMBER}/files" --paginate \
76+ --jq '.[].filename' | while IFS= read -r f; do
77+ if [ -f "target-repo/$f" ]; then echo "$f"; fi
78+ done > files_to_check.txt
79+ count=$(wc -l < files_to_check.txt | tr -d ' ')
6580 if [ "$count" -eq 0 ]; then echo "skip-validation=true" >> $GITHUB_OUTPUT; else echo "skip-validation=false" >> $GITHUB_OUTPUT; fi
6681 echo "files-count=$count" >> $GITHUB_OUTPUT
6782
@@ -70,18 +85,17 @@ jobs:
7085 if : steps.changed-files.outputs.skip-validation != 'true'
7186 continue-on-error : true
7287 env :
73- COPYRIGHT_CHECK_COMMIT_SHA : ${{ github.event.pull_request.head.sha }}
7488 CONFIG_FILE : ${{ steps.setup-config.outputs.config-file }}
7589 run : |
7690 script="pr-workflows/scripts/copyrightcheck.py"
7791 cfg="$CONFIG_FILE"
7892 [ -f "$script" ] || { echo "script missing"; exit 1; }
7993 chmod +x "$script"
80- files =$(tr '\n' ' ' < files_to_check.txt )
81- python3 "$script" --config "$cfg" --working-dir target-repo $files > validation_output.txt 2>&1
82- ec=$?
83- if [ $ec -eq 0 ]; then echo "status=success" >> $GITHUB_OUTPUT; else echo "status=failed" >> $GITHUB_OUTPUT; fi
84- exit $ec
94+ export COPYRIGHT_CHECK_COMMIT_SHA =$(git -C target-repo rev-parse HEAD )
95+ python3 "$script" --config "$cfg" --working-dir target-repo \
96+ --files-from-stdin < files_to_check.txt > validation_output.txt 2>&1 \
97+ && echo "status=success" >> " $GITHUB_OUTPUT" \
98+ || { echo "status=failed" >> "$GITHUB_OUTPUT"; exit 1; }
8599
86100 - name : Extract Markdown summary
87101 if : always() && steps.changed-files.outputs.skip-validation != 'true'
@@ -94,8 +108,11 @@ jobs:
94108
95109 - name : Post / Update PR comment with summary
96110 id : pr-comment
97- if : always() && steps.changed-files.outputs.skip-validation != 'true'
98- uses : actions/github-script@v7
111+ # workflow_call: token is scoped to the calling repo — write access works.
112+ # pull_request_target (org ruleset): token is read-only for the triggering repo —
113+ # createComment/updateComment will 403. Skip and rely on Job Summary instead.
114+ if : always() && steps.changed-files.outputs.skip-validation != 'true' && github.event_name == 'workflow_call'
115+ uses : actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
99116 env :
100117 VALIDATION_STATUS : ${{ steps.validate.outputs.status }}
101118 with :
@@ -138,7 +155,7 @@ jobs:
138155 if [ "$COMMENT_ACTION" = "updated" ] || [ "$COMMENT_ACTION" = "created" ]; then
139156 echo "::error title=Copyright Validation Failed::See the $COMMENT_ACTION PR comment for detailed results.";
140157 else
141- echo "::error title=Copyright Validation Failed::See the PR comment (unavailable or failed to post) .";
158+ echo "::error title=Copyright Validation Failed::Copyright headers are missing or invalid — see the Job Summary for details .";
142159 fi
143160 exit 1
144161 fi
0 commit comments