ci: block the maintainer's address, leave contributors' alone #305
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ["v*"] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| full_matrix: | |
| description: "Also run Windows and macOS (billed at 2x and 10x). Tags do this automatically." | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Every PR commit must carry a Signed-off-by trailer (DCO -- see CONTRIBUTING.md). | |
| dco: | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - name: Check Signed-off-by on every PR commit | |
| env: | |
| # No `git fetch` here: persist-credentials:false leaves the checkout without | |
| # credentials, so fetching fails with "could not read Username". fetch-depth:0 | |
| # already put the base commit in the local history, so the event's base SHA is | |
| # enough. Passed through the environment rather than interpolated into the | |
| # shell, so no ${{ }} value is ever evaluated as code. | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| run: | | |
| missing=0 | |
| while read -r sha; do | |
| if ! git log -1 --format=%B "$sha" | grep -qi '^Signed-off-by: .\+ <.\+@.\+>'; then | |
| echo "::error::commit $sha has no Signed-off-by trailer" | |
| missing=1 | |
| fi | |
| # --no-merges: on a pull_request event the checkout HEAD is GitHub's own | |
| # synthetic merge commit (authored by web-flow), which can never carry a | |
| # sign-off. DCO applies to authored commits, not the platform's merge. | |
| done < <(git rev-list --no-merges "$BASE_SHA..HEAD") | |
| exit "$missing" | |
| # Commit messages and PR bodies are as public as the diff and much easier to leak | |
| # into -- container paths, session identifiers, personal addresses. Checked here | |
| # because the local test can only see the history it has. | |
| no-private-data: | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - name: Scan commit messages for local or personal data | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| run: | | |
| # Local paths and session identifiers. | |
| local_data='/home/[A-Za-z0-9._-]+/|/Users/[A-Za-z0-9._-]+/|session_[0-9A-Za-z]{12,}|claude\.ai/code/session|/tmp/claude-[0-9]' | |
| # Any address, not a handful of consumer domains: a corporate or self-hosted | |
| # address is somebody's just as much as a gmail one. The last label must look like | |
| # a TLD, so `claude_code@2.1.263.json` and `pkg@1.2.3.tar.gz` are not matches. | |
| email='[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,24}' | |
| # Addresses that identify a service rather than a person. | |
| allowed='@users\.noreply\.github\.com$|^noreply@anthropic\.com$|^support@github\.com$|^noreply@github\.com$' | |
| # Suffixes that make a match a file path rather than an address. | |
| not_mail='\.(json|ya?ml|md|txt|py|js|ts|tsx|sh|toml|cfg|ini|lock|log|csv|tsv|html?|svg|png|jpe?g|gif|pdf|zip|gz|tar|whl|so|dll|exe)$' | |
| found=0 | |
| while read -r sha; do | |
| msg=$(git log -1 --format=%B "$sha") | |
| # Name the commit, never echo the match: a log line is a publication too. | |
| if printf '%s' "$msg" | grep -qiE "$local_data"; then | |
| echo "::error::commit $sha contains a local path or session identifier" | |
| found=1 | |
| fi | |
| if printf '%s' "$msg" | grep -oiE "$email" | grep -ivE "$not_mail" | grep -qivE "$allowed"; then | |
| echo "::error::commit $sha contains an email address; sign off with your GitHub noreply address" | |
| found=1 | |
| fi | |
| done < <(git rev-list --no-merges "$BASE_SHA..HEAD") | |
| exit "$found" | |
| brand-assets: | |
| # docs/assets/social-preview.svg is derived from the package -- adapters, events, | |
| # decision outcomes, version -- so adding an adapter or an event silently invalidates | |
| # the committed card. Nothing on it is hand-typed (every count is len() of a list read | |
| # at render time), but a stale image is wrong all the same, and the image is what a | |
| # link preview shows to someone who has not read the repo yet. | |
| # | |
| # The check compares the SVG, not the PNG: the SVG is text derived only from | |
| # repository data and is identical on any machine, while a PNG depends on the font | |
| # installed on the renderer. Facts reach the SVG first. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 | |
| with: | |
| python-version: "3.12" | |
| - name: Install the asset renderer | |
| run: pip install --require-hashes -r .github/requirements/assets.txt | |
| - name: Check the social preview matches the package | |
| run: cd docs/assets && python gen_brand_assets.py --check | |
| lint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 | |
| with: | |
| python-version: "3.11" | |
| - run: pip install --require-hashes -r .github/requirements/lint.txt | |
| - run: ruff check . | |
| - run: ruff format --check . | |
| test: | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # Linux-only by default; Actions minutes are billed 2x on Windows and 10x on | |
| # macOS. The other platforms are not deleted because the defects that justify | |
| # them are platform-specific (path handling, line endings, shell invocation), | |
| # and a release must never be cut having been tested on Linux alone -- so a | |
| # v* tag always runs the full matrix without anyone having to remember. | |
| os: [ubuntu-latest] | |
| # 3.9 is the floor the package claims; 3.13 surfaces deprecations early. | |
| python-version: ["3.9", "3.11", "3.13"] | |
| include: >- | |
| ${{ fromJSON( | |
| (startsWith(github.ref, 'refs/tags/v') || inputs.full_matrix) | |
| && '[{"os":"windows-latest","python-version":"3.11"},{"os":"macos-latest","python-version":"3.11"}]' | |
| || '[]') }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - run: pip install --require-hashes -r .github/requirements/dev.txt | |
| # --no-deps because dev.txt already carries every dependency, hash-verified; | |
| # the package itself declares none. Resolving here would reintroduce an | |
| # unverified fetch on top of a verified set. | |
| - run: pip install --no-deps -e . | |
| - run: pytest -q | |
| - name: CLI smoke | |
| shell: bash | |
| run: | | |
| agentseam --version | |
| agentseam agents | |
| agentseam matrix | head -3 | |
| agentseam doctor --repo . | |
| # The runtime path must stay importable with no third-party packages: adapters are | |
| # copied verbatim into other projects, so an accidental dependency breaks exactly | |
| # the consumers this library exists for. Cheaper to assert than to discover. | |
| # The vendor example pages are generated from the real code paths, so a behaviour change | |
| # that nobody regenerated leaves documentation asserting something the library no longer | |
| # does. Same script the test suite runs, for the reason given on the stdlib-only job. | |
| examples: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 | |
| with: | |
| python-version: "3.11" | |
| - name: Generated examples match the library | |
| run: python examples/generate.py --check | |
| stdlib-only: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 | |
| with: | |
| python-version: "3.11" | |
| # The same script the test suite runs, on purpose: the first version of this | |
| # check was inline here, drifted from the local test, and shipped a red pipeline. | |
| - name: Import with user site-packages disabled | |
| env: | |
| PYTHONNOUSERSITE: "1" | |
| run: python tests/check_stdlib_only.py | |
| # docs/figures/make_*.py render the README's figures FROM src/agentseam/matrix.py -- an | |
| # agent or event added to the matrix, or a grade that shifts, changes a committed SVG. | |
| # Regenerating and diffing here is the only thing that catches a figure nobody re-ran. | |
| # No cairosvg install: the generators are stdlib-only plus docs/figures/palette.py, and | |
| # social-card.png (which does use cairosvg, guarded by an ImportError fallback in | |
| # make_social.py) is not reproduced here on purpose -- it depends on the renderer's | |
| # installed fonts, so only the SVG it was rendered from is asserted against the package. | |
| figures: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 | |
| with: | |
| python-version: "3.12" | |
| - name: Regenerate figures | |
| run: cd docs/figures && for g in make_*.py; do python "$g"; done | |
| - name: Fail on drift | |
| run: git diff --exit-code -- docs/figures |