Updates check workflow, to review pull requests

This commit is contained in:
Alicia Sykes 2026-02-23 15:16:57 +00:00
parent 66d9970144
commit 2476a57bac
6 changed files with 700 additions and 119 deletions

View file

@ -18,13 +18,10 @@ jobs:
non_yaml_changed: ${{ steps.changes.outputs.non_yaml_changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }}
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -q -r lib/requirements.txt
- name: Detect changed files
id: changes
run: python lib/checks/detect-changes.py --base-ref ${{ github.event.pull_request.base.sha }}
@ -32,54 +29,64 @@ jobs:
if: steps.changes.outputs.non_yaml_changed == 'true'
run: python lib/checks/warn-non-yaml.py --base-ref ${{ github.event.pull_request.base.sha }}
readme:
name: README edits
pr-meta:
name: PR metadata
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
fetch-depth: 0
python-version: "3.12"
- name: Check PR metadata
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
PR_DRAFT: ${{ github.event.pull_request.draft }}
run: python lib/checks/check-pr-meta.py
- name: Upload findings
if: always()
uses: actions/upload-artifact@v4
with:
name: findings-meta
path: /tmp/findings-meta.json
if-no-files-found: ignore
file-checks:
name: File checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }}
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -q -r lib/requirements.txt
- name: Check for direct README edits
run: python lib/checks/check-readme-edits.py --base-ref ${{ github.event.pull_request.base.sha }}
schema:
name: Schema validation
data-validation:
name: Data validation
needs: detect-changes
if: needs.detect-changes.outputs.yaml_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -q -r lib/requirements.txt
- name: Validate schema
run: make validate
diff:
name: Single-entry rule
needs: detect-changes
if: needs.detect-changes.outputs.yaml_changed == 'true'
runs-on: ubuntu-latest
outputs:
has_service_changes: ${{ steps.diff.outputs.has_service_changes }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }}
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -q -r lib/requirements.txt
- name: YAML diff analysis
- name: Schema validation
id: schema
continue-on-error: true
run: make validate
- name: YAML diff
id: diff
continue-on-error: true
run: python lib/checks/check-yaml-diff.py --base-ref ${{ github.event.pull_request.base.sha }}
- name: Check additions
env:
SCHEMA_OUTCOME: ${{ steps.schema.outcome }}
run: python lib/checks/check-additions.py
- name: Upload diff data
if: always()
uses: actions/upload-artifact@v4
@ -87,11 +94,21 @@ jobs:
name: pr-diff
path: /tmp/pr-diff.json
if-no-files-found: ignore
- name: Upload findings
if: always()
uses: actions/upload-artifact@v4
with:
name: findings-data
path: /tmp/findings-data.json
if-no-files-found: ignore
- name: Fail if critical checks failed
if: steps.schema.outcome == 'failure' || steps.diff.outcome == 'failure'
run: exit 1
links:
name: Link validation
needs: diff
if: needs.diff.result == 'success' && needs.diff.outputs.has_service_changes == 'true'
project-checks:
name: Project checks
needs: data-validation
if: "!cancelled() && needs.data-validation.result != 'skipped'"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -104,93 +121,44 @@ jobs:
with:
name: pr-diff
path: /tmp
- name: Validate links
run: python lib/checks/check-links.py --diff-json /tmp/pr-diff.json
continue-on-error: true
- name: Check project health
env:
PR_USER: ${{ github.event.pull_request.user.login }}
GITHUB_TOKEN: ${{ github.token }}
run: python lib/checks/check-project.py
- name: Upload findings
if: always()
uses: actions/upload-artifact@v4
with:
name: findings-project
path: /tmp/findings-project.json
if-no-files-found: ignore
template:
name: PR template
summary:
name: Summary
if: always()
needs: [detect-changes, pr-meta, file-checks, data-validation, project-checks]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -q -r lib/requirements.txt
- name: PR template check
- name: Download all findings
uses: actions/download-artifact@v4
with:
pattern: findings-*
path: /tmp/artifacts
merge-multiple: true
continue-on-error: true
- name: Format comment
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: python lib/checks/check-template.py
comment:
name: Summary
if: always()
needs: [readme, schema, diff, links, template]
runs-on: ubuntu-latest
steps:
- name: Save PR metadata
run: |
mkdir -p /tmp/pr-meta
echo "${{ github.event.pull_request.number }}" > /tmp/pr-meta/number.txt
echo "${{ github.run_id }}" > /tmp/pr-meta/run-id.txt
- name: Build failure comment
if: contains(needs.*.result, 'failure')
run: |
cat > /tmp/pr-meta/comment.md << 'HEADER'
<!-- pr-check-bot -->
> [!CAUTION]
> ## PR check failed
HEADER
if [ "${{ needs.readme.result }}" = "failure" ]; then
cat >> /tmp/pr-meta/comment.md << 'EOF'
### Direct README edits
The auto-generated section of the README must not be edited directly.
Please make your changes in `awesome-privacy.yml` instead — the README is regenerated from it.
EOF
fi
if [ "${{ needs.schema.result }}" = "failure" ]; then
cat >> /tmp/pr-meta/comment.md << 'EOF'
### Schema validation failed
`awesome-privacy.yml` does not conform to the expected schema.
Please check your YAML against the structure defined in `lib/schema.json`.
EOF
fi
if [ "${{ needs.diff.result }}" = "failure" ]; then
cat >> /tmp/pr-meta/comment.md << 'EOF'
### Single-entry rule
Each PR should add, modify, or remove only **one** service at a time.
If you need to change multiple services, please split them into separate PRs.
EOF
fi
if [ "${{ needs.links.result }}" = "failure" ]; then
cat >> /tmp/pr-meta/comment.md << 'EOF'
### Link validation failed
One or more URLs in your entry could not be verified.
Please ensure all links are correct and accessible.
EOF
fi
if [ "${{ needs.template.result }}" = "failure" ]; then
cat >> /tmp/pr-meta/comment.md << 'EOF'
### PR template incomplete
Your PR description is missing required sections.
Please fill out the **Type**, **Changes**, and **Checklist** sections of the PR template.
EOF
fi
printf '\n---\n*See the [workflow logs](%s/%s/actions/runs/%s) for full details.*\n' \
"${{ github.server_url }}" "${{ github.repository }}" "${{ github.run_id }}" \
>> /tmp/pr-meta/comment.md
PR_USER: ${{ github.event.pull_request.user.login }}
PR_NUMBER: ${{ github.event.pull_request.number }}
RUN_ID: ${{ github.run_id }}
README_FAILED: ${{ needs.file-checks.result == 'failure' && 'true' || 'false' }}
run: python lib/checks/format-comment.py
- name: Upload PR metadata
if: always()
uses: actions/upload-artifact@v4

