Skip to content

Commit b9453f5

Browse files
SAKMZclaude
andcommitted
Initial commit: n8n-iac, infrastructure-as-code for n8n
Author n8n workflows as typed Python, compile them to wire-exact n8n JSON through a deterministic pipeline, validate them offline, and deploy with a Terraform-shaped plan/apply lifecycle. Layout: - n8n_iac/core wire/authoring Pydantic models, the AG -> WIR -> Wire compiler (passes b1-b6), and the total plan validator (R1-R10, C1-C4, W1-W4, H1-H6, S1-S3) - n8n_iac/nodes Node shell plus Webhook, HttpRequest, Set, Switch and the Workflow container - n8n_iac/client layered REST client: transport (retries, pagination, error mapping), resource APIs, and the plan/apply deployer with versionId optimistic locking - n8n_iac/cli validate / plan / apply / destroy / import Architecture is specified in FABLE_SPEC.md and OPUS_SPEC.md, which govern the code rather than describing it. Gates: 519 tests, 100% line and branch coverage, mypy --strict clean, ruff clean. Determinism (byte-identical output under any PYTHONHASHSEED) is a tested property. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0 parents  commit b9453f5

102 files changed

Lines changed: 16023 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitattributes‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Determinism is the point of this project, and the golden fixtures are
2+
# compared byte-for-byte. A Windows checkout rewriting LF -> CRLF would break
3+
# them on that platform only, so line endings are pinned everywhere.
4+
* text=auto eol=lf
5+
6+
*.png binary
7+
*.jpg binary
8+
*.ico binary

‎.github/workflows/ci.yml‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: true
15+
16+
jobs:
17+
# OPUS_SPEC §3.7: Python 3.12 / 3.13 x Linux, macOS, Windows.
18+
# 3.12 is the strict floor -- do not add 3.10/3.11 back.
19+
test:
20+
name: pytest (py${{ matrix.python-version }} / ${{ matrix.os }})
21+
runs-on: ${{ matrix.os }}
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
os: [ubuntu-latest, macos-latest, windows-latest]
26+
python-version: ["3.12", "3.13"]
27+
steps:
28+
- uses: actions/checkout@v4
29+
30+
- name: Install Poetry
31+
run: pipx install poetry
32+
33+
- uses: actions/setup-python@v5
34+
with:
35+
python-version: ${{ matrix.python-version }}
36+
37+
- name: Install dependencies
38+
run: poetry install --no-interaction
39+
40+
# The coverage gate (100% line AND branch) lives in pyproject.toml.
41+
- name: Run tests with coverage gate
42+
run: poetry run pytest --cov --cov-branch
43+
44+
quality:
45+
name: types, lint, codegen drift
46+
runs-on: ubuntu-latest
47+
steps:
48+
- uses: actions/checkout@v4
49+
50+
- name: Install Poetry
51+
run: pipx install poetry
52+
53+
- uses: actions/setup-python@v5
54+
with:
55+
python-version: "3.12"
56+
57+
- name: Install dependencies
58+
run: poetry install --no-interaction
59+
60+
- name: mypy --strict
61+
run: poetry run mypy --strict n8n_iac
62+
63+
- name: ruff
64+
run: poetry run ruff check n8n_iac tests
65+
66+
# The async facade is committed generated code; this fails if it was
67+
# hand-edited or if the sync source moved without regenerating.
68+
- name: Async facade is regeneration-stable
69+
run: poetry run python -m n8n_iac.client._async_codegen --check

‎.gitignore‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
8+
# Distribution / packaging
9+
build/
10+
dist/
11+
*.egg-info/
12+
.eggs/
13+
14+
# Virtualenvs
15+
.venv/
16+
venv/
17+
env/
18+
19+
# Tooling caches
20+
.mypy_cache/
21+
.pytest_cache/
22+
.ruff_cache/
23+
.hypothesis/
24+
.tox/
25+
.nox/
26+
27+
# Coverage
28+
.coverage
29+
.coverage.*
30+
coverage.xml
31+
htmlcov/
32+
33+
# n8n-iac runtime artifacts: deployment state binds uids to a specific
34+
# instance's workflow ids. It is environment-local and must never be shared.
35+
.n8n-iac-state.json
36+
*.n8n-iac-state.json
37+
38+
# Editors / OS
39+
.idea/
40+
.vscode/
41+
*.swp
42+
.DS_Store
43+
Thumbs.db

