Safely upload linter results from forks to GitHub

This makes lint results from pull requests appear as GitHub checks
similarly to how pull requests from branches are able to work

Based on https://securitylab.github.com/research/github-actions-preventing-pwn-requests
pyro-refactor
krzys-h 2021-02-22 11:12:21 +01:00
parent 9c91fd1e52
commit 9f2f7780a4
No known key found for this signature in database
GPG Key ID: 56E6DBB8DF016002
2 changed files with 92 additions and 47 deletions

View File

@ -79,16 +79,12 @@ jobs:
with: with:
name: HTML results name: HTML results
path: build/html_report path: build/html_report
- run: pip install requests - name: Generate GitHub annotations JSON and process check result
- name: Send linter results to GitHub
shell: python shell: python
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ACTUALLY_SEND: ${{ github.event.type != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
run: | run: |
import os import os
import sys import sys
import requests import json
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
OVERALL_STABLE_RULES=[ OVERALL_STABLE_RULES=[
@ -141,18 +137,6 @@ jobs:
"whitespace", "whitespace",
] ]
# None of the available actions seem to do what I want, they all do stupid things like adding another check... let's just do it manually
# GitHub also doesn't seem to provide you with the check suite or check run ID, so we have to get it from the action ID via the API
s = requests.Session()
s.headers.update({
'Authorization': 'token ' + os.environ['GITHUB_TOKEN'],
'Accept': 'application/vnd.github.antiope-preview+json' # Annotations are still technically a preview feature of the API
})
action_run = s.get(os.environ['GITHUB_API_URL'] + "/repos/" + os.environ['GITHUB_REPOSITORY'] + "/actions/runs/" + os.environ['GITHUB_RUN_ID']).json()
check_suite = s.get(action_run['check_suite_url']).json()
check_suite_runs = s.get(check_suite['check_runs_url']).json()
check_run = check_suite_runs['check_runs'][0] # NOTE: This assumes that the 'lint' job is the first one in the workflow. You could find it by name if you really wanted.
def we_care_about(file_name, type): def we_care_about(file_name, type):
if 'CBot' in file_name: if 'CBot' in file_name:
return type in OVERALL_STABLE_RULES return type in OVERALL_STABLE_RULES
@ -161,6 +145,7 @@ jobs:
results = ET.parse('build/colobot_lint_report.xml') results = ET.parse('build/colobot_lint_report.xml')
annotations = [] annotations = []
stable_annotations = []
for error in results.find('errors').findall('error'): for error in results.find('errors').findall('error'):
location = error.find('location') location = error.find('location')
file_name = os.path.relpath(location.get('file'), os.environ['GITHUB_WORKSPACE']) file_name = os.path.relpath(location.get('file'), os.environ['GITHUB_WORKSPACE'])
@ -175,42 +160,35 @@ jobs:
elif severity == 'information': elif severity == 'information':
gh_severity = 'notice' gh_severity = 'notice'
if not we_care_about(file_name, type): annotation = {
# don't send the unstable rules to github at all as there are way too many of them and it would overload the API rate limit
continue
print('{}:{}: [{}] {}'.format(file_name, line_num, type, msg))
annotations.append({
'path': file_name, 'path': file_name,
'start_line': line_num, 'start_line': line_num,
'end_line': line_num, 'end_line': line_num,
'annotation_level': gh_severity, 'annotation_level': gh_severity,
'title': type, 'title': type,
'message': msg 'message': msg
}) }
annotations.append(annotation)
summary = 'colobot-lint found {} issues'.format(len(annotations)) if we_care_about(file_name, type):
all_ok = len(annotations) == 0 # don't send the unstable rules to github at all as there are way too many of them and it would overload the API rate limit
stable_annotations.append(annotation)
print('{}:{}: [{}] {}'.format(file_name, line_num, type, msg))
summary = 'colobot-lint found {} issues'.format(len(stable_annotations))
all_ok = len(stable_annotations) == 0
print('Conclusion: {}'.format(summary)) print('Conclusion: {}'.format(summary))
if os.environ['ACTUALLY_SEND'] != "true": with open("build/annotations.json", "w") as f:
print('Skip uploading the results as annotations because tokens from forks are readonly and there seems to be no way to do it. Blame GitHub Actions devs.') json.dump(annotations, f, indent=4)
else: with open("build/stable_annotations.json", "w") as f:
# Annotations have to be sent in batches of 50 json.dump(stable_annotations, f, indent=4)
first = True
while first or len(annotations) > 0:
first = False
to_send = annotations[:50]
annotations = annotations[50:]
data = {
'output': {
'title': summary,
'summary': summary,
'annotations': to_send
}
}
r = s.patch(check_run['url'], json=data)
r.raise_for_status()
sys.exit(0 if all_ok else 1) sys.exit(0 if all_ok else 1)
- name: Upload results (JSON)
uses: actions/upload-artifact@v2
with:
name: JSON results
path: |
build/annotations.json
build/stable_annotations.json
if: ${{ always() }}

View File

@ -0,0 +1,67 @@
name: Linter upload results
# Upload linter results after succesful linter run
# This is done in a separate workflow to safely use the read-write GitHub token
# See https://securitylab.github.com/research/github-actions-preventing-pwn-requests
on:
workflow_run:
workflows: ["Linter"]
types:
- completed
jobs:
lint_upload:
runs-on: ubuntu-16.04
steps:
- run: pip install requests
- name: Download linter results
uses: dawidd6/action-download-artifact@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: lint.yml
run_id: ${{ github.event.workflow_run.id }}
name: JSON results
path: results
- name: Send linter results to GitHub
shell: python
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: |
import os
import json
import requests
# Load the results from the lint job artifact
with open("results/stable_annotations.json", "r") as f:
annotations = json.load(f)
summary = 'colobot-lint found {} issues'.format(len(annotations))
# None of the available actions seem to do what I want, they all do stupid things like adding another check... let's just do it manually
# GitHub also doesn't seem to provide you with the check suite or check run ID, so we have to get it from the action ID via the API
s = requests.Session()
s.headers.update({
'Authorization': 'token ' + os.environ['GITHUB_TOKEN'],
'Accept': 'application/vnd.github.antiope-preview+json' # Annotations are still technically a preview feature of the API
})
action_run = s.get(os.environ['GITHUB_API_URL'] + "/repos/" + os.environ['GITHUB_REPOSITORY'] + "/actions/runs/" + os.environ['RUN_ID']).json()
check_suite = s.get(action_run['check_suite_url']).json()
check_suite_runs = s.get(check_suite['check_runs_url']).json()
check_run = check_suite_runs['check_runs'][0] # NOTE: This assumes that the 'lint' job is the first one in the workflow. You could find it by name if you really wanted.
# Annotations have to be sent in batches of 50
first = True
while first or len(annotations) > 0:
first = False
to_send = annotations[:50]
annotations = annotations[50:]
data = {
'output': {
'title': summary,
'summary': summary,
'annotations': to_send
}
}
r = s.patch(check_run['url'], json=data)
r.raise_for_status()