View file

@ -33,7 +33,6 @@ jobs:
script: |
const fs = require('fs');
const marker = '<!-- pr-check-bot -->';
const conclusion = context.payload.workflow_run.conclusion;
// Determine the PR number
let prNumber;
@ -73,12 +72,8 @@ jobs:
});
const existing = comments.find(c => c.body.includes(marker));
if (conclusion === 'failure') {
const commentFile = 'pr-meta/comment.md';
if (!fs.existsSync(commentFile)) {
console.log('No comment.md found — skipping comment.');
return;
}
const commentFile = 'pr-meta/comment.md';
if (fs.existsSync(commentFile)) {
const body = fs.readFileSync(commentFile, 'utf8').trim();
if (existing) {
await github.rest.issues.updateComment({
@ -96,7 +91,7 @@ jobs:
});
}
} else if (existing) {
// Checks passed — remove stale failure comment
// No findings — remove stale comment
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,

View file

@ -0,0 +1,173 @@
"""Validates data quality for added/modified services using the diff JSON."""
import json
import os
import sys
import yaml
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
DATA_PATH = os.path.join(PROJECT_ROOT, "awesome-privacy.yml")
DIFF_PATH = "/tmp/pr-diff.json"
FINDINGS_PATH = "/tmp/findings-data.json"
REQUIRED_FIELDS = ("name", "description", "url", "icon")
CONTRIBUTING = "https://github.com/Lissy93/awesome-privacy/blob/main/.github/CONTRIBUTING.md"
SCHEMA_MSG = (
"Some of the schema checks have failed. Please check that your addition"
" contains all the required fields, with acceptable values, nothing"
" additional and that it is following valid YAML syntax"
)
MULTIPLE_MSG = "Please make just one addition per pull request"
MISSING_TPL = (
"Did you include all required fields? Looks like {fields} is missing or"
f" invalid. Please see the [required fields]({CONTRIBUTING}#service-fields)"
" for available fields."
)
POSITION_MSG = (
"New entries must be added to the end of the section, unless otherwise requested"
)
OPENSOURCE_MSG = (
"You indicated this app/service is not open source. This will likely make"
" it ineligible for listing on Awesome Privacy in accordance with our"
f" [Requirements]({CONTRIBUTING}#requirements)."
" Please ensure that this is justified in your PR body."
)
def load_json(path):
"""Load JSON from a file, returning None on any error."""
try:
with open(path) as f:
return json.load(f)
except Exception:
return None
def load_yaml_data(path):
"""Load YAML from a file, returning None on any error."""
try:
with open(path) as f:
return yaml.safe_load(f)
except Exception:
return None
def find_section_services(head, category, section):
"""Return the services list for a category/section pair, or None."""
for cat in head.get("categories", []):
if cat.get("name") == category:
for sec in cat.get("sections", []):
if sec.get("name") == section:
return sec.get("services", [])
return None
def find_service_fields(head, category, section, service_name):
"""Look up a service's fields in the head YAML."""
services = find_section_services(head, category, section)
if services:
for svc in services:
if svc.get("name") == service_name:
return svc
return None
def check_required_fields(diff, head):
"""Return a finding if any added/modified service is missing required fields."""
missing = set()
for svc in diff.get("services", {}).get("added", []):
fields = svc.get("fields", {})
for f in REQUIRED_FIELDS:
if not fields.get(f):
missing.add(f)
for svc in diff.get("services", {}).get("modified", []):
if not head:
continue
fields = find_service_fields(
head, svc["category"], svc["section"], svc["service"]
)
if fields:
for f in REQUIRED_FIELDS:
if not fields.get(f):
missing.add(f)
if missing:
names = ", ".join(f"`{f}`" for f in sorted(missing))
return MISSING_TPL.format(fields=names)
return None
def check_position(diff, head):
"""Return a finding if a newly added service is not at the end of its section."""
if not head:
return None
for svc in diff.get("services", {}).get("added", []):
services = find_section_services(head, svc["category"], svc["section"])
if services and services[-1].get("name") != svc["service"]:
return POSITION_MSG
return None
def check_open_source(diff):
"""Return a finding if an added service has openSource missing or not true."""
for svc in diff.get("services", {}).get("added", []):
fields = svc.get("fields", {})
if fields.get("openSource") is not True:
return OPENSOURCE_MSG
return None
def check_single_entry(diff):
"""Return a finding if the diff contains multiple service or section changes."""
services = diff.get("services", {})
svc_count = (
len(services.get("added", []))
+ len(services.get("removed", []))
+ len(services.get("modified", []))
)
if svc_count > 1:
return MULTIPLE_MSG
if svc_count == 0:
sec_count = len(diff.get("sections", []))
if sec_count > 1:
return MULTIPLE_MSG
return None
def main():
findings = []
try:
if os.environ.get("SCHEMA_OUTCOME") == "failure":
findings.append(SCHEMA_MSG)
diff = load_json(DIFF_PATH)
head = load_yaml_data(DATA_PATH)
if diff:
finding = check_single_entry(diff)
if finding:
findings.append(finding)
finding = check_required_fields(diff, head)
if finding:
findings.append(finding)
finding = check_position(diff, head)
if finding:
findings.append(finding)
finding = check_open_source(diff)
if finding:
findings.append(finding)
except Exception:
pass
with open(FINDINGS_PATH, "w") as f:
json.dump(findings, f)
sys.exit(0)
if __name__ == "__main__":
main()

122
lib/checks/check-pr-meta.py Normal file
View file

@ -0,0 +1,122 @@
"""Checks PR metadata: title format, draft status, template completeness, and checkboxes."""
import json
import os
import re
import sys
FINDINGS_PATH = "/tmp/findings-meta.json"
BAD_TITLES = {"update readme.md", "update awesome-privacy.yml"}
TITLE_MSG = (
"The pull request title does not follow the format defined in our guidelines."
" Please rename it to `[Add/Remove/Update] [software name] in [software section]`"
)
DRAFT_MSG = (
"Please avoid opening WIP pull requests."
" Your PR should be 100% ready and complete before submitting"
)
TEMPLATE_MSG = (
"Please fill in pull request template in full."
" You can find a copy of this"
" [here](https://github.com/Lissy93/awesome-privacy/blob/main/.github/PULL_REQUEST_TEMPLATE.md)"
)
CHECKBOX_MSG = (
"Ensure you have completed the checklist (put a tick the checkboxes with `[x]`),"
" to confirm that you've read the contributing guidelines, checked your submission,"
" indicated your affiliation and agree to follow our CoC"
)
def extract_section(body, header):
"""Extract content between a ### header and the next delimiter."""
pattern = rf"###\s*{re.escape(header)}\s*\n(.*?)(?=\n---|\n###|\Z)"
match = re.search(pattern, body, re.DOTALL)
return match.group(1) if match else None
def strip_html_comments(text):
"""Remove HTML comments from text."""
return re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL).strip()
def check_title(title):
"""Return a finding if the PR title matches a known-bad pattern."""
if title and title.strip().lower() in BAD_TITLES:
return TITLE_MSG
return None
def check_draft(draft_str):
"""Return a finding if the PR is in draft state."""
if str(draft_str).lower() == "true":
return DRAFT_MSG
return None
def check_template(body):
"""Return a finding if required template sections are missing or empty."""
for header in ("Type", "Changes", "Checklist"):
content = extract_section(body, header)
if content is None or not strip_html_comments(content):
return TEMPLATE_MSG
return None
def check_checkboxes(body):
"""Return a finding if any checklist checkboxes are unchecked."""
section = extract_section(body, "Checklist")
if section is None:
return None
checked = re.findall(r"- \[x\]", section, re.IGNORECASE)
unchecked = re.findall(r"- \[ \]", section)
if not checked and not unchecked:
return None
if unchecked:
return CHECKBOX_MSG
return None
def write_findings(findings):
"""Write the findings list to the output JSON file."""
with open(FINDINGS_PATH, "w") as f:
json.dump(findings, f)
def main():
findings = []
critical = False
try:
title = os.environ.get("PR_TITLE", "")
body = os.environ.get("PR_BODY", "")
draft = os.environ.get("PR_DRAFT", "false")
finding = check_title(title)
if finding:
findings.append(finding)
finding = check_draft(draft)
if finding:
findings.append(finding)
if not body or not body.strip():
findings.append(TEMPLATE_MSG)
critical = True
else:
finding = check_template(body)
if finding:
findings.append(finding)
critical = True
finding = check_checkboxes(body)
if finding:
findings.append(finding)
except Exception:
pass
write_findings(findings)
sys.exit(1 if critical else 0)
if __name__ == "__main__":
main()

217
lib/checks/check-project.py Normal file
View file

@ -0,0 +1,217 @@
"""Checks project health: URL reachability, GitHub repo stars, activity, and author match."""
import json
import os
import sys
from datetime import datetime, timezone
import requests
import yaml
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
DATA_PATH = os.path.join(PROJECT_ROOT, "awesome-privacy.yml")
DIFF_PATH = "/tmp/pr-diff.json"
FINDINGS_PATH = "/tmp/findings-project.json"
TIMEOUT = 10
USER_AGENT = "awesome-privacy-ci/1.0"
MIN_STARS = 100
INACTIVE_DAYS = 90
LINK_MSG = (
"Our automated checks were unable to verify the link(s) you included"
" were reachable, so please double check this yourself"
)
AUTHOR_MSG = (
"Looks like you are the author of this package. Please ensure that you"
" have clearly disclosed this in your PR body for transparency"
)
STARS_MSG = (
"It looks like your submission is adding a quite small project."
" In some circumstances we may ask you to resubmit this once the project"
" is more mature and has a proven track record of good practices and maintenance."
)
ACTIVITY_MSG = (
"Please confirm that the project you are adding is actively maintained,"
" as it looks to not have had any recent updates in the past 3 months."
)
def load_diff(path):
"""Load the diff JSON, returning None on any error."""
try:
with open(path) as f:
return json.load(f)
except Exception:
return None
def check_url(url):
"""Return True if the URL is reachable, True on any error (no false positives)."""
try:
resp = requests.head(
url, timeout=TIMEOUT, allow_redirects=True,
headers={"User-Agent": USER_AGENT},
)
if resp.status_code >= 400:
resp = requests.get(
url, timeout=TIMEOUT, allow_redirects=True,
headers={"User-Agent": USER_AGENT}, stream=True,
)
resp.close()
return resp.status_code < 400
except Exception:
return True
def parse_github_field(value):
"""Parse a github field into (owner, repo), or (None, None) on failure."""
if not value:
return None, None
if value.startswith("https://github.com/"):
parts = value.removeprefix("https://github.com/").strip("/").split("/")
if len(parts) >= 2:
return parts[0], parts[1]
return None, None
if "/" in value:
parts = value.split("/")
if len(parts) == 2:
return parts[0], parts[1]
return None, None
def fetch_repo(owner, repo, token):
"""Fetch GitHub repo metadata, returning None on any error."""
try:
headers = {"Accept": "application/vnd.github.v3+json", "User-Agent": USER_AGENT}
if token:
headers["Authorization"] = f"token {token}"
resp = requests.get(
f"https://api.github.com/repos/{owner}/{repo}",
headers=headers, timeout=TIMEOUT,
)
if resp.status_code == 200:
return resp.json()
except Exception:
pass
return None
def load_yaml_data():
"""Load the head YAML, returning None on any error."""
try:
with open(DATA_PATH) as f:
return yaml.safe_load(f)
except Exception:
return None
def find_service_in_head(head, category, section, service_name):
"""Look up a service in the head YAML by path."""
if not head:
return None
for cat in head.get("categories", []):
if cat.get("name") == category:
for sec in cat.get("sections", []):
if sec.get("name") == section:
for svc in sec.get("services", []):
if svc.get("name") == service_name:
return svc
return None
def get_services(diff, key):
"""Safely extract a service list from the diff."""
return diff.get("services", {}).get(key, [])
def check_links(diff, head):
"""Return LINK_MSG if any service URL is unreachable."""
for svc in get_services(diff, "added"):
url = svc.get("fields", {}).get("url")
if url and not check_url(url):
return LINK_MSG
for svc in get_services(diff, "modified"):
if "url" not in svc.get("changed_fields", []):
continue
head_svc = find_service_in_head(
head, svc["category"], svc["section"], svc["service"]
)
if head_svc:
url = head_svc.get("url")
if url and not check_url(url):
return LINK_MSG
return None
def check_repo_signals(diff, pr_user, token):
"""Check GitHub repo author match, stars, and activity for added services."""
findings = []
if not token:
return findings
cache = {}
for svc in get_services(diff, "added"):
gh = svc.get("fields", {}).get("github")
owner, repo = parse_github_field(gh)
if not owner:
continue
cache_key = f"{owner}/{repo}"
if cache_key not in cache:
cache[cache_key] = fetch_repo(owner, repo, token)
data = cache[cache_key]
if not data:
continue
repo_owner = data.get("owner", {})
if (
pr_user
and repo_owner.get("type") == "User"
and repo_owner.get("login", "").lower() == pr_user.lower()
and AUTHOR_MSG not in findings
):
findings.append(AUTHOR_MSG)
stars = data.get("stargazers_count", 0)
if stars < MIN_STARS and STARS_MSG not in findings:
findings.append(STARS_MSG)
pushed = data.get("pushed_at")
if pushed and ACTIVITY_MSG not in findings:
try:
pushed_dt = datetime.fromisoformat(pushed.replace("Z", "+00:00"))
now = datetime.now(timezone.utc)
if (now - pushed_dt).days > INACTIVE_DAYS:
findings.append(ACTIVITY_MSG)
except Exception:
pass
return findings
def main():
findings = []
try:
diff = load_diff(DIFF_PATH)
if not diff:
with open(FINDINGS_PATH, "w") as f:
json.dump(findings, f)
sys.exit(0)
head = load_yaml_data()
finding = check_links(diff, head)
if finding:
findings.append(finding)
pr_user = os.environ.get("PR_USER", "")
token = os.environ.get("GITHUB_TOKEN", "")
findings.extend(check_repo_signals(diff, pr_user, token))
except Exception:
pass
with open(FINDINGS_PATH, "w") as f:
json.dump(findings, f)
sys.exit(0)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,106 @@
"""Aggregates findings from all check jobs into a formatted PR comment."""
import json
import os
import sys
ARTIFACTS_DIR = "/tmp/artifacts"
OUTPUT_DIR = "/tmp/pr-meta"
README_MSG = (
"Do not edit the README directly. This file is auto-generated from the"
" content in `awesome-privacy.yml`, and so your changes will be overridden!"
" Instead, only modify the YAML file, and be sure to follow our Contributing Guidelines."
)
CONTRIBUTING = "https://github.com/Lissy93/awesome-privacy/blob/main/.github/CONTRIBUTING.md"
COMMENT_TEMPLATE = """<!-- pr-check-bot -->
Hello @{user}
Thank you for contributing to Awesome Privacy! We will review your PR shortly. In the meantime, please ensure that your submission is inline with our guidelines in our [Contributing Requirements]({contributing}).
Looks like there could be some issues in your PR. Please double check that:
{findings}
> [!NOTE]
> I am a bot, and sometimes make mistakes in my suggestions. But a human will review your submission shortly!"""
def load_findings(filename):
"""Load a findings JSON array from the artifacts directory, or empty list on error."""
try:
with open(os.path.join(ARTIFACTS_DIR, filename)) as f:
data = json.load(f)
return data if isinstance(data, list) else []
except Exception:
return []
def collect_findings(readme_failed):
"""Gather all findings in display order: meta, readme, data, project."""
all_findings = []
all_findings.extend(load_findings("findings-meta.json"))
if readme_failed:
all_findings.append(README_MSG)
all_findings.extend(load_findings("findings-data.json"))
all_findings.extend(load_findings("findings-project.json"))
return all_findings
def format_comment(findings, user):
"""Build the markdown comment from findings."""
bullet_list = "\n".join(f"- {f}" for f in findings)
return COMMENT_TEMPLATE.format(
user=user, contributing=CONTRIBUTING, findings=bullet_list,
)
def write_step_summary(findings):
"""Write a summary to GITHUB_STEP_SUMMARY."""
summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
if not summary_file:
return
lines = ["## PR Check Summary\n"]
if findings:
lines.append(f"⚠️ Found {len(findings)} issue(s):\n")
for f in findings:
lines.append(f"- {f}")
else:
lines.append("✅ All checks passed.\n")
with open(summary_file, "a") as f:
f.write("\n".join(lines) + "\n")
def main():
try:
user = os.environ.get("PR_USER", "contributor")
pr_number = os.environ.get("PR_NUMBER", "")
run_id = os.environ.get("RUN_ID", "")
readme_failed = os.environ.get("README_FAILED", "false") == "true"
os.makedirs(OUTPUT_DIR, exist_ok=True)
if pr_number:
with open(os.path.join(OUTPUT_DIR, "number.txt"), "w") as f:
f.write(pr_number)
if run_id:
with open(os.path.join(OUTPUT_DIR, "run-id.txt"), "w") as f:
f.write(run_id)
findings = collect_findings(readme_failed)
write_step_summary(findings)
if findings:
comment = format_comment(findings, user)
with open(os.path.join(OUTPUT_DIR, "comment.md"), "w") as f:
f.write(comment)
except Exception:
pass
sys.exit(0)
if __name__ == "__main__":
main()