diff --git a/.github/workflows/profile-checks.yml b/.github/workflows/profile-checks.yml new file mode 100644 index 0000000..d90a214 --- /dev/null +++ b/.github/workflows/profile-checks.yml @@ -0,0 +1,30 @@ +name: Profile checks + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + parity: + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: python -m pip install pytest + - name: Check profile parity and privacy boundaries + env: + PROFILE_PRIVATE_REPO_DENYLIST: ${{ secrets.PROFILE_PRIVATE_REPO_DENYLIST }} + run: python -m pytest -q diff --git a/tests/test_private_guard.py b/tests/test_private_guard.py new file mode 100644 index 0000000..9dce7ce --- /dev/null +++ b/tests/test_private_guard.py @@ -0,0 +1,21 @@ +"""Synthetic checks for the externally configured profile privacy guard.""" +import pytest + +from test_profile_parity import check_public_profile + + +def test_public_links_checked_without_private_inventory(monkeypatch): + monkeypatch.delenv("PROFILE_PRIVATE_REPO_DENYLIST", raising=False) + check_public_profile("https://github.com/dev-bricks/CodeBox") + with pytest.raises(AssertionError, match="unlisted repository"): + check_public_profile("https://github.com/dev-bricks/synthetic-unlisted") + + +@pytest.mark.parametrize("separator", [",", ";", "\n", " "]) +def test_configured_guard_redacts_failure(monkeypatch, separator): + name = "synthetic-private-project" + monkeypatch.setenv("PROFILE_PRIVATE_REPO_DENYLIST", f"other{separator}{name}") + with pytest.raises(AssertionError) as caught: + check_public_profile(name.upper()) + assert name not in str(caught.value).casefold() + check_public_profile("Ordinary public profile text") diff --git a/tests/test_profile_parity.py b/tests/test_profile_parity.py index daaa97b..848a99f 100644 --- a/tests/test_profile_parity.py +++ b/tests/test_profile_parity.py @@ -1,5 +1,6 @@ """Text and link parity checks for dev-bricks organization profile, inventory, and public metadata.""" +import os import re from pathlib import Path import pytest @@ -31,12 +32,15 @@ ".github", ] -PRIVATE_REPOS_DENYLIST = [ - "automation-master", - "githubbot", - "ralph-loop-codex-marketplace", - "trusted-peer-setup", -] +def check_public_profile(text): + """Check public links and an optional externally supplied private inventory.""" + public_names = {name.casefold() for name in PUBLIC_REPOS} + links = re.findall(r'https://github\.com/dev-bricks/([^/\s)#?"<>]+)', text) + if any(name.casefold() not in public_names for name in links): + raise AssertionError("Profile contains an unlisted repository link") + private_names = re.split(r"[\s,;]+", os.environ.get("PROFILE_PRIVATE_REPO_DENYLIST", "")) + if any(name and name.casefold() in text.casefold() for name in private_names): + raise AssertionError("Profile contains a prohibited repository reference") PUBLIC_ACTIVITY_DATES = { ".github": "2026-10-01", @@ -94,13 +98,9 @@ def test_public_repo_inventory(file_contents): def test_zero_private_leak_guard(file_contents): - """Zero-leak invariant: ensure private repos are NEVER leaked into public profile surfaces.""" + """Check public links and any externally configured private-name guard.""" for filename in ["README.md", "profile/README.md", "profile/README_de.md", "llms.txt"]: - text = file_contents[filename] - for private_repo in PRIVATE_REPOS_DENYLIST: - assert private_repo not in text, ( - f"SECURITY VIOLATION: Private repository '{private_repo}' leaked into public file {filename}!" - ) + check_public_profile(file_contents[filename]) def test_check_timestamp_parity(file_contents):