Skip to content

ci: block the maintainer's address, leave contributors' alone #305

ci: block the maintainer's address, leave contributors' alone

ci: block the maintainer's address, leave contributors' alone #305

Workflow file for this run

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