Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/icm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Stage outputs belong to one run in one checkout and are never committed.
*/stages/*/output/
11 changes: 11 additions & 0 deletions .github/icm/01-fix/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 01-fix — fix a defect

One run is one defect in the signature, the receiver pipeline, the models or the FastAPI helper, or a model that has fallen behind the deliveries (route `model-drift`): reproduced, fixed at its cause, and proposed as a pull request with the evidence. The history so far is the package itself and the fixes that made its publishing work (`git log --oneline origin/main`); this pipeline is the shape the next change takes.

A fix that changes what the receiver accepts, or a public name, is a breaking change: the spec of that is stage 01's last step, and the person decides before stage 02.

Stages, in order; inputs, outputs and checks for each are in [CONTEXT.md](CONTEXT.md):

1. [Reproduce](stages/01_reproduce/CONTEXT.md) — make it fail on purpose.
2. [Fix](stages/02_fix/CONTEXT.md) — change the cause, watch the reproduction pass.
3. [Verify](stages/03_verify/CONTEXT.md) — the checks, the pull request.
9 changes: 9 additions & 0 deletions .github/icm/01-fix/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 01-fix — the pipeline

Make it fail, make it pass for the right reason, prove it where CI will.

| Stage | Job | Input | Output | Human check |
|---|---|---|---|---|
| [`01_reproduce`](stages/01_reproduce/CONTEXT.md) | reproduce the reported defect | the report | `output/reproduction.md` | A person runs the command or reads its output, confirms it fails for the reported reason, and decides whether a behaviour change is wanted before any fix is written. |
| [`02_fix`](stages/02_fix/CONTEXT.md) | change the cause, not the symptom | 01_reproduce's `reproduction.md` | `output/change.md` | A person reads the diff beside the reproduction: it removes the cause, no verification step was loosened or reordered, and any behaviour change is the one approved. Edit `change.md` if it is wrong; stage 03 reads it. |
| [`03_verify`](stages/03_verify/CONTEXT.md) | checks, pull request | 02_fix's `change.md` | `output/pull-request.md` | A person reads `pull-request.md` and its evidence, then says whether to open it. The reviewer, not the author, merges, once every check on the last commit is green. |
25 changes: 25 additions & 0 deletions .github/icm/01-fix/stages/01_reproduce/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 01_reproduce — reproduce the reported defect

One job: turn the report into a pytest case that fails on demand, for the reason reported.

## Inputs
- Working (this run): the report — read whole with what it links: an issue (`gh issue view <n> --comments`), a red run (`gh run view <run-id> --log-failed`), a delivery the receiver rejected or parsed wrongly, or a change to the delivery types in Alteriom/webhook-client.
- Reference (every run): `.github/icm/_shared/architecture.md`
- Reference (every run): `.github/icm/_shared/build-and-test.md`
- Reference (every run): `.github/icm/_shared/gotchas.md`

Do NOT load: the other pipeline, `.github/icm/_shared/release.md`, other runs' output.

## Process
1. If the report could be a vulnerability in the verification (a signature, timestamp or size check that can be bypassed), stop before writing anything: this pipeline ends in a public pull request, and a private report through the repository's Security tab is the route.
2. Restate the report as observed versus expected: the input (a delivery with every secret, id and repository name replaced by test values), the step of the pipeline that answered, and what it should have done.
3. Find the step with [architecture.md](../../../_shared/architecture.md) and name the suspected cause as `path:line`. For `model-drift`, name the field and the value the model refuses, and where the TypeScript types or the supplied delivery show it.
4. In a virtual environment with `pip install -e ".[dev]"`, write the failing test in the file [build-and-test.md](../../../_shared/build-and-test.md) "Where a test belongs" names, signing deliveries with `sign_payload` and a current timestamp as the existing tests do.
5. Run it (`pytest tests/<file> -v`) and keep the failing output. If it passes, the reproduction or the report is wrong: say which, and stop.
6. Say whether the fix will change what the receiver accepts or a public name ([gotchas.md](../../../_shared/gotchas.md) "Deliveries without an aggregate count as duplicates" is one already open).

## Outputs
- reproduction.md → output/ — the restated report; the suspected cause (`path:line`); the test and command; the failing output; whether the fix is a behaviour change.

## Human check
A person runs the command or reads its output, confirms it fails for the reported reason, and decides whether a behaviour change is wanted before any fix is written.
23 changes: 23 additions & 0 deletions .github/icm/01-fix/stages/02_fix/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 02_fix — change the cause, not the symptom

One job: the smallest change at the defect's cause that turns the reproduction green without weakening any check.

## Inputs
- Working (this run): `.github/icm/01-fix/stages/01_reproduce/output/reproduction.md`
- Reference (every run): `.github/icm/_shared/architecture.md`
- Reference (every run): `.github/icm/_shared/build-and-test.md`

Do NOT load: the other pipeline, `.github/icm/_shared/release.md`, other runs' output.

## Process
1. Confirm the cause in the code before changing anything; if it moved, record where it really is.
2. Change the cause, within what the person approved at stage 01. The verification order stays as [architecture.md](../../../_shared/architecture.md) "The modules" lists it; no check is loosened, and the comparison stays constant-time.
3. A model change follows the TypeScript types in Alteriom/webhook-client or the delivery the person supplied: a new field is optional with a default; a new `Literal` value is added, not the `Literal` removed.
4. Keep `__all__` and every public signature unless the person approved otherwise. Do not edit the reproduction to make it pass.
5. Run the reproduction (green now), then `pytest tests/ -v` and `python -m build`.

## Outputs
- change.md → output/ — the cause in one paragraph; each file changed and why; the reproduction's passing output; the full test and build results; anything deliberately not addressed.

## Human check
A person reads the diff beside the reproduction: it removes the cause, no verification step was loosened or reordered, and any behaviour change is the one approved. Edit `change.md` if it is wrong; stage 03 reads it.
22 changes: 22 additions & 0 deletions .github/icm/01-fix/stages/03_verify/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 03_verify — checks, pull request

One job: the pull request a reviewer can merge, with the evidence CI will look for.

## Inputs
- Working (this run): `.github/icm/01-fix/stages/02_fix/output/change.md`
- Reference (every run): `.github/icm/_shared/build-and-test.md`
- Reference (only when the README describes the changed behaviour): `README.md`

Do NOT load: the other pipeline, other runs' output.

## Process
1. Run what CI runs ([build-and-test.md](../../../_shared/build-and-test.md) "What CI runs"): the tests on each Python version you have among 3.10 to 3.12 (at least the oldest), and `python -m build`.
2. Update the `README.md` section that described the old behaviour. Do not change `pyproject.toml`'s version: that is a release.
3. Write the pull request: what was wrong, how you know (the reproduction), what the change does, whether it changes what the receiver accepts; the checks run and their results. Title `fix(<module>): <the outcome>`. Nothing in it names a real secret, host or delivery.
4. After the human check, the agent opens it against `main` (`gh pr create --base main --title "<title>" --body-file <this stage's output/pull-request.md>`). The agent never merges.

## Outputs
- pull-request.md → output/ — the title and body, the checks run with their results, the README change as written.

## Human check
A person reads `pull-request.md` and its evidence, then says whether to open it. The reviewer, not the author, merges, once every check on the last commit is green.
11 changes: 11 additions & 0 deletions .github/icm/02-release/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 02-release — release a version

One run is one version of `alteriom-webhook-client`, from the merged changes to the same number on PyPI. One release so far (`v0.1.0`), and it took three runs of `publish.yml` while publishing moved to trusted publishing, which is why the steps are written down.

A published GitHub release uploads to PyPI, so publishing it is the person's act, after the version bump has merged. The agent plans, prepares the bump, drafts the notes, and confirms the result. It never creates a tag or a release, and never uploads.

Stages, in order; inputs, outputs and checks for each are in [CONTEXT.md](CONTEXT.md):

1. [Plan](stages/01_plan/CONTEXT.md) — what ships, and which number.
2. [Prepare](stages/02_prepare/CONTEXT.md) — the version bump pull request and the draft notes.
3. [Publish](stages/03_publish/CONTEXT.md) — the person publishes the release; the agent confirms PyPI.
9 changes: 9 additions & 0 deletions .github/icm/02-release/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 02-release — the pipeline

Choose the number, bump it in one pull request, let a person publish the release, and confirm PyPI has it.

| Stage | Job | Input | Output | Human check |
|---|---|---|---|---|
| [`01_plan`](stages/01_plan/CONTEXT.md) | choose what ships and its number | the changes since the previous release | `output/release-plan.md` | A maintainer approves the version number and the notes. The number is permanent once uploaded: PyPI never takes the same version twice. |
| [`02_prepare`](stages/02_prepare/CONTEXT.md) | the version bump pull request | 01_plan's `release-plan.md` | `output/release-pr.md` | A maintainer reviews the bump with every check green, merges it, and then decides to publish: `gh release create vX.Y.Z --target <merge commit on main> --notes-file <notes>` publishes to PyPI. |
| [`03_publish`](stages/03_publish/CONTEXT.md) | the person publishes, the agent confirms | 02_prepare's `release-pr.md` | `output/publication.md` | A person confirms the version is on PyPI and installs, built from `main`, before it is announced. |
23 changes: 23 additions & 0 deletions .github/icm/02-release/stages/01_plan/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 01_plan — choose what ships and its number

One job: a release plan that names the version, justifies its class, and drafts the release notes.

## Inputs
- Working (this run): the changes since the previous release — `git log --oneline v<previous>..origin/main` and `git diff --stat v<previous> origin/main -- src/ pyproject.toml`.
- Reference (every run): `.github/icm/_shared/release.md`
- Reference (every run): `.github/icm/_shared/gotchas.md`

Do NOT load: the other pipeline, other runs' output.

## Process
1. Find the previous tag (`git tag --list 'v*' --sort=-v:refname | head -3`) and what PyPI has (`pip index versions alteriom-webhook-client`).
2. List every merged change and say whether it changes what ships (`src/`, `pyproject.toml` dependencies) or not. Nothing that ships means no release: say so and stop.
3. Classify against [release.md](../../../_shared/release.md) "The number" and say which change decides it. A change to what the receiver accepts is named first.
4. Confirm `main` is green (`gh run list --branch main --workflow ci.yml --limit 3`).
5. Draft the release notes in the shape of `v0.1.0`'s (`gh release view v0.1.0`), with the install line for the new version.

## Outputs
- release-plan.md → output/ — the version and the change that decides its class; what ships; the state of `main`; the drafted notes.

## Human check
A maintainer approves the version number and the notes. The number is permanent once uploaded: PyPI never takes the same version twice.
22 changes: 22 additions & 0 deletions .github/icm/02-release/stages/02_prepare/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 02_prepare — the version bump pull request

One job: one pull request that sets the version, passes every check, and carries the approved notes for the person to publish.

## Inputs
- Working (this run): `.github/icm/02-release/stages/01_plan/output/release-plan.md`
- Reference (every run): `.github/icm/_shared/release.md`
- Reference (every run): `.github/icm/_shared/build-and-test.md`

Do NOT load: the other pipeline, other runs' output.

## Process
1. Branch `release/X.Y.Z` from current `origin/main`.
2. Set `version` in `pyproject.toml` to `X.Y.Z`; nothing else carries the version ([release.md](../../../_shared/release.md) "The number").
3. In a fresh virtual environment: `pip install -e ".[dev]"`, `pytest tests/ -v`, `python -m build`, and check that `dist/` holds `alteriom_webhook_client-X.Y.Z` wheel and sdist.
4. Open the pull request against `main`, titled `chore: release vX.Y.Z`; the body is the approved notes, the checks' output, and the steps the person takes after the merge.

## Outputs
- release-pr.md → output/ — the pull request link, the diff, the checks' output, and the person's steps: tag `vX.Y.Z` on the merge commit, then publish a GitHub release from that tag with the approved notes.

## Human check
A maintainer reviews the bump with every check green, merges it, and then decides to publish: `gh release create vX.Y.Z --target <merge commit on main> --notes-file <notes>` publishes to PyPI.
21 changes: 21 additions & 0 deletions .github/icm/02-release/stages/03_publish/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 03_publish — the person publishes, the agent confirms

One job: confirm that the release the person published uploaded the right version from the right commit. The agent runs no publishing command.

## Inputs
- Working (this run): `.github/icm/02-release/stages/02_prepare/output/release-pr.md` — or, for a check of an earlier release (route `publish-check`), only the tag `vX.Y.Z` the person names; start at step 2.
- Reference (every run): `.github/icm/_shared/release.md`
- Reference (only when the run failed at the upload): `.github/icm/_shared/gotchas.md`

Do NOT load: the other pipeline, other runs' output, the source tree.

## Process
1. Confirm the tag points at the merge commit on `main` (`git fetch --tags && git branch -r --contains vX.Y.Z` lists `origin/main`) and that `pyproject.toml` there says `X.Y.Z`.
2. Find the `publish.yml` run for the release (`gh run list --workflow publish.yml --limit 3`) and read both jobs. A failure in `test` uploaded nothing. A failure at the upload is read against [gotchas.md](../../../_shared/gotchas.md) "Publishing is tied to names on PyPI's side" before anything is re-run; the person decides the re-run.
3. Check PyPI for `X.Y.Z` (`pip index versions alteriom-webhook-client`), and that a clean environment can `pip install alteriom-webhook-client==X.Y.Z` and import `WebhookReceiver`.

## Outputs
- publication.md → output/ — the tag and its commit; the workflow run with each job's result; the version on PyPI with a link; the install check.

## Human check
A person confirms the version is on PyPI and installs, built from `main`, before it is announced.
13 changes: 13 additions & 0 deletions .github/icm/_shared/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Shared reference

What every job on this repository draws on. Each file points at the repository file that owns a fact; follow the pointer for the detail.

| File | Answers |
|---|---|
| [architecture.md](architecture.md) | where each part of the package lives, and what it must agree with in the TypeScript client |
| [build-and-test.md](build-and-test.md) | how to install, test and build, where a test belongs, what CI runs |
| [release.md](release.md) | how a version is chosen and published, and what only a person does |
| [gotchas.md](gotchas.md) | traps already in the code or the history, each with its source |
| [documents.md](documents.md) | the repository's documents |

Each stage's contract names the files it loads.
22 changes: 22 additions & 0 deletions .github/icm/_shared/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
verified_at: 2026-10-15
verified_commit: 337bb90fc61fd822113654f550d491de4e76aa38
---

# Architecture — where each part of the package lives

## The modules

- `src/alteriom_webhook_client/signature.py` — `sign_payload` and `verify_signature`: HMAC-SHA256 over `"{timestamp}." + body` (the raw bytes), as `sha256=<hex>`, compared with `hmac.compare_digest`.
- `src/alteriom_webhook_client/receiver.py` — `WebhookReceiver.receive` (`src/alteriom_webhook_client/receiver.py:71`) runs the pipeline in order: payload size (413), the two signature headers, timestamp age, signature, JSON, model validation (400 for both), then duplicate detection (`src/alteriom_webhook_client/receiver.py:129`) in an in-memory LRU with a one-hour TTL (`src/alteriom_webhook_client/receiver.py:26`). Every failure is a `WebhookVerificationError` with a status code.
- `src/alteriom_webhook_client/models.py` — Pydantic v2 models `SubscriptionDelivery`, `Aggregate`, `Enrichment`, `WebhookEvent`.
- `src/alteriom_webhook_client/fastapi.py` — `webhook_receiver(...)` returns a FastAPI dependency that reads the raw body and turns the error into an `HTTPException`. FastAPI is an optional extra (`pyproject.toml:17`); only this module imports it.
- `src/alteriom_webhook_client/__init__.py` — the public names (`__all__`); the FastAPI helper is imported from its own module.

## What it must agree with

The signature format and the delivery models mirror the TypeScript package in Alteriom/webhook-client (its `src/receiver.ts` and `src/types.ts`), as the module docstrings say (`src/alteriom_webhook_client/signature.py:3`, `src/alteriom_webhook_client/models.py:3`). The service that sends the deliveries is private: a model or header change starts from the TypeScript package or from a delivery the person supplies, never from guesses about the service.

## The package

Hatchling builds the wheel from `src/alteriom_webhook_client` (`pyproject.toml:26`). The only runtime dependency is `pydantic>=2.0` (`pyproject.toml:12`); Python 3.10 or later (`pyproject.toml:11`). The version is `pyproject.toml:7`; the package does not expose `__version__`.
20 changes: 20 additions & 0 deletions .github/icm/_shared/build-and-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
verified_at: 2026-10-15
verified_commit: 337bb90fc61fd822113654f550d491de4e76aa38
---

# Build and test — the commands, and what CI runs

## Locally

- In a virtual environment: `pip install -e ".[dev]"` (pytest, pytest-asyncio, httpx, FastAPI, uvicorn: `pyproject.toml:18`), then `pytest tests/ -v`. Async tests run without markers (`asyncio_mode = "auto"`, `pyproject.toml:29`).
- `pip install build && python -m build` makes the sdist and wheel in `dist/`, as CI does.
- There is no linter or type checker configured; follow the style of the module you touch.

## Where a test belongs

One file per module: `tests/test_signature.py` (format and comparison), `tests/test_receiver.py` (each pipeline step and its status code, with deliveries signed by `sign_payload` and a current timestamp), `tests/test_fastapi.py` (the dependency through FastAPI's test client over httpx). No test reaches a network or a real secret.

## What CI runs

`ci.yml` on pull requests to `main` and pushes to `main`: the tests on Python 3.10, 3.11 and 3.12 (`.github/workflows/ci.yml:14`), then a build whose `dist/` is uploaded as an artifact. `publish.yml` runs the tests again before it publishes ([release.md](release.md)). `icm-check.yml` runs when an entry file or `.github/icm/` changes; CodeQL runs on its schedule.
18 changes: 18 additions & 0 deletions .github/icm/_shared/documents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
verified_at: 2026-10-15
verified_commit: 337bb90fc61fd822113654f550d491de4e76aa38
---

# Documents — what this repository has for people

The agent-facing files are the root `AGENTS.md` (the entry) and this `.github/icm/` folder. The one user document restates facts the code owns, and the code is right when they disagree.

| Document | What it answers | Owner of the facts |
|---|---|---|
| [README.md](../../../README.md) | install, the FastAPI quick start, manual and low-level use, configuration, the models | `src/alteriom_webhook_client/` |

There is no changelog, contributing guide or security policy file; the GitHub release notes record what changed, and private vulnerability reporting is enabled on the repository.

## Known drift

None found at the commit above: the README's defaults (five-minute age, 10 MB) match `WebhookReceiver` (`src/alteriom_webhook_client/receiver.py:51`).
Loading
Loading