@biffo/cli 0.39.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/_skeletons/plugin-template/.github/workflows/ci.yml +124 -0
  2. package/_skeletons/plugin-template/.github/workflows/release.yml +114 -0
  3. package/_skeletons/plugin-template/README.md +232 -0
  4. package/_skeletons/plugin-template/biffo.plugin.json +89 -0
  5. package/_skeletons/plugin-template/pyproject.toml +100 -0
  6. package/_skeletons/plugin-template/registry-schema.json +294 -0
  7. package/_skeletons/plugin-template/src/__init__.py +0 -0
  8. package/_skeletons/plugin-template/src/example_plugin/__init__.py +10 -0
  9. package/_skeletons/plugin-template/src/example_plugin/main.py +63 -0
  10. package/_skeletons/plugin-template/src/example_plugin/manifest.py +30 -0
  11. package/_skeletons/plugin-template/src/example_plugin/plugin.py +92 -0
  12. package/_skeletons/plugin-template/terraform/README.md +146 -0
  13. package/_skeletons/plugin-template/terraform/main.tf +167 -0
  14. package/_skeletons/plugin-template/terraform/outputs.tf +33 -0
  15. package/_skeletons/plugin-template/terraform/variables.tf +133 -0
  16. package/_skeletons/plugin-template/tests/conftest.py +17 -0
  17. package/_skeletons/plugin-template/tests/fakes.py +74 -0
  18. package/_skeletons/plugin-template/tests/test_example_plugin.py +108 -0
  19. package/_skeletons/registry/README.md +27 -0
  20. package/_skeletons/registry/plugins.json +5 -0
  21. package/_skeletons/registry/registry-schema.json +294 -0
  22. package/_skeletons/sibling-template/.github/renovate.json +38 -0
  23. package/_skeletons/sibling-template/.github/workflows/ci.yml +185 -0
  24. package/_skeletons/sibling-template/.github/workflows/codeql.yml +56 -0
  25. package/_skeletons/sibling-template/.github/workflows/deploy.yml +219 -0
  26. package/_skeletons/sibling-template/.github/workflows/destroy-infra.yml +73 -0
  27. package/_skeletons/sibling-template/README.md +164 -0
  28. package/_skeletons/sibling-template/_gitignore +71 -0
  29. package/_skeletons/sibling-template/apps/frontend/.env.example +14 -0
  30. package/_skeletons/sibling-template/apps/frontend/eslint.config.mjs +10 -0
  31. package/_skeletons/sibling-template/apps/frontend/next.config.ts +20 -0
  32. package/_skeletons/sibling-template/apps/frontend/package.json +42 -0
  33. package/_skeletons/sibling-template/apps/frontend/pnpm-lock.yaml +5268 -0
  34. package/_skeletons/sibling-template/apps/frontend/pnpm-workspace.yaml +19 -0
  35. package/_skeletons/sibling-template/apps/frontend/src/app/globals.css +34 -0
  36. package/_skeletons/sibling-template/apps/frontend/src/app/layout.tsx +15 -0
  37. package/_skeletons/sibling-template/apps/frontend/src/app/page.test.tsx +75 -0
  38. package/_skeletons/sibling-template/apps/frontend/src/app/page.tsx +85 -0
  39. package/_skeletons/sibling-template/apps/frontend/src/lib/api-client.ts +58 -0
  40. package/_skeletons/sibling-template/apps/frontend/src/lib/auth.test.ts +135 -0
  41. package/_skeletons/sibling-template/apps/frontend/src/lib/auth.ts +87 -0
  42. package/_skeletons/sibling-template/apps/frontend/src/test-setup.ts +1 -0
  43. package/_skeletons/sibling-template/apps/frontend/tsconfig.json +29 -0
  44. package/_skeletons/sibling-template/apps/frontend/vitest.config.ts +18 -0
  45. package/_skeletons/sibling-template/biffo.sibling.json +7 -0
  46. package/_skeletons/sibling-template/infra/backend.tf +23 -0
  47. package/_skeletons/sibling-template/infra/main.tf +79 -0
  48. package/_skeletons/sibling-template/infra/outputs.tf +17 -0
  49. package/_skeletons/sibling-template/infra/variables.tf +63 -0
  50. package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/main.tf +114 -0
  51. package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/outputs.tf +12 -0
  52. package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/variables.tf +41 -0
  53. package/_skeletons/sibling-template/modules/cloud/aws/compute/main.tf +157 -0
  54. package/_skeletons/sibling-template/modules/cloud/aws/compute/outputs.tf +4 -0
  55. package/_skeletons/sibling-template/modules/cloud/aws/compute/variables.tf +70 -0
  56. package/_skeletons/sibling-template/modules/cloud/aws/storage/main.tf +78 -0
  57. package/_skeletons/sibling-template/modules/cloud/aws/storage/outputs.tf +4 -0
  58. package/_skeletons/sibling-template/modules/cloud/aws/storage/variables.tf +12 -0
  59. package/_skeletons/sibling-template/services/api/pyproject.toml +69 -0
  60. package/_skeletons/sibling-template/services/api/src/api/__init__.py +0 -0
  61. package/_skeletons/sibling-template/services/api/src/api/config.py +31 -0
  62. package/_skeletons/sibling-template/services/api/src/api/core_client.py +64 -0
  63. package/_skeletons/sibling-template/services/api/src/api/main.py +43 -0
  64. package/_skeletons/sibling-template/services/api/src/api/middleware/__init__.py +0 -0
  65. package/_skeletons/sibling-template/services/api/src/api/middleware/auth.py +111 -0
  66. package/_skeletons/sibling-template/services/api/src/api/routers/__init__.py +0 -0
  67. package/_skeletons/sibling-template/services/api/src/api/routers/whoami.py +21 -0
  68. package/_skeletons/sibling-template/services/api/tests/conftest.py +24 -0
  69. package/_skeletons/sibling-template/services/api/tests/test_whoami.py +21 -0
  70. package/_skeletons/sibling-template/services/api/uv.lock +1160 -0
  71. package/core.version +1 -1
  72. package/dist/index.js +113 -87
  73. package/package.json +5 -4
