From ce3a59e13911a18ec706ec2d80f2274a021a9241 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Thu, 26 Feb 2026 12:24:43 +0000 Subject: [PATCH] Updates contributing and automated checks to ensure project age --- .github/CONTRIBUTING.md | 13 +++-- .github/workflows/pr-check.yml | 10 +++- lib/.gitignore | 1 + lib/checks/check-additions.py | 4 +- lib/checks/check-project.py | 103 +++++++++++++++++++++++++++++++++ lib/checks/check-yaml-diff.py | 30 +++++++--- lib/checks/format-comment.py | 73 +++++++++++++++-------- 7 files changed, 196 insertions(+), 38 deletions(-) create mode 100644 lib/.gitignore diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b9e07ef..245ab68 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -62,6 +62,10 @@ For software to be included in this list, it must meet the following requirement - A stable (non-alpha/beta) release is required at a minimum - Must be accessible to the general public, and not just a select group of people - If technical knowledge is required to run it, the software must be well documented +- **Mature** + - Software needs to have a proven track record of commitment to maintenance + - Repositories must not be newly created, and the first stable release older than 4 months + - Projects primarily written with AI or vibe coded are not suitable for listing here _There may be some exceptions, but these would need to be fully justified, reviewed by the community, and the drawbacks / anti-features must be clearly listed along-side the software. @@ -79,19 +83,18 @@ Your pull request must follow these requirements. Failure to do so, might result - You must respond to any comments or requests for changes in a timely manner, 14 days maximum - Write short but descriptive git commit messages, under 50 characters. This must be in the format of `Adds [software-name] to [section-name]`. Your PR will be rejected if you name it `Updates README.md` - Only include a single addition / amendment / removal, per pull request -- You must complete each of the sections in the pull request template. Do not delete it! +- You must complete each of the sections in the [pull request template](https://github.com/Lissy93/awesome-privacy/blob/main/.github/PULL_REQUEST_TEMPLATE.md). Do not delete it! - Where applicable, include links to supporting material for your addition: git repo, docs, recent security audits, etc. This will make researching it much easier for reviewers - While adding new software to the list, don't make your entry read like an advert. Be objective, and include drawbacks as well as strengths - Your entry should be added at the bottom of the appropriate category, unless otherwise requested -- If there are other pull requests open, please help review them before submitting yours -- A pull request must receive multiple approval reviews before it can be merged - You must be transparent about your affiliation with a product or service that you are adding. It's totally okay to submit your own projects as additions (providing they meet the requirements), but if you don't declare your association with that project then there becomes a clear conflict of interest -- You must adhere to the Contributor Covenant Code of Conduct +- You must adhere to the [Contributor Covenant Code of Conduct](https://github.com/Lissy93/awesome-privacy?tab=coc-ov-file#contributor-covenant-code-of-conduct) - Don't open a Draft / WIP pull request while you work on the guidelines. A pull request should be 100% ready and should adhere to all the above guidelines when you open it - Your changes must be correctly spelled, and with good grammar - Your changes must be correctly formatted, in valid yaml and markdown -- The addition title must be a link the project - The addition description must be no less than 50, and no more than 250 characters, keep it clear and to the point +- If there are other pull requests open, please help review them before submitting yours +- A pull request must receive multiple approval reviews before it can be merged --- diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index d30babe..d4dfac5 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -82,7 +82,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: pr-diff - path: /tmp/pr-diff.json + path: | + /tmp/pr-diff.json + /tmp/pr-diff-summary.md if-no-files-found: ignore - name: Upload findings if: always() @@ -142,6 +144,12 @@ jobs: path: /tmp/artifacts merge-multiple: true continue-on-error: true + - name: Download diff data + uses: actions/download-artifact@v4 + with: + name: pr-diff + path: /tmp/artifacts + continue-on-error: true - name: Format comment env: PR_USER: ${{ github.event.pull_request.user.login }} diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/lib/checks/check-additions.py b/lib/checks/check-additions.py index f972f1e..b2cf8b4 100644 --- a/lib/checks/check-additions.py +++ b/lib/checks/check-additions.py @@ -131,7 +131,7 @@ 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: + if fields.get("openSource") is not True and not fields.get("github"): return OPENSOURCE_MSG return None @@ -221,7 +221,7 @@ def check_description_length(diff): for svc in diff.get("services", {}).get("added", []): desc = svc.get("fields", {}).get("description", "") length = len(desc) - if length < 50 or length > 250: + if length < 50 or length > 280: return DESC_LENGTH_MSG.format(length=length) return None diff --git a/lib/checks/check-project.py b/lib/checks/check-project.py index abb2d87..bf8335f 100644 --- a/lib/checks/check-project.py +++ b/lib/checks/check-project.py @@ -17,6 +17,12 @@ TIMEOUT = 10 USER_AGENT = "awesome-privacy-ci/1.0" MIN_STARS = 100 INACTIVE_DAYS = 90 +MIN_AGE_DAYS = 120 +AI_COMMIT_THRESHOLD = 5 +AI_BOT_AUTHORS = [ + "noreply@anthropic.com", + "devin-ai-integration[bot]", +] LINK_MSG = ( "Our automated checks were unable to verify the link(s) you included" @@ -35,6 +41,29 @@ 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." ) +MATURITY_MSG = ( + "This project appears to be quite new (created less than 4 months ago)." + " Repositories should have a proven track record before listing." +) +AI_CODE_MSG = ( + "This project appears to contain AI-generated code." + " Additional care will be needed when reviewing the submission." +) +FORK_MSG = ( + "The GitHub link in this listing is a fork." + " Please confirm it's the correct (and actively maintained) repository" +) +LICENSE_MSG = ( + "There doesn't appear to be a license included in the project's GitHub repo" +) +ARCHIVED_MSG = ( + "The GitHub project linked has been archived." + " Additions must be actively maintained." +) +SECURITY_MSG = ( + "This project has open security vulnerabilities (critical or high severity)" + " flagged by GitHub Dependabot. Please verify these have been addressed" +) def load_diff(path): @@ -154,6 +183,51 @@ def check_links(diff, head): return None +def check_ai_commits(owner, repo, token): + """Return AI_CODE_MSG if recent commits contain significant AI bot activity.""" + 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}/commits", + headers=headers, timeout=TIMEOUT, params={"per_page": 100}, + ) + if resp.status_code != 200: + return None + bot_set = {a.lower() for a in AI_BOT_AUTHORS} + count = 0 + for commit in resp.json(): + author = commit.get("commit", {}).get("author", {}) + email = (author.get("email") or "").lower() + name = (author.get("name") or "").lower() + if email in bot_set or name in bot_set: + count += 1 + if count >= AI_COMMIT_THRESHOLD: + return AI_CODE_MSG + except Exception: + pass + return None + + +def check_security_alerts(owner, repo, token): + """Return SECURITY_MSG if the repo has open critical/high Dependabot alerts.""" + 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}/dependabot/alerts", + headers=headers, timeout=TIMEOUT, + params={"state": "open", "severity": "critical,high", "per_page": 1}, + ) + if resp.status_code == 200 and resp.json(): + return SECURITY_MSG + except Exception: + pass + return None + + def check_repo_signals(diff, pr_user, token): """Check GitHub repo author match, stars, and activity for added services.""" findings = [] @@ -185,6 +259,15 @@ def check_repo_signals(diff, pr_user, token): if stars < MIN_STARS and STARS_MSG not in findings: findings.append(STARS_MSG) + if data.get("fork") and FORK_MSG not in findings: + findings.append(FORK_MSG) + + if not data.get("license") and LICENSE_MSG not in findings: + findings.append(LICENSE_MSG) + + if data.get("archived") and ARCHIVED_MSG not in findings: + findings.append(ARCHIVED_MSG) + pushed = data.get("pushed_at") if pushed and ACTIVITY_MSG not in findings: try: @@ -195,6 +278,26 @@ def check_repo_signals(diff, pr_user, token): except Exception: pass + created = data.get("created_at") + if created and MATURITY_MSG not in findings: + try: + created_dt = datetime.fromisoformat(created.replace("Z", "+00:00")) + now = datetime.now(timezone.utc) + if (now - created_dt).days < MIN_AGE_DAYS: + findings.append(MATURITY_MSG) + except Exception: + pass + + if AI_CODE_MSG not in findings: + finding = check_ai_commits(owner, repo, token) + if finding: + findings.append(finding) + + if SECURITY_MSG not in findings: + finding = check_security_alerts(owner, repo, token) + if finding: + findings.append(finding) + return findings diff --git a/lib/checks/check-yaml-diff.py b/lib/checks/check-yaml-diff.py index 5ddc798..0ae7c40 100644 --- a/lib/checks/check-yaml-diff.py +++ b/lib/checks/check-yaml-diff.py @@ -13,6 +13,7 @@ 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_OUTPUT_PATH = "/tmp/pr-diff.json" +SUMMARY_OUTPUT_PATH = "/tmp/pr-diff-summary.md" EXIT_PASS = 0 EXIT_RULE_VIOLATION = 1 @@ -112,13 +113,8 @@ def fmt_path(key): return " → ".join(key) if isinstance(key, tuple) else key -def write_step_summary(diff_result): - """Write a bullet-point Markdown summary to $GITHUB_STEP_SUMMARY.""" - summary_file = os.environ.get("GITHUB_STEP_SUMMARY") - if not summary_file: - return - - lines = ["## YAML Diff Analysis\n"] +def format_diff_bullets(diff_result): + """Build bullet-point lines summarizing all changes. Returns list of strings or empty list.""" bullets = [] for svc in diff_result["services"]["added"]: @@ -149,6 +145,25 @@ def write_step_summary(diff_result): f"in {dup['category']} → {dup['section']}" ) + return bullets + + +def write_diff_summary(diff_result): + """Write the bullet-point summary to a file for downstream consumers.""" + bullets = format_diff_bullets(diff_result) + if bullets: + with open(SUMMARY_OUTPUT_PATH, "w") as f: + f.write("\n".join(bullets) + "\n") + + +def write_step_summary(diff_result): + """Write a bullet-point Markdown summary to $GITHUB_STEP_SUMMARY.""" + summary_file = os.environ.get("GITHUB_STEP_SUMMARY") + if not summary_file: + return + + bullets = format_diff_bullets(diff_result) + lines = ["## YAML Diff Analysis\n"] if bullets: lines.extend(bullets) else: @@ -213,6 +228,7 @@ def main(): write_github_output("has_service_changes", str(bool(added or removed or modified)).lower()) write_step_summary(diff_result) + write_diff_summary(diff_result) added_count = len(added) if added_count > 1: diff --git a/lib/checks/format-comment.py b/lib/checks/format-comment.py index d2789b1..ca00a47 100644 --- a/lib/checks/format-comment.py +++ b/lib/checks/format-comment.py @@ -7,19 +7,9 @@ import sys ARTIFACTS_DIR = "/tmp/artifacts" OUTPUT_DIR = "/tmp/pr-meta" -CONTRIBUTING = "https://github.com/Lissy93/awesome-privacy/blob/main/.github/CONTRIBUTING.md" - -COMMENT_TEMPLATE = """ -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!""" +REPO_URL = "https://github.com/Lissy93/awesome-privacy" +CONTRIBUTING = f"{REPO_URL}/blob/main/.github/CONTRIBUTING.md" +DIFF_SUMMARY_PATH = os.path.join(ARTIFACTS_DIR, "pr-diff-summary.md") def load_findings(filename): @@ -41,12 +31,49 @@ def collect_findings(): 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 load_diff_summary(): + """Load the pre-formatted diff summary, or None if unavailable.""" + try: + with open(DIFF_SUMMARY_PATH) as f: + content = f.read().strip() + return content if content else None + except Exception: + return None + + +def format_comment(findings, user, changes_summary, run_id): + """Build the markdown comment.""" + parts = [ + f"\nHello @{user}\n", + f"Thank you for contributing to Awesome Privacy! We will review your " + f"submission shortly. In the meantime, please ensure all changes are " + f"correct and inline with our [Contributing Requirements]({CONTRIBUTING}).\n", + ] + + if findings: + bullet_list = "\n".join(f"- {f}" for f in findings) + parts.append( + f"Our automated checks detected some issues:\n\n{bullet_list}\n\n" + f"> [!NOTE]\n" + f"> I am a bot, and sometimes make mistakes in my suggestions. " + f"But a human will review your submission shortly!" + ) + else: + parts.append("> ✅ All our automated checks have passed.") + + if changes_summary: + parts.append( + f"
Summary of Changes:\n\n" + f"{changes_summary}\n
" + ) + + if run_id: + parts.append( + f'For full details, please see workflow run ' + f'{run_id}' + ) + + return "\n\n".join(parts) + "\n" def write_step_summary(findings): @@ -81,12 +108,12 @@ def main(): f.write(run_id) findings = collect_findings() + changes_summary = load_diff_summary() 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) + comment = format_comment(findings, user, changes_summary, run_id) + with open(os.path.join(OUTPUT_DIR, "comment.md"), "w") as f: + f.write(comment) except Exception: pass