@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.
- package/_skeletons/plugin-template/.github/workflows/ci.yml +124 -0
- package/_skeletons/plugin-template/.github/workflows/release.yml +114 -0
- package/_skeletons/plugin-template/README.md +232 -0
- package/_skeletons/plugin-template/biffo.plugin.json +89 -0
- package/_skeletons/plugin-template/pyproject.toml +100 -0
- package/_skeletons/plugin-template/registry-schema.json +294 -0
- package/_skeletons/plugin-template/src/__init__.py +0 -0
- package/_skeletons/plugin-template/src/example_plugin/__init__.py +10 -0
- package/_skeletons/plugin-template/src/example_plugin/main.py +63 -0
- package/_skeletons/plugin-template/src/example_plugin/manifest.py +30 -0
- package/_skeletons/plugin-template/src/example_plugin/plugin.py +92 -0
- package/_skeletons/plugin-template/terraform/README.md +146 -0
- package/_skeletons/plugin-template/terraform/main.tf +167 -0
- package/_skeletons/plugin-template/terraform/outputs.tf +33 -0
- package/_skeletons/plugin-template/terraform/variables.tf +133 -0
- package/_skeletons/plugin-template/tests/conftest.py +17 -0
- package/_skeletons/plugin-template/tests/fakes.py +74 -0
- package/_skeletons/plugin-template/tests/test_example_plugin.py +108 -0
- package/_skeletons/registry/README.md +27 -0
- package/_skeletons/registry/plugins.json +5 -0
- package/_skeletons/registry/registry-schema.json +294 -0
- package/_skeletons/sibling-template/.github/renovate.json +38 -0
- package/_skeletons/sibling-template/.github/workflows/ci.yml +185 -0
- package/_skeletons/sibling-template/.github/workflows/codeql.yml +56 -0
- package/_skeletons/sibling-template/.github/workflows/deploy.yml +219 -0
- package/_skeletons/sibling-template/.github/workflows/destroy-infra.yml +73 -0
- package/_skeletons/sibling-template/README.md +164 -0
- package/_skeletons/sibling-template/_gitignore +71 -0
- package/_skeletons/sibling-template/apps/frontend/.env.example +14 -0
- package/_skeletons/sibling-template/apps/frontend/eslint.config.mjs +10 -0
- package/_skeletons/sibling-template/apps/frontend/next.config.ts +20 -0
- package/_skeletons/sibling-template/apps/frontend/package.json +42 -0
- package/_skeletons/sibling-template/apps/frontend/pnpm-lock.yaml +5268 -0
- package/_skeletons/sibling-template/apps/frontend/pnpm-workspace.yaml +19 -0
- package/_skeletons/sibling-template/apps/frontend/src/app/globals.css +34 -0
- package/_skeletons/sibling-template/apps/frontend/src/app/layout.tsx +15 -0
- package/_skeletons/sibling-template/apps/frontend/src/app/page.test.tsx +75 -0
- package/_skeletons/sibling-template/apps/frontend/src/app/page.tsx +85 -0
- package/_skeletons/sibling-template/apps/frontend/src/lib/api-client.ts +58 -0
- package/_skeletons/sibling-template/apps/frontend/src/lib/auth.test.ts +135 -0
- package/_skeletons/sibling-template/apps/frontend/src/lib/auth.ts +87 -0
- package/_skeletons/sibling-template/apps/frontend/src/test-setup.ts +1 -0
- package/_skeletons/sibling-template/apps/frontend/tsconfig.json +29 -0
- package/_skeletons/sibling-template/apps/frontend/vitest.config.ts +18 -0
- package/_skeletons/sibling-template/biffo.sibling.json +7 -0
- package/_skeletons/sibling-template/infra/backend.tf +23 -0
- package/_skeletons/sibling-template/infra/main.tf +79 -0
- package/_skeletons/sibling-template/infra/outputs.tf +17 -0
- package/_skeletons/sibling-template/infra/variables.tf +63 -0
- package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/main.tf +114 -0
- package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/outputs.tf +12 -0
- package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/variables.tf +41 -0
- package/_skeletons/sibling-template/modules/cloud/aws/compute/main.tf +157 -0
- package/_skeletons/sibling-template/modules/cloud/aws/compute/outputs.tf +4 -0
- package/_skeletons/sibling-template/modules/cloud/aws/compute/variables.tf +70 -0
- package/_skeletons/sibling-template/modules/cloud/aws/storage/main.tf +78 -0
- package/_skeletons/sibling-template/modules/cloud/aws/storage/outputs.tf +4 -0
- package/_skeletons/sibling-template/modules/cloud/aws/storage/variables.tf +12 -0
- package/_skeletons/sibling-template/services/api/pyproject.toml +69 -0
- package/_skeletons/sibling-template/services/api/src/api/__init__.py +0 -0
- package/_skeletons/sibling-template/services/api/src/api/config.py +31 -0
- package/_skeletons/sibling-template/services/api/src/api/core_client.py +64 -0
- package/_skeletons/sibling-template/services/api/src/api/main.py +43 -0
- package/_skeletons/sibling-template/services/api/src/api/middleware/__init__.py +0 -0
- package/_skeletons/sibling-template/services/api/src/api/middleware/auth.py +111 -0
- package/_skeletons/sibling-template/services/api/src/api/routers/__init__.py +0 -0
- package/_skeletons/sibling-template/services/api/src/api/routers/whoami.py +21 -0
- package/_skeletons/sibling-template/services/api/tests/conftest.py +24 -0
- package/_skeletons/sibling-template/services/api/tests/test_whoami.py +21 -0
- package/_skeletons/sibling-template/services/api/uv.lock +1160 -0
- package/core.version +1 -1
- package/dist/index.js +113 -87
- 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"
|