|
1 | 1 | #!/usr/bin/env python3 |
| 2 | +import json |
2 | 3 | import os |
3 | 4 | import sys |
4 | 5 | import subprocess |
5 | 6 | import re |
6 | | -from github import Github, Auth # type: ignore |
| 7 | +from github import Github, Auth, GithubException # type: ignore |
7 | 8 |
|
8 | 9 |
|
9 | 10 | # Constants for message titles |
@@ -96,73 +97,102 @@ def add_job_summary() -> int: |
96 | 97 | return 0 if result_text is None else 1 |
97 | 98 |
|
98 | 99 |
|
| 100 | +def is_fork_pr() -> bool: |
| 101 | + """Returns True when the triggering PR originates from a forked repository.""" |
| 102 | + event_path = os.getenv("GITHUB_EVENT_PATH") |
| 103 | + if not event_path: |
| 104 | + return False |
| 105 | + try: |
| 106 | + with open(event_path, "r") as f: |
| 107 | + event = json.load(f) |
| 108 | + pr = event.get("pull_request", {}) |
| 109 | + head_full_name = pr.get("head", {}).get("repo", {}).get("full_name", "") |
| 110 | + base_full_name = pr.get("base", {}).get("repo", {}).get("full_name", "") |
| 111 | + return bool( |
| 112 | + head_full_name and base_full_name and head_full_name != base_full_name |
| 113 | + ) |
| 114 | + except Exception: |
| 115 | + return False |
| 116 | + |
| 117 | + |
99 | 118 | def add_pr_comments() -> int: |
100 | 119 | """Posts the commit check result as a comment on the pull request.""" |
101 | 120 | if PR_COMMENTS == "false": |
102 | 121 | return 0 |
103 | 122 |
|
| 123 | + # Fork PRs triggered by the pull_request event receive a read-only token; |
| 124 | + # the GitHub API will always reject comment writes with 403. |
| 125 | + if is_fork_pr(): |
| 126 | + print( |
| 127 | + "::warning::Skipping PR comment: pull requests from forked repositories " |
| 128 | + "cannot write comments via the pull_request event (GITHUB_TOKEN is " |
| 129 | + "read-only for forks). Use the pull_request_target event or the " |
| 130 | + "two-workflow artifact pattern instead. " |
| 131 | + "See https://github.com/commit-check/commit-check-action/issues/77" |
| 132 | + ) |
| 133 | + return 0 |
| 134 | + |
104 | 135 | try: |
105 | 136 | token = os.getenv("GITHUB_TOKEN") |
106 | 137 | repo_name = os.getenv("GITHUB_REPOSITORY") |
107 | 138 | pr_number = os.getenv("GITHUB_REF") |
108 | 139 | if pr_number is not None: |
109 | 140 | pr_number = pr_number.split("/")[-2] |
110 | 141 | else: |
111 | | - # Handle the case where GITHUB_REF is not set |
112 | 142 | raise ValueError("GITHUB_REF environment variable is not set") |
113 | 143 |
|
114 | | - # Initialize GitHub client |
115 | | - # Use new Auth API to avoid deprecation warning |
116 | 144 | if not token: |
117 | 145 | raise ValueError("GITHUB_TOKEN is not set") |
| 146 | + |
118 | 147 | g = Github(auth=Auth.Token(token)) |
119 | 148 | repo = g.get_repo(repo_name) |
120 | 149 | pull_request = repo.get_issue(int(pr_number)) |
121 | 150 |
|
122 | 151 | # Prepare comment content |
123 | 152 | result_text = read_result_file() |
124 | | - pr_comments = ( |
| 153 | + pr_comment_body = ( |
125 | 154 | SUCCESS_TITLE |
126 | 155 | if result_text is None |
127 | 156 | else f"{FAILURE_TITLE}\n```\n{result_text}\n```" |
128 | 157 | ) |
129 | 158 |
|
130 | 159 | # Fetch all existing comments on the PR |
131 | 160 | comments = pull_request.get_comments() |
| 161 | + matching_comments = [ |
| 162 | + c |
| 163 | + for c in comments |
| 164 | + if c.body.startswith(SUCCESS_TITLE) or c.body.startswith(FAILURE_TITLE) |
| 165 | + ] |
132 | 166 |
|
133 | | - # Track if we found a matching comment |
134 | | - matching_comments = [] |
135 | | - last_comment = None |
136 | | - |
137 | | - for comment in comments: |
138 | | - if comment.body.startswith(SUCCESS_TITLE) or comment.body.startswith( |
139 | | - FAILURE_TITLE |
140 | | - ): |
141 | | - matching_comments.append(comment) |
142 | 167 | if matching_comments: |
143 | 168 | last_comment = matching_comments[-1] |
144 | | - |
145 | | - if last_comment.body == pr_comments: |
| 169 | + if last_comment.body == pr_comment_body: |
146 | 170 | print(f"PR comment already up-to-date for PR #{pr_number}.") |
147 | 171 | return 0 |
148 | | - else: |
149 | | - # If the last comment doesn't match, update it |
150 | | - print(f"Updating the last comment on PR #{pr_number}.") |
151 | | - last_comment.edit(pr_comments) |
152 | | - |
153 | | - # Delete all older matching comments |
| 172 | + print(f"Updating the last comment on PR #{pr_number}.") |
| 173 | + last_comment.edit(pr_comment_body) |
154 | 174 | for comment in matching_comments[:-1]: |
155 | 175 | print(f"Deleting an old comment on PR #{pr_number}.") |
156 | 176 | comment.delete() |
157 | 177 | else: |
158 | | - # No matching comments, create a new one |
159 | 178 | print(f"Creating a new comment on PR #{pr_number}.") |
160 | | - pull_request.create_comment(body=pr_comments) |
| 179 | + pull_request.create_comment(body=pr_comment_body) |
161 | 180 |
|
162 | 181 | return 0 if result_text is None else 1 |
| 182 | + except GithubException as e: |
| 183 | + if e.status == 403: |
| 184 | + print( |
| 185 | + "::warning::Unable to post PR comment (403 Forbidden). " |
| 186 | + "Ensure your workflow grants 'issues: write' permission. " |
| 187 | + f"Error: {e.data.get('message', str(e))}", |
| 188 | + file=sys.stderr, |
| 189 | + ) |
| 190 | + return 0 |
| 191 | + print(f"Error posting PR comment: {e}", file=sys.stderr) |
| 192 | + return 0 |
163 | 193 | except Exception as e: |
164 | 194 | print(f"Error posting PR comment: {e}", file=sys.stderr) |
165 | | - return 1 |
| 195 | + return 0 |
166 | 196 |
|
167 | 197 |
|
168 | 198 | def log_error_and_exit( |
|
0 commit comments