From 70bddf5ffc8866e7d06fbf99a8d16f36d7f1dda9 Mon Sep 17 00:00:00 2001 From: Kostas Georgiou Date: Tue, 13 Oct 2026 09:59:07 +0300 Subject: [PATCH 1/2] ci: check each pull request for terms that must not appear here --- .github/workflows/private-terms.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/private-terms.yml diff --git a/.github/workflows/private-terms.yml b/.github/workflows/private-terms.yml new file mode 100644 index 0000000..74383f4 --- /dev/null +++ b/.github/workflows/private-terms.yml @@ -0,0 +1,26 @@ +name: Private terms + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + - name: Check the pull request + env: + TERMS: ${{ secrets.PRIVATE_TERMS }} + ALLOW: ${{ secrets.PRIVATE_ALLOW }} + TITLE: ${{ github.event.pull_request.title }} + BODY: ${{ github.event.pull_request.body }} + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: python3 .github/scripts/check_terms.py From f31da263e290a10a39bf969c34f5e80c24febddb Mon Sep 17 00:00:00 2001 From: Kostas Georgiou Date: Tue, 13 Oct 2026 09:59:09 +0300 Subject: [PATCH 2/2] ci: check each pull request for terms that must not appear here --- .github/scripts/check_terms.py | 68 ++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/scripts/check_terms.py diff --git a/.github/scripts/check_terms.py b/.github/scripts/check_terms.py new file mode 100644 index 0000000..5a8f143 --- /dev/null +++ b/.github/scripts/check_terms.py @@ -0,0 +1,68 @@ +"""Check a pull request for terms that must not appear in this repository. + +The terms are not stored here. They come from the PRIVATE_TERMS secret, one regular expression per +line, matched without regard to case. PRIVATE_ALLOW holds text that is removed before the check +(for example the copyright line). The check reads the pull request title and description, every +commit message in the pull request, and every added line of the diff. + +The log never prints the text that matched, because the log of a public repository is public too. +It prints where the match is and the number of the term in the list. + +A pull request from a fork cannot read secrets, so there the check prints a notice and passes. +""" +import os +import re +import subprocess +import sys + + +def patterns(name): + return [re.compile(line.strip(), re.I) for line in os.environ.get(name, "").splitlines() + if line.strip()] + + +def git(*args): + return subprocess.run(["git", *args], capture_output=True, text=True, check=True).stdout + + +def texts(base, head): + """(where, text) for everything this pull request adds.""" + yield "the pull request title", os.environ.get("TITLE", "") + yield "the pull request description", os.environ.get("BODY", "") + for sha in git("rev-list", f"{base}..{head}").split(): + yield f"the message of commit {sha[:7]}", git("log", "-1", "--format=%B", sha) + path, line = None, 0 + for row in git("diff", "--unified=0", "--no-color", f"{base}...{head}").splitlines(): + if row.startswith("+++ "): + path = row[6:] if row.startswith("+++ b/") else None + elif row.startswith("@@"): + m = re.search(r"\+(\d+)", row) + line = int(m.group(1)) if m else 0 + elif row.startswith("+") and path: + yield f"{path}:{line}", row[1:] + line += 1 + + +def main(): + terms = patterns("TERMS") + if not terms: + print("No term list is available (a pull request from a fork cannot read secrets). Skipped.") + return 0 + allow = patterns("ALLOW") + hits = 0 + for where, text in texts(os.environ["BASE"], os.environ["HEAD"]): + for a in allow: + text = a.sub("", text) + for number, term in enumerate(terms, 1): + if term.search(text): + print(f"{where} matches term {number}") + hits += 1 + if hits: + print(f"{hits} match(es). Rewrite these places for a reader outside the project and push again.") + return 1 + print(f"Checked against {len(terms)} terms. Nothing matched.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())