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