Skip to content

Commit 46f06b8

Browse files
authored
chore: close external PRs that touch blacklisted files (#166)
Instead of just commenting on external PRs that touch blacklisted files this commit causes them to be closed as well. We intend to use this in dfinity/ic to automatically close PRs by non-DFINITY contributors that touch files under `.github`. See: dfinity/ic#7307. This also changes the definition of an "external" PR from any PR created by a non-DFINITY member to any PR created from a fork. The latter is easier to determine because we don't need to query the GitHub API. Also note that the set of PRs created from forks includes the set of PRs created by non-DFINITY members since non-DFINITY members can create PRs from source repos since they're not allowed to push there. Finally this commit simplifies the `reusable_workflows/repo_policies/check_external_changes.py` Python code by not fetching the `.github/repo_policies/EXTERNAL_CONTRIB_BLACKLIST` file from within the script but using the `actions/checkout` action instead. Tested here: dfinity/test-compliant-repository-public#72 (comment).
1 parent 29b1a74 commit 46f06b8

File tree

3 files changed

+58
-109
lines changed

3 files changed

+58
-109
lines changed

.github/workflows/internal_vs_external.yml

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,10 @@ permissions:
1111
pull-requests: write
1212

1313
jobs:
14-
check-membership:
15-
uses: dfinity/public-workflows/.github/workflows/check_membership.yml@main
16-
secrets: inherit
17-
1814
revoke-approvals:
1915
name: Check Revoke Approvals
2016
runs-on: ubuntu-latest
21-
needs: check-membership
22-
if: ${{ needs.check-membership.outputs.is_member != 'true' && needs.check-membership.result == 'success' }}
17+
if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository
2318
steps:
2419
- name: Dismiss Pull Request Reviews
2520
if: ${{ ! github.event.pull_request_target.draft }}
@@ -54,52 +49,61 @@ jobs:
5449
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # actor is github actions with above permissions
5550
GH_ORG: ${{ github.repository_owner }}
5651
REPO: ${{ github.event.repository.name }}
57-
PULL_NUMBER: ${{ github.event.pull_request.number }}
52+
PULL_NUMBER: ${{ github.event.pull_request.number }}
5853

5954
check-external-file-changes:
6055
name: Check Unallowed File Changes
6156
runs-on: ubuntu-latest
62-
needs: check-membership
63-
if: ${{ needs.check-membership.outputs.is_member != 'true' && needs.check-membership.result == 'success' }}
57+
if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository
6458
steps:
65-
# First check out code from public-workflows
66-
- name: Checkout
67-
uses: actions/checkout@v4
59+
- name: Checkout EXTERNAL_CONTRIB_BLACKLIST from ${{ github.repository }}
60+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
6861
with:
69-
repository: dfinity/public-workflows
70-
path: public-workflows
62+
path: repo
63+
# actions/checkout will checkout the target repo and default branch by default
64+
# when triggered by pull_request_target. However for security reasons we want to
65+
# be explicit here.
66+
repository: ${{ github.repository }}
67+
ref: ${{ github.event.repository.default_branch }}
68+
sparse-checkout: .github/repo_policies/EXTERNAL_CONTRIB_BLACKLIST
7169

72-
- name: Python Setup
73-
uses: ./public-workflows/.github/workflows/python-setup
70+
- name: Checkout check_external_changes.py from dfinity/public-workflows
71+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
7472
with:
75-
working-directory: public-workflows
73+
repository: dfinity/public-workflows
74+
path: public-workflows
75+
sparse-checkout: reusable_workflows/repo_policies/check_external_changes.py
7676

7777
- name: Get changed files
78-
id: changed-files
7978
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
8079
with:
81-
use_rest_api: 'true'
80+
use_rest_api: true
81+
json: true
82+
write_output_files: true
8283

8384
- name: Check External Changes
84-
id: check-external-changes
85-
run: |
86-
export PYTHONPATH="$PWD/public-workflows/reusable_workflows/"
87-
python public-workflows/reusable_workflows/repo_policies/check_external_changes.py
88-
shell: bash
85+
if: ${{ hashFiles('repo/.github/repo_policies/EXTERNAL_CONTRIB_BLACKLIST') != '' }}
86+
id: check_external_changes
87+
run: public-workflows/reusable_workflows/repo_policies/check_external_changes.py
8988
env:
90-
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
91-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # no actual token stored, read-only permissions
92-
ORG: ${{ github.repository_owner }}
93-
REPO: ${{ github.event.repository.name }}
89+
# populated by the action
90+
# https://github.com/tj-actions/changed-files/blob/d03a93c0dbfac6d6dd6a0d8a5e7daff992b07449/README.md?plain=1#L569-L572
91+
CHANGED_FILES_JSON_PATH: ".github/outputs/all_changed_and_modified_files.json"
92+
EXTERNAL_CONTRIB_BLACKLIST_PATH: "repo/.github/repo_policies/EXTERNAL_CONTRIB_BLACKLIST"
9493

95-
- name: Add PR Comment
94+
- name: Close PR
9695
uses: actions/github-script@v7
97-
if: ${{ failure() }}
96+
if: ${{ !cancelled() && steps.check_external_changes.conclusion == 'failure' }}
9897
with:
9998
script: |
100-
let message = "Changes made to unallowed files. "
99+
github.rest.pulls.update({
100+
pull_number: context.issue.number,
101+
owner: context.repo.owner,
102+
repo: context.repo.repo,
103+
state: 'closed'
104+
})
105+
let message = "Closed Pull Request since changes were made to [unallowed files](${{ github.server_url }}/${{ github.repository }}/blob/${{ github.event.repository.default_branch }}/.github/repo_policies/EXTERNAL_CONTRIB_BLACKLIST).\n\n"
101106
message += 'Please see details here: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n\n'
102-
103107
github.rest.issues.createComment({
104108
issue_number: context.issue.number,
105109
owner: context.repo.owner,

reusable_workflows/repo_policies/check_external_changes.py

100644100755
Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,39 @@
1+
#!/usr/bin/env python3
12
import fnmatch
3+
import json
4+
import os
25
import sys
6+
from pathlib import Path
37

4-
import github3
58

6-
from shared.utils import download_gh_file, load_env_vars
9+
def main():
10+
EXTERNAL_CONTRIB_BLACKLIST_PATH = os.environ['EXTERNAL_CONTRIB_BLACKLIST_PATH']
711

8-
EXTERNAL_CONTRIB_BLACKLIST_PATH = ".github/repo_policies/EXTERNAL_CONTRIB_BLACKLIST"
12+
blacklisted_patterns = [
13+
pattern for line in Path(EXTERNAL_CONTRIB_BLACKLIST_PATH).read_text().splitlines()
14+
if (pattern := line.split("#")[0].strip()) != ""
15+
]
916

17+
with open(os.environ['CHANGED_FILES_JSON_PATH'], 'r') as f:
18+
changed_files = json.load(f)
1019

11-
def get_blacklisted_files(repo: github3.github.repo) -> list[str]:
12-
"""
13-
Loads the config from the repository that contains the list of blacklisted files.
14-
"""
15-
try:
16-
config_file = download_gh_file(repo, EXTERNAL_CONTRIB_BLACKLIST_PATH)
17-
except github3.exceptions.NotFoundError:
18-
return []
19-
blacklisted_files = [
20-
line for line in config_file.splitlines() if line.strip() and not line.strip().startswith("#")
21-
]
22-
return blacklisted_files
20+
print(f"Changed files: {changed_files}")
21+
print(f"Blacklisted patterns: {blacklisted_patterns}")
2322

23+
if blacklisted_patterns == []:
24+
print("No blacklisted patterns found.")
25+
sys.exit(0)
2426

25-
def check_files_against_blacklist(changed_files: list, blacklist_files: list) -> None:
26-
"""
27-
Check if any changed files match the blacklist rules using glob pattern matching.
28-
"""
29-
violations = []
30-
for file in changed_files:
31-
for rule in blacklist_files:
32-
if fnmatch.fnmatch(file, rule): # Use glob pattern matching
33-
violations.append(file)
27+
violations = [
28+
file for pattern in blacklisted_patterns for file in changed_files
29+
if fnmatch.fnmatch(file, pattern)
30+
]
3431

3532
if len(violations) > 0:
3633
print(f"No changes allowed to files: {violations}")
3734
sys.exit(1)
3835

39-
else:
40-
print("All changed files pass conditions.")
41-
42-
43-
def main():
44-
# Environment variables
45-
REQUIRED_ENV_VARS = ["REPO", "CHANGED_FILES", "ORG", "GH_TOKEN"]
46-
env_vars = load_env_vars(REQUIRED_ENV_VARS)
47-
48-
gh = github3.login(token=env_vars["GH_TOKEN"])
49-
repo = gh.repository(owner=env_vars["ORG"], repository=env_vars["REPO"])
50-
51-
# Get changed files
52-
changed_files = env_vars["CHANGED_FILES"].split()
53-
print(f"Changed files: {changed_files}")
54-
55-
blacklist_files = get_blacklisted_files(repo)
56-
57-
if blacklist_files == []:
58-
print("No blacklisted files found found.")
59-
sys.exit(0)
60-
61-
# Check changed files against blacklist
62-
check_files_against_blacklist(changed_files, blacklist_files)
36+
print("All changed files pass conditions.")
6337

6438

6539
if __name__ == "__main__":

reusable_workflows/tests/test_external_changes.py

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)