‎CLAUDE.md‎

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# CLAUDE.md
2+
3+
Guidance for Claude Code (and any contributor) working in this repository.
4+
5+
`n8n_iac` is an infrastructure-as-code SDK for n8n: it models workflows as typed Python,
6+
compiles them to wire-exact n8n JSON through a deterministic pipeline, validates them offline,
7+
and deploys them with a Terraform-shaped plan/apply lifecycle.
8+
9+
---
10+
11+
## Source of Truth
12+
13+
Two specifications define **all** architecture. Code exists to implement them, not to
14+
reinterpret them.
15+
16+
- [`FABLE_SPEC.md`](FABLE_SPEC.md) — the core engine: wire/authoring Pydantic models, the
17+
IR/AST compiler (capture → normalize → validate → emit), and the deterministic plan
18+
validator (rules R1–R10, C1–C3, node rules W1–W4 / H1–H6).
19+
- [`OPUS_SPEC.md`](OPUS_SPEC.md) — the scaffolding: node classes, the layered REST client,
20+
the deployer/CLI, and the Pytest suite structure.
21+
22+
**Every change to architectural logic MUST conform strictly to these specs.** If a task
23+
requires behavior that contradicts a spec, stop and update the spec first (a reviewable
24+
change) — do not diverge in code. If the two specs disagree, `FABLE_SPEC.md` governs the
25+
core engine and `OPUS_SPEC.md` governs everything that depends on it. When in doubt, cite the
26+
governing section (e.g. "FABLE §3.4 C1") in the PR description and the code comment.
27+
28+
---
29+
30+
## Toolchain & Standards (enforced)
31+
32+
| Concern | Requirement |
33+
|---|---|
34+
| **Language** | Python **3.12+**. This is the floor; do not use syntax or stdlib that predates 3.12, and do not lower it. |
35+
| **Dependency & packaging** | **Poetry** only. All deps, scripts, and tool config live in `pyproject.toml`. Do not add `requirements.txt`, `setup.py`, or `pip install` steps. |
36+
| **Data modeling** | **Pydantic v2** exclusively. No v1 APIs, no `pydantic.v1` shim. |
37+
| **Testing** | **Pytest**. No `unittest`, no `nose`. Property tests use Hypothesis (per OPUS §3.3). |
38+
| **Typing** | **`mypy --strict`** must pass with zero errors. Fully annotate every signature; no implicit `Any`, no untyped `def`. `ruff` also gates the build. |
39+
40+
> **Version note.** OPUS §3.7's CI matrix is Python 3.12 / 3.13. **3.12 is the strict
41+
> floor** — never reintroduce 3.10/3.11 support or compatibility shims for them.
42+
43+
---
44+
45+
## Pydantic v2 rules (non-negotiable)
46+
47+
The wire/authoring split (FABLE §1) is the single most important decision in the engine.
48+
Preserve it.
49+
50+
- **Wire models** (`n8n_iac.core.wire`) are serialization-exact and derive from a shared
51+
`StrictBase`:
52+
53+
```python
54+
class StrictBase(BaseModel):
55+
model_config = ConfigDict(
56+
extra="forbid", # unknown keys on the wire are a bug
57+
populate_by_name=True, # snake_case in Python, camelCase on the wire
58+
alias_generator=to_lower_camel,
59+
validate_assignment=True,
60+
frozen=True, # immutable post-construction
61+
ser_json_bytes="base64",
62+
)
63+
```
64+
65+
`extra="forbid"` applies to the **envelope and node metadata only**. Node `parameters`
66+
stay `dict[str, Any]` at the wire layer (FABLE §1.1) — n8n's parameter space is open.
67+
Parameter strictness lives in the **node schema layer** (FABLE §1.4), never on the wire model.
68+
69+
- **Authoring models** (`n8n_iac.core.dsl`) are the Python-facing classes. They are **never
70+
serialized directly** — the compiler lowers them to wire models.
71+
72+
- Node classes (OPUS §1.1) **validate nothing themselves**: `**params` is stored raw and
73+
validated by the compiler against `PARAMS_MODEL`. One validation site, one diagnostic format.
74+
75+
- Pydantic `ValidationError` is **never** raised to the user. The boundary layer converts it
76+
into `SchemaViolation` diagnostics (FABLE §1.5).
77+
78+
- Version-conditional and discriminated shapes use Pydantic **discriminated unions**, not
79+
branching code (FABLE §1.4: `Webhook` `multipleMethods`, HTTP Request `authentication`).
80+
81+
---
82+
83+
## Determinism (a tested property, not an aspiration)
84+
85+
Byte-identical JSON on every machine and every run is a hard requirement (FABLE §2.2, §3.3):
86+
87+
- Never iterate a `dict`/`set` for anything that reaches output; use the canonical order from
88+
compiler pass b6.
89+
- Node IDs and `webhookId` are `uuid5`-derived from a required `Workflow.uid` — **never**
90+
`uuid4`, never random.
91+
- Diagnostics sort by `(severity, rule_id, node_name, port, index)`; cycles report in
92+
lexicographically-minimal rotation.
93+
- `PYTHONHASHSEED` independence is tested (OPUS §3.2 hash-seed matrix). Do not weaken it.
94+
95+
---
96+
97+
## Testing & coverage
98+
99+
- Suite layout is fixed by OPUS §3.1 (`unit/`, `integration/`, `e2e/`, `property/`, `golden/`,
100+
`contract/`). Add tests to the matching directory.
101+
- Coverage target is **100% line and branch** on `n8n_iac/` (`--cov-fail-under=100
102+
--cov-branch`, OPUS §3.6). Every `raise` path and both branches of every discriminated
103+
union / version gate need a test. No blanket `# pragma: no cover`.
104+
- E2E tests (`@pytest.mark.e2e`) need a real n8n and are excluded by default.
105+
106+
---
107+
108+
## Common commands
109+
110+
```bash
111+
poetry install # set up the environment
112+
poetry run pytest # full suite (e2e excluded by default)
113+
poetry run pytest tests/unit # fast unit tests
114+
poetry run pytest -m e2e # opt-in live tests (needs Docker + n8n)
115+
poetry run pytest --update-golden # regenerate golden files (review the diff!)
116+
poetry run mypy --strict n8n_iac # strict type check — must be clean
117+
poetry run ruff check n8n_iac # lint gate
118+
```
119+
120+
---
121+
122+
## Working here
123+
124+
- Match the surrounding code's idiom, naming, and comment density.
125+
- Do not add API transport logic to the core engine, or planning logic to the transport layer
126+
— the layering in OPUS §2.1 is strictly downward.
127+
- When you touch a rule (R*, C*, W*, H*) or a lowering function, update the corresponding
128+
golden fixture and the targeted mutation test (OPUS §3.3–§3.4).

‎CONTRIBUTING.md‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Contributing to n8n-iac
2+
3+
Thanks for considering a contribution. This project has unusually strict gates for its size —
4+
they exist because the whole value proposition is *trustworthy* output, and they are cheap to
5+
keep green if you run them as you go.
6+
7+
## The specs are the source of truth
8+
9+
Architecture is defined in two documents, and code implements them rather than reinterpreting
10+
them:
11+
12+
- [`FABLE_SPEC.md`](FABLE_SPEC.md) — wire/authoring models, the compiler pipeline, and the
13+
validator rules (R1–R10, C1–C4, W1–W4, H1–H6, S1–S3).
14+
- [`OPUS_SPEC.md`](OPUS_SPEC.md) — node classes, the REST client, the deployer/CLI, and the test
15+
suite layout.
16+
17+
**If a change requires behavior that contradicts a spec, update the spec first**, in its own
18+
commit, so the decision is reviewable on its own terms. Don't diverge silently in code. Where the
19+
two disagree, `FABLE_SPEC.md` governs the core engine. Cite the governing section in your PR
20+
description and in the code comment (e.g. "FABLE §3.4 R5").
21+
22+
## The four gates
23+
24+
All four must pass. CI runs them on Python 3.12 and 3.13 across Linux, macOS, and Windows.
25+
26+
```bash
27+
poetry install
28+
poetry run pytest --cov --cov-branch # 100% line AND branch, no exceptions
29+
poetry run mypy --strict n8n_iac # zero errors, fully annotated
30+
poetry run ruff check n8n_iac tests
31+
```
32+
33+
No Poetry on your machine? A plain venv works — `pip install -e .` plus the dev dependencies, or
34+
just set `PYTHONPATH=.`.
35+
36+
### On the coverage gate
37+
38+
100% line *and* branch is the target, and `--cov-fail-under=100` enforces it. Both sides of every
39+
discriminated union and every version gate need a test, and so does every `raise` path.
40+
41+
Blanket `# pragma: no cover` is not accepted. An individual pragma is, when the line is genuinely
42+
unreachable — but it must carry its justification inline and it will be scrutinized in review.
43+
There is currently exactly one in the codebase.
44+
45+
### On typing
46+
47+
`mypy --strict` means no implicit `Any` and no untyped `def`. Two house rules beyond that:
48+
49+
- Prefer an explicit `raise` over `assert` for narrowing. `assert` produces a branch that
50+
coverage can never satisfy, which is why the gate keeps catching it.
51+
- Pydantic `ValidationError` must never escape to the user. Convert it to a `SchemaViolation`
52+
diagnostic at the boundary (FABLE §1.5) so there is one diagnostic format.
53+
54+
## Determinism is a tested property
55+
56+
Byte-identical JSON on every machine, every run, under any `PYTHONHASHSEED`. When touching
57+
anything that reaches output:
58+
59+
- Never iterate a `dict` or `set` for ordering — use the canonical order from compiler pass b6.
60+
- Node IDs and `webhookId` are `uuid5`-derived from `Workflow.uid`. Never `uuid4`, never random.
61+
- Diagnostics sort by `(severity, rule_id, node_name, port, index)`.
62+
63+
## Where tests go
64+
65+
The layout is fixed by OPUS §3.1 — put your test in the matching directory:
66+
67+
| Directory | For |
68+
| -------------------- | ---------------------------------------------------------- |
69+
| `tests/unit/` | Single functions, lowering, individual rules |
70+
| `tests/integration/` | Deployer against the in-process `FakeN8n` |
71+
| `tests/property/` | Hypothesis invariants over generated DAGs |
72+
| `tests/golden/` | Snapshot fixtures (`pytest --update-golden` to regenerate) |
73+
| `tests/e2e/` | `@pytest.mark.e2e` — needs a real n8n, excluded by default |
74+
75+
When you change a rule (R\*, C\*, W\*, H\*) or a lowering function, update the corresponding golden
76+
fixture **and** the targeted mutation test. Review the golden diff by hand — `--update-golden`
77+
will happily bless a regression.
78+
79+
## Especially wanted
80+
81+
- **Live-instance testing.** The e2e suite is the biggest gap in the project; everything today
82+
runs against a fake. Docker-compose fixtures and `@pytest.mark.e2e` coverage are the single
83+
most valuable contribution right now.
84+
- **Node schemas.** 16 types are modeled out of hundreds. Adding one is self-contained: a
85+
`PARAMS_MODEL`, a registry entry, and tests.
86+
- **Bug reports with a minimal reproducing workflow.** A 10-line `Workflow(...)` that
87+
misbehaves is worth more than a long description.
88+
89+
## Pull requests
90+
91+
Keep the diff focused, explain *why* in the description, and cite the spec section for anything
92+
architectural. If you're planning something large, open an issue first so we can agree on the
93+
approach before you write it.

0 commit comments

Comments
 (0)