signal-bot/.github/workflows/ci.yml
Krzysztof kuhy Rudnicki 37a27c6dff feat: refuse to commit personal data, learned from outside the repository
The leak this repository had was not carelessness with a secret. It was
somebody reaching for a real name because it was the example in front of
them -- a fixture built from a live database, an envelope pasted out of a
running container, a help text written while looking at the actual group.
Three different people-shaped mistakes, none of which a rule about "be
careful" would have stopped.

So it is a gate. The hard part is that the thing being checked must not
contain the list of what to refuse, or the check *is* the leak. It reads the
names at run time from outside: the photo pack's manifest, the message
database, the local .env. It never prints the word it matched, because this
output reaches CI logs. On a machine with none of that private data it says
"shapes only" out loud rather than passing quietly, and CI enforces the half
that needs nothing -- phone numbers, uuids and Signal group ids, compared
against an allowlist of values invented for the fixtures.

It runs on staged files, on the commit message -- where two of the leaks
actually travelled -- and over the whole tree in CI.

It paid for itself twice before it was wired up. The live test group's id was
in tests/test_config.py in two spellings, and truncated in three more files,
after four rounds of grepping by hand had pronounced the tree clean -- nobody
thinks to grep for base64. Then it refused the commit that introduced it,
because DOCS-privacy.md had used a real name as the example of a name.

An allowlist line may be scoped to one file, which is how the given names
that are also popes stay legal in popes.json and refused in every fixture
that has no business carrying them. The threshold is three characters,
because one of this group's given names is three characters long.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017w1HZ3cZ7neL3rFdQ6VMBA
2026-08-30 23:28:29 +02:00

71 lines
2.1 KiB
YAML

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Set up Python
run: uv python install 3.14
- name: Install dependencies
run: |
uv venv .venv
uv pip install --python .venv/bin/python -e '.[dev]'
# Mirrors the no-noqa pre-commit hook. A suppression must not be able
# to reach main just because someone committed without hooks installed.
- name: block suppressions
run: |
if grep -rnEi '#\s*(noqa|type:\s*ignore|ruff:\s*noqa)' \
--include='*.py' signal_bot fake_signal tests; then
echo '::error::suppressions are not allowed; fix the finding or ask first'
exit 1
fi
# The name half of this check needs the operator's private data and so
# can only run locally; the shape half -- phone numbers, uuids, Signal
# group ids -- works anywhere and is what CI enforces on every push.
- name: no personal data
run: ./scripts/check_personal_data.sh --all
- name: ruff format
run: .venv/bin/ruff format --check .
- name: ruff check (ALL rules)
run: .venv/bin/ruff check .
- name: mypy --strict
run: .venv/bin/mypy signal_bot fake_signal
- name: shellcheck
# --severity=style is the floor CI enforces; the local pre-commit hook
# uses the same flag so a version skew cannot pass locally and fail here.
run: shellcheck -x --severity=style run.sh scripts/*.sh
- name: pytest (100% branch coverage)
run: .venv/bin/python -m pytest -q
# The real bot against the fake Signal server. No account, no network.
- name: end-to-end
run: ./scripts/e2e.sh
# Boot order reversed: the bot starts first and Signal appears later,
# which is what a reboot looks like and what put the unit in `failed`
# for eight hours on 2026-08-30.
- name: cold start
run: ./scripts/cold_start.sh