@@ -0,0 +1,124 @@
1
+ name: CI
2
+
3
+ # Scaled down from biffo-template's own root .github/workflows/ci.yml for a
4
+ # single Python package: a plugin repo (per ADR-0003) extends the SDK and
5
+ # has no JS/TS and no Terraform of its own — Terraform, if a plugin ships
6
+ # any, is provisioned by the *consuming* project's infra, not this repo's
7
+ # CI (see README's "What this repo does not build" section).
8
+
9
+ on:
10
+ push:
11
+ branches: [main]
12
+ pull_request:
13
+ branches: [main]
14
+
15
+ concurrency:
16
+ group: ${{ github.workflow }}-${{ github.ref }}
17
+ cancel-in-progress: true
18
+
19
+ env:
20
+ PYTHON_VERSION: '3.13'
21
+
22
+ jobs:
23
+ lint:
24
+ name: Lint
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
28
+ - uses: astral-sh/setup-uv@v5
29
+ with:
30
+ enable-cache: true
31
+ - run: uv sync --all-groups
32
+ - run: uv run ruff check .
33
+ - run: uv run ruff format --check .
34
+
35
+ typecheck:
36
+ name: Type Check
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
40
+ - uses: astral-sh/setup-uv@v5
41
+ with:
42
+ enable-cache: true
43
+ - run: uv sync --all-groups
44
+ - run: uv run pyright
45
+
46
+ test:
47
+ name: Test
48
+ runs-on: ubuntu-latest
49
+ steps:
50
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
51
+ - uses: astral-sh/setup-uv@v5
52
+ with:
53
+ enable-cache: true
54
+ - run: uv sync --all-groups
55
+ - run: uv run pytest --cov --cov-report=xml
56
+ - uses: codecov/codecov-action@v5
57
+ if: always()
58
+ with:
59
+ files: coverage.xml
60
+
61
+ validate-manifest:
62
+ name: Validate biffo.plugin.json
63
+ runs-on: ubuntu-latest
64
+ steps:
65
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
66
+ - uses: astral-sh/setup-uv@v5
67
+ with:
68
+ enable-cache: true
69
+ - run: uv sync --all-groups
70
+ # Authoritative check: load_manifest() runs the *real* Pydantic model
71
+ # (biffo_plugin_sdk.plugin.PluginManifest) that the SDK, the Core API's
72
+ # discover_plugin_manifests(), and `biffo plugin install` all actually
73
+ # validate against — same reasoning cli/src/lib/plugin-manifest.ts
74
+ # documents for why the CLI validates this way rather than against
75
+ # registry-schema.json directly (see the next step).
76
+ - name: Validate against the SDK's PluginManifest model (authoritative)
77
+ run: |
78
+ uv run python -c "
79
+ from biffo_plugin_sdk.plugin import load_manifest
80
+ manifest = load_manifest('biffo.plugin.json')
81
+ print(f'OK: {manifest.name}@{manifest.version} — '
82
+ f'{len(manifest.tables)} table(s), {len(manifest.api_routes)} route(s)')
83
+ "
84
+ # Advisory check, not a gate: registry-schema.json (vendored from
85
+ # _skeletons/registry/registry-schema.json in biffo-template) is the
86
+ # nominal marketplace registry schema, but its api_routes shape
87
+ # predates issue #19's table/operation CRUD-synthesis redesign — it
88
+ # still requires a free-form `handler` function-name field, and its
89
+ # `path` regex rejects the `{id}` path parameter every single-row
90
+ # route (read/update/delete) is required to have. A manifest that is
91
+ # valid per the real PluginManifest model above (checked and pinned in
92
+ # this repo's uv.lock) will therefore *fail* strict validation here —
93
+ # confirmed directly against this template's own example manifest.
94
+ # Run non-blocking until the registry schema is corrected upstream;
95
+ # see README.md's "Manifest validation" section for the full story.
96
+ - name: Validate against the vendored registry schema (advisory, non-blocking)
97
+ continue-on-error: true
98
+ run: |
99
+ uv run python -m jsonschema -i biffo.plugin.json registry-schema.json
100
+
101
+ security-secrets:
102
+ name: Secret Scan
103
+ runs-on: ubuntu-latest
104
+ permissions:
105
+ contents: read
106
+ pull-requests: read
107
+ steps:
108
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
109
+ with:
110
+ fetch-depth: 0
111
+ - uses: gitleaks/gitleaks-action@v2
112
+ env:
113
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
114
+
115
+ security-deps:
116
+ name: Dependency Audit
117
+ runs-on: ubuntu-latest
118
+ steps:
119
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
120
+ - uses: astral-sh/setup-uv@v5
121
+ with:
122
+ enable-cache: true
123
+ - run: uv sync --all-groups
124
+ - run: uv run pip-audit
@@ -0,0 +1,114 @@
1
+ name: Release
2
+
3
+ # Runs on every push of a `v*` tag: bumps/verifies the version via
4
+ # python-semantic-release, builds the sdist/wheel, and publishes to PyPI.
5
+ #
6
+ # ASPIRATIONAL PYPI PUBLISH: this repo (and every plugin repo forked from
7
+ # this template) has no real PyPI project or publishing credential yet, so
8
+ # the publish job below runs and fails at the
9
+ # `pypa/gh-action-pypi-publish` step — that's expected, not a bug in this
10
+ # template. Configure a publisher for *your* plugin's own PyPI project and
11
+ # it starts publishing on the next tag push with no further changes. See
12
+ # README.md's "PyPI publishing" section.
13
+ #
14
+ # Note this is about *this plugin's* own distribution. The SDK it depends
15
+ # on, biffo-plugin-sdk, has its own release path upstream —
16
+ # biffo-template's .github/workflows/publish-sdk.yml, triggered by an
17
+ # `sdk-v*` tag — and its own independent semver. See README.md's
18
+ # "biffo-plugin-sdk dependency" section for what that means for `uv sync`
19
+ # before the SDK's first release lands.
20
+ #
21
+ # Prefer PyPI Trusted Publishing (OIDC) over a long-lived PYPI_API_TOKEN
22
+ # when you set this up: it needs no stored secret to rotate or leak. The
23
+ # `id-token: write` permission below is already in place for it, and
24
+ # publish-sdk.yml upstream is a worked example.
25
+ #
26
+ # biffo-template does not use semantic-release anywhere else in the
27
+ # monorepo (its own root has no release.yml — checked before writing this;
28
+ # versioning there is manual) so there's no existing in-repo pattern to
29
+ # follow. python-semantic-release is used here as the standard tool for a
30
+ # single, independently-versioned Python package driven by Conventional
31
+ # Commits (which this monorepo's own commitlint config already enforces on
32
+ # every commit message, including in this plugin repo once forked).
33
+
34
+ on:
35
+ push:
36
+ tags: ['v*']
37
+
38
+ permissions:
39
+ contents: write
40
+ id-token: write # required for PyPI trusted publishing
41
+
42
+ env:
43
+ PYTHON_VERSION: '3.13'
44
+
45
+ jobs:
46
+ release:
47
+ name: Build, Version & Publish
48
+ runs-on: ubuntu-latest
49
+ environment:
50
+ name: pypi
51
+ url: https://pypi.org/project/biffo-plugin-example/
52
+ steps:
53
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
54
+ with:
55
+ fetch-depth: 0
56
+ token: ${{ secrets.GITHUB_TOKEN }}
57
+
58
+ - uses: astral-sh/setup-uv@v5
59
+ with:
60
+ enable-cache: true
61
+
62
+ - run: uv sync --all-groups
63
+
64
+ # Full CI gate re-run on the tag, not just a build — a tag push must
65
+ # be at least as strict as a merged PR before anything is published.
66
+ - run: uv run ruff check .
67
+ - run: uv run ruff format --check .
68
+ - run: uv run pyright
69
+ - run: uv run pytest --cov
70
+
71
+ - name: Determine version from tag
72
+ id: version
73
+ run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
74
+
75
+ - name: Verify pyproject.toml version matches tag
76
+ run: |
77
+ project_version=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
78
+ if [ "$project_version" != "${{ steps.version.outputs.version }}" ]; then
79
+ echo "::error::pyproject.toml version ($project_version) does not match tag ${GITHUB_REF_NAME}"
80
+ exit 1
81
+ fi
82
+
83
+ - name: Install python-semantic-release
84
+ run: uv tool install python-semantic-release
85
+
86
+ - name: Generate release notes
87
+ run: semantic-release changelog
88
+
89
+ - name: Build distribution
90
+ run: uv build
91
+
92
+ # PYPI PUBLISH — see the file-level comment above. This step is real
93
+ # and will run on every `v*` tag push, but has no credentials
94
+ # configured yet anywhere in the plugin ecosystem (this template, the
95
+ # SDK, or the RBAC reference plugin) and is expected to fail here
96
+ # until a PyPI project + trusted publisher (or PYPI_API_TOKEN) is set
97
+ # up for this specific plugin package.
98
+ - name: Publish to PyPI
99
+ uses: pypa/gh-action-pypi-publish@release/v1
100
+ with:
101
+ # Prefer PyPI's trusted publishing (OIDC, via the `id-token: write`
102
+ # permission above) over a long-lived API token where possible —
103
+ # configure a trusted publisher for this repo at
104
+ # https://pypi.org/manage/project/<name>/settings/publishing/
105
+ # once the project exists. Fall back to `password:
106
+ # ${{ secrets.PYPI_API_TOKEN }}` if trusted publishing isn't set
107
+ # up for this project yet.
108
+ skip-existing: true
109
+
110
+ - name: Create GitHub Release
111
+ uses: softprops/action-gh-release@v2
112
+ with:
113
+ generate_release_notes: true
114
+ files: dist/*
@@ -0,0 +1,232 @@
1
+ # Biffo Plugin Repository Template
2
+
3
+ > **This is a skeleton, not a live repository.** It lives at
4
+ > `_skeletons/plugin-template/` inside the `biffo-template` monorepo — the
5
+ > same pattern `_skeletons/registry/` already uses for the plugin registry
6
+ > (see that directory's `README.md`). Nothing in here runs as part of
7
+ > biffo-template's own CI/CD, and this directory is **not** a member of the
8
+ > monorepo's pnpm or uv workspace (check `pnpm-workspace.yaml` and the root
9
+ > `pyproject.toml`'s `[tool.uv.workspace]` — neither lists `_skeletons/`).
10
+ > It exists to be **copied into a brand-new GitHub repository** when someone
11
+ > creates a Biffo plugin, per [ADR-0003](../../docs/ADR/0003-plugin-system-and-marketplace.md)
12
+ > section 2 ("Plugin Repository Structure"). No such repository
13
+ > (`keiranholloway/biffo-plugin-template` or otherwise) has actually been
14
+ > created — that was an explicit decision for this issue (#26): build the
15
+ > skeleton here, don't stand up a real external repo, so it can be reviewed
16
+ > and iterated on like any other code change in this monorepo.
17
+ >
18
+ > To use it: copy this entire directory's contents (minus this note) into a
19
+ > new repository, rename `example_plugin` throughout to your plugin's name,
20
+ > replace `biffo.plugin.json` with your own manifest, and follow the setup
21
+ > steps below.
22
+
23
+ ## What's here
24
+
25
+ ```
26
+ plugin-template/
27
+ ├── .github/workflows/
28
+ │ ├── ci.yml # lint, typecheck, test, manifest validation, security scans — runs on PR
29
+ │ └── release.yml # semantic-release + PyPI publish — runs on `v*` tag push
30
+ ├── biffo.plugin.json # example manifest: one table, four generic-CRUD routes
31
+ ├── registry-schema.json # vendored copy of the registry's manifest JSON Schema (see below)
32
+ ├── pyproject.toml # depends on biffo-plugin-sdk
33
+ ├── terraform/ # the plugin's own infra — Lambda, EventBridge subscription, Core API IAM
34
+ │ ├── main.tf
35
+ │ ├── variables.tf
36
+ │ ├── outputs.tf
37
+ │ └── README.md
38
+ ├── src/example_plugin/
39
+ │ ├── __init__.py
40
+ │ ├── manifest.py # path to biffo.plugin.json
41
+ │ ├── plugin.py # ExamplePlugin(BiffoPluginBase) — the reference implementation
42
+ │ └── main.py # Lambda entrypoint dispatching EventBridge events
43
+ └── tests/
44
+ ├── conftest.py
45
+ ├── fakes.py # in-memory fake of the Core API's generic CRUD routes
46
+ └── test_example_plugin.py
47
+ ```
48
+
49
+ This is the canonical plugin shape: the `BiffoPluginBase` subclass, the
50
+ `src/<name>/{plugin.py, main.py}` layout, the manifest, the `terraform/`
51
+ module, and the fake-Core-API test pattern.
52
+
53
+ The working first-party example to read alongside it is the **orchestration
54
+ engine** (`services/_plugins/orchestrator/` in the biffo-template monorepo) — same base
55
+ class, same layout, and it ships a real `terraform/` too. It has considerably
56
+ more moving parts (workflow definitions, actions, SigV4 calls into the Core
57
+ API); this template's `example_plugin` is deliberately the smallest version of
58
+ the same shape: one table (`example_widgets`), four routes, one `on_install`
59
+ seed, one `@subscribe` handler.
60
+
61
+ > Earlier revisions of this README pointed at an RBAC reference plugin at
62
+ > `services/rbac/`. That plugin was removed by
63
+ > [ADR-0011](https://github.com/keiranholloway/biffo-template/blob/main/docs/ADR/0011-authorization-is-a-core-concern.md)
64
+ > (authorization is a Core concern, not a plugin) and no longer exists.
65
+
66
+ ## Standalone repo, not a monorepo package
67
+
68
+ Unlike biffo-template's own in-monorepo plugins (e.g. `services/_plugins/orchestrator/`,
69
+ a uv workspace member that resolves `biffo-plugin-sdk` from the workspace and
70
+ so needs no `[build-system]` of its own), this template's `pyproject.toml` **does** declare
71
+ `[build-system]` (hatchling) because it's meant to live in its own
72
+ repository with its own independent `uv sync` / `uv.lock`, not inside
73
+ biffo-template's workspace. Once copied out, `uv sync && uv run pytest`
74
+ works standalone with no dependency on the rest of biffo-template.
75
+
76
+ ## The `biffo-plugin-sdk` dependency: PyPI pin, pending the first release
77
+
78
+ `pyproject.toml` declares:
79
+
80
+ ```toml
81
+ dependencies = [
82
+ "biffo-plugin-sdk>=1.0,<2.0",
83
+ ...
84
+ ]
85
+ ```
86
+
87
+ This is a **PyPI-style version pin**, and it is the correct end state: the
88
+ SDK is versioned `1.0.0` and biffo-template's
89
+ [`.github/workflows/publish-sdk.yml`](https://github.com/keiranholloway/biffo-template/blob/main/.github/workflows/publish-sdk.yml)
90
+ builds and publishes it to PyPI (via Trusted Publishing) on a pushed
91
+ `sdk-v*` tag. `>=1.0,<2.0` matches the `"biffo-plugin-sdk": "^1.0"` that
92
+ `biffo.plugin.json` declares, and the SDK carries its own independent
93
+ semver — it is **not** tied to the template's `core.version`, so a major
94
+ bump here means the plugin API broke and nothing else.
95
+
96
+ **Ordering caveat.** The release _pipeline_ exists; the _release_ does not
97
+ yet. `biffo-plugin-sdk` has never been uploaded — the PyPI project is
98
+ unregistered until the owner configures the Trusted Publisher and pushes
99
+ `sdk-v1.0.0`. Until that happens, `uv sync` in a freshly-copied plugin repo
100
+ still cannot resolve this dependency, and you need one of the two local
101
+ overrides below. Once 1.0.0 is live, **delete the override** — the
102
+ `dependencies` entry above already points at the real thing.
103
+
104
+ Two ways to make local development work before that happens:
105
+
106
+ 1. **Path dependency** (if developing inside a biffo-template checkout,
107
+ e.g. for a plugin you plan to upstream into `services/`): add
108
+ ```toml
109
+ [tool.uv.sources]
110
+ biffo-plugin-sdk = { path = "../../packages/python-sdk", editable = true }
111
+ ```
112
+ 2. **Git dependency** (developing this plugin as a genuinely separate repo
113
+ against an unpublished SDK):
114
+ ```toml
115
+ [tool.uv.sources]
116
+ biffo-plugin-sdk = { git = "https://github.com/keiranholloway/biffo-template", subdirectory = "packages/python-sdk" }
117
+ ```
118
+
119
+ Either override goes in `[tool.uv.sources]` only — the PyPI-style
120
+ `dependencies` entry above stays as-is, so removing the override is the
121
+ only change needed once the SDK actually ships to PyPI.
122
+
123
+ ## Manifest validation: why CI doesn't hard-gate on `registry-schema.json`
124
+
125
+ `ci.yml`'s `validate-manifest` job runs two checks:
126
+
127
+ 1. **Authoritative, blocking**: `biffo_plugin_sdk.plugin.load_manifest()` —
128
+ the real Pydantic model the SDK, the Core API's
129
+ `discover_plugin_manifests()`, and `biffo plugin install` all actually
130
+ validate a manifest against.
131
+ 2. **Advisory, non-blocking** (`continue-on-error: true`): the vendored
132
+ `registry-schema.json` (a copy of `_skeletons/registry/registry-schema.json`
133
+ as of when this template was written), via
134
+ `python -m jsonschema -i biffo.plugin.json registry-schema.json`.
135
+
136
+ These two checks currently **disagree**, and check 2 is expected to fail.
137
+ `registry-schema.json`'s `api_routes` shape predates issue #19's
138
+ declarative table/operation CRUD-synthesis redesign: it still requires a
139
+ free-form `handler` function-name field (this template's manifest has no
140
+ such field — routes are declared as `table`/`operation` pairs instead, per
141
+ the real `RouteDef`/`RouteDefinition` models), and its `path` regex
142
+ (`^/[a-z0-9/_-]+$`) rejects the literal `{`/`}` characters that every
143
+ single-row route (`read`/`update`/`delete`) is required to have in its path
144
+ (e.g. `/widgets/{id}`). This was verified directly, not assumed: running
145
+ `jsonschema` against this template's own example manifest produces six
146
+ errors, all in `api_routes`. `cli/src/lib/plugin-manifest.ts` in
147
+ biffo-template already documents this exact same finding and deliberately
148
+ validates against the real Pydantic/Zod models instead of
149
+ `registry-schema.json` for the same reason.
150
+
151
+ This template follows that precedent: the manifest you ship must pass
152
+ check 1 (it's what actually gets enforced at plugin-install and db-init
153
+ time); check 2 is kept for visibility, so a future correction of
154
+ `registry-schema.json` upstream is easy to notice (it'll start passing) but
155
+ never blocks your CI in the meantime. If `registry-schema.json` is
156
+ corrected, remove this note and consider making check 2 blocking too.
157
+
158
+ ## Contribution guidelines
159
+
160
+ - **Conventional Commits.** `feat: ...`, `fix: ...`, `chore: ...`,
161
+ `docs: ...`, `test: ...`, `refactor: ...`, `perf: ...` — the same types
162
+ biffo-template's own root `CLAUDE.md` documents. `release.yml`'s
163
+ changelog generation depends on this.
164
+ - **One plugin, one repo.** Don't vendor unrelated code here — this repo
165
+ should contain exactly the plugin's manifest, source, tests, and
166
+ `terraform/`. The plugin's infrastructure lives in `terraform/` at the repo
167
+ root (ADR-0003 section 2), and **this template ships one** — see
168
+ [`terraform/README.md`](terraform/). The example plugin does need it: it
169
+ declares an `event_subscriptions` entry, so without a Lambda and an
170
+ EventBridge rule that subscription never fires. `biffo plugin install`
171
+ copies `terraform/` into the user's monorepo at `modules/plugins/<name>/`,
172
+ and it does so **silently only if the directory exists** — a plugin that
173
+ deletes it gets a clean install and dead event handlers. A CI guard in
174
+ biffo-template (`pnpm --filter @biffo/cli check:plugin-terraform`) fails any
175
+ manifest that declares `event_subscriptions` without a `terraform/`
176
+ directory; keep both in step.
177
+ - **Every table gets `tenant_id`.** You never declare it yourself — it's
178
+ auto-injected (ADR-0001). Declaring `id`, `tenant_id`, `created_at`, or
179
+ `updated_at` in a table's `columns` fails manifest validation.
180
+ - **No database client.** Plugins talk to the Core API over HTTP via the SDK's
181
+ Core client only (ADR-0002) — never `psycopg2`/`asyncpg`/SQLAlchemy against
182
+ the database directly. `BiffoPluginBase.api` is a SigV4-signing
183
+ `SignedCoreClient` by default (ADR-0009); `terraform/` grants the Lambda role
184
+ `execute-api:Invoke` on `/api/v1/internal/*`, and you must add that role to
185
+ the Core API's `BIFFO_SERVICE_PRINCIPAL_ARN_ALLOWLIST` — see
186
+ [`terraform/README.md`](terraform/).
187
+ - **Run the full check suite before opening a PR:**
188
+ ```bash
189
+ uv sync --all-groups
190
+ uv run ruff check .
191
+ uv run ruff format --check .
192
+ uv run pyright
193
+ uv run pytest --cov
194
+ ```
195
+
196
+ ## Branch protection setup
197
+
198
+ Once you've created the real repository from this skeleton, protect `main`
199
+ with the **same settings biffo-template's own root repo uses**, codified as
200
+ Terraform in `modules/source-control/github/main.tf`'s
201
+ `github_branch_protection.main` resource in this monorepo — copy that
202
+ module (or these settings) rather than inventing new ones:
203
+
204
+ | Setting | Value |
205
+ | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
206
+ | Required status checks | `strict = true`; contexts: `CI / Lint`, `CI / Type Check`, `CI / Test`, `CI / Validate biffo.plugin.json`, `CI / Secret Scan` (the job names from this repo's own `ci.yml`) |
207
+ | Required approving reviews | 1 |
208
+ | Dismiss stale reviews on new commits | Yes |
209
+ | Require review from Code Owners | Yes (add a `.github/CODEOWNERS` file) |
210
+ | Enforce for admins | Yes |
211
+ | Require linear history | Yes |
212
+ | Allow force pushes | No |
213
+ | Allow branch deletion | No |
214
+
215
+ If you manage the new repo with Terraform too (recommended — see this
216
+ monorepo's own "IaC as first-class citizen" convention), use
217
+ `modules/source-control/github/` directly rather than hand-writing the
218
+ equivalent `github_branch_protection` resource. If you're setting this up
219
+ by hand via the GitHub UI instead: **Settings → Branches → Add branch
220
+ protection rule**, pattern `main`, and set each field above to match.
221
+
222
+ ## PyPI publishing
223
+
224
+ `release.yml` triggers on `v*` tag pushes, re-runs the full CI gate against
225
+ the tagged commit, verifies the tag matches `pyproject.toml`'s
226
+ `project.version`, builds the sdist/wheel, and publishes to PyPI via
227
+ `pypa/gh-action-pypi-publish`. **This publish step will not succeed until a
228
+ real PyPI project exists for this plugin and either a trusted publisher (OIDC)
229
+ or a `PYPI_API_TOKEN` secret is configured** — see `release.yml`'s header
230
+ comment for the full rationale (this is intentional, per issue #26's
231
+ decision to build the release workflow as if publishing were real even
232
+ though no plugin in the ecosystem is on PyPI yet).
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "example-plugin",
3
+ "version": "0.1.0",
4
+ "description": "Reference example plugin shipped with the plugin repository template — a single table with generic list/create/read/delete routes. Replace this manifest with your own plugin's tables and routes.",
5
+ "author": "Biffo Team",
6
+ "tags": ["example"],
7
+ "required_core_version": ">=0.0.0",
8
+ "tables": [
9
+ {
10
+ "name": "example_widgets",
11
+ "columns": [
12
+ {
13
+ "name": "name",
14
+ "type": "String(100)",
15
+ "nullable": false,
16
+ "description": "Widget name."
17
+ },
18
+ {
19
+ "name": "description",
20
+ "type": "Text",
21
+ "nullable": true
22
+ },
23
+ {
24
+ "name": "is_active",
25
+ "type": "Boolean",
26
+ "nullable": true,
27
+ "description": "Nullable because declared column defaults are not applied by the generated migration DDL (only the in-process SQLAlchemy model) — see README's 'Known limitations' section. Treat NULL as false; this plugin's own on_install() always sets it explicitly."
28
+ }
29
+ ],
30
+ "indexes": [
31
+ {
32
+ "name": "idx_example_widgets_tenant_name",
33
+ "columns": ["tenant_id", "name"],
34
+ "unique": true
35
+ }
36
+ ],
37
+ "permissions": {
38
+ "list": { "allowed": true },
39
+ "read": { "allowed": true },
40
+ "create": { "allowed": true, "required_role": ["example-plugin.admin"] },
41
+ "delete": { "allowed": true, "required_role": ["example-plugin.admin"] }
42
+ }
43
+ }
44
+ ],
45
+ "api_routes": [
46
+ {
47
+ "method": "GET",
48
+ "path": "/widgets",
49
+ "table": "example_widgets",
50
+ "operation": "list",
51
+ "description": "List widgets for the caller's tenant."
52
+ },
53
+ {
54
+ "method": "POST",
55
+ "path": "/widgets",
56
+ "table": "example_widgets",
57
+ "operation": "create",
58
+ "description": "Create a widget."
59
+ },
60
+ {
61
+ "method": "GET",
62
+ "path": "/widgets/{id}",
63
+ "table": "example_widgets",
64
+ "operation": "read",
65
+ "description": "Get a single widget."
66
+ },
67
+ {
68
+ "method": "DELETE",
69
+ "path": "/widgets/{id}",
70
+ "table": "example_widgets",
71
+ "operation": "delete",
72
+ "description": "Delete a widget."
73
+ }
74
+ ],
75
+ "event_subscriptions": [
76
+ {
77
+ "source": "biffo.core",
78
+ "detail_type": "UserCreated",
79
+ "description": "Example subscription: logs newly created users. Replace with real logic, or remove this block if your plugin doesn't need it."
80
+ }
81
+ ],
82
+ "ui_components": [
83
+ { "type": "nav-link", "label": "Example Plugin", "path": "/admin/example-plugin" },
84
+ { "type": "page", "label": "Widgets", "path": "/admin/example-plugin/widgets" }
85
+ ],
86
+ "dependencies": {
87
+ "biffo-plugin-sdk": "^1.0"
88
+ }
89
+ }
@@ -0,0 +1,100 @@
1
+ [project]
2
+ name = "biffo-plugin-example"
3
+ version = "0.1.0"
4
+ description = "Example Biffo plugin — replace this with your own plugin's name and description"
5
+ requires-python = ">=3.13"
6
+ dependencies = [
7
+ # biffo-plugin-sdk has a real release path as of biffo-template's
8
+ # .github/workflows/publish-sdk.yml: the package is versioned 1.0.0 and
9
+ # publishes to PyPI (via Trusted Publishing) when an `sdk-v*` tag is
10
+ # pushed. This pin is therefore the *correct end state* and is left as-is.
11
+ #
12
+ # ORDERING: the pin only resolves once the owner has actually pushed
13
+ # `sdk-v1.0.0` and PyPI has the release. Until that first publish,
14
+ # `uv sync` in a freshly-copied plugin repo cannot resolve it — the same
15
+ # as before this comment was written, so nothing regressed here; what
16
+ # changed is that the fix is now one tag push away rather than unbuilt.
17
+ # Deliberately NOT worked around with a git/path source in the committed
18
+ # file: baking one in would mean every plugin repo forked from this
19
+ # skeleton silently keeps building against a moving git ref long after
20
+ # the real release exists. Use a local `[tool.uv.sources]` override until
21
+ # then — see the README's "biffo-plugin-sdk dependency" section, which
22
+ # documents both options and says which to delete once 1.0.0 is live.
23
+ "biffo-plugin-sdk>=1.0,<2.0",
24
+ "aws-lambda-powertools[tracer]>=3.4.0",
25
+ "httpx>=0.28.1",
26
+ ]
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "pytest>=8.3.4",
31
+ "pytest-asyncio>=0.25.2",
32
+ "pytest-cov>=6.0.0",
33
+ "ruff>=0.8.0",
34
+ "pyright>=1.1.0",
35
+ "jsonschema>=4.23.0",
36
+ "pip-audit>=2.7.3",
37
+ ]
38
+
39
+ [build-system]
40
+ requires = ["hatchling"]
41
+ build-backend = "hatchling.build"
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ # Explicit rather than relying on hatchling's default "package name matches
45
+ # a src/ directory" heuristic — the project name (`biffo-plugin-example`)
46
+ # is a distribution/PyPI name, while `example_plugin` is the importable
47
+ # package name; they don't have to match, and won't once you rename this
48
+ # template's package to your own plugin's name. Update this path when you do.
49
+ packages = ["src/example_plugin"]
50
+
51
+ [tool.ruff]
52
+ target-version = "py313"
53
+ line-length = 100
54
+
55
+ [tool.ruff.lint]
56
+ select = ["E", "F", "I", "N", "W", "B", "S", "UP", "ANN"]
57
+ ignore = [
58
+ "S101", # allow assert in tests
59
+ ]
60
+
61
+ [tool.ruff.lint.per-file-ignores]
62
+ # S101: assert is the normal pytest idiom. ANN: test functions don't need
63
+ # return-type annotations. S106: fakes.py's mock BiffoAPIClient(token=...)
64
+ # is a fixed test fixture value, not a real credential.
65
+ "tests/**/*.py" = ["S101", "ANN", "S106"]
66
+
67
+ [tool.ruff.format]
68
+ quote-style = "double"
69
+ indent-style = "space"
70
+
71
+ [tool.pyright]
72
+ pythonVersion = "3.13"
73
+ typeCheckingMode = "standard"
74
+ venvPath = "."
75
+ venv = ".venv"
76
+
77
+ [tool.pytest.ini_options]
78
+ testpaths = ["tests"]
79
+ python_files = ["test_*.py", "*_test.py"]
80
+ python_classes = ["Test*"]
81
+ python_functions = ["test_*"]
82
+ asyncio_mode = "auto"
83
+
84
+ [tool.coverage.run]
85
+ source = ["src"]
86
+ omit = ["*/tests/*"]
87
+
88
+ # python-semantic-release config for release.yml. This repo does not use
89
+ # semantic-release to *create* tags (unlike its most common setup) — it's
90
+ # triggered by an already-pushed `v*` tag instead (see release.yml's header
91
+ # comment), so only `version_toml` (for the tag/pyproject.toml consistency
92
+ # check) and changelog generation are used; `semantic-release version`'s own
93
+ # commit/tag/push behavior is never invoked in CI.
94
+ [tool.semantic_release]
95
+ version_toml = ["pyproject.toml:project.version"]
96
+ branch = "main"
97
+ build_command = "uv build"
98
+
99
+ [tool.semantic_release.changelog]
100
+ changelog_file = "CHANGELOG.md"