Phase 5: Alembic baseline, per-site deploy, ADRs to docs/adr
Migration runner ready and a sister site can deploy from a clean
checkout with one .env file.
ADRs relocated (migrations/adr/ -> docs/adr/):
- migrations/ is now Alembic territory, not docs.
- All cross-references updated: CLAUDE.md, docs/PLUGIN-HOOKS.md,
docs/PLUGIN-QUICKSTART.md.
Alembic initialized (migrations/):
- env.py, script.py.mako, alembic.ini copied from Flask-Migrate
templates so `flask db migrate` and `flask db upgrade` work without
a one-time `flask db init` (which would clash with the existing
migrations/ directory).
- Baseline migration generated via autogenerate, captures all 47
tables (core models + 6 plugins) as the upgrade target. Ready for
per-site `flask db upgrade` from an empty schema.
Deploy artifacts:
- Dockerfile: python:3.12-slim base, gunicorn server, non-root user,
healthcheck against /api/auth/login. Single image bundles all six
plugins; sites enable via `flask plugin install <name>`.
- docker-compose.yml: MySQL 8 + API container, healthcheck-gated
startup, env-driven secrets that fail loud on missing values
(`${SECRET_KEY:?}` form).
- .env.example: full env-var inventory with comments. Calls out
required vs optional. Matches what ProductionConfig.validate
enforces.
docs/DEPLOY.md:
- Step-by-step per-site runbook: clone, configure .env, bring up
stack, run migrations, seed reference data, install plugins,
create admin, front with TLS, backups, updates.
- Common-issues table.
- Cross-links to ADR-004 (per-site rationale), ADR-003 (plugin
distribution), and the config source.
Skills:
- migrating-asset-schema: Alembic + one-shot data migration policy.
Rules: additive first, renames are three steps, destructive ops
need rollback, equipment migration filter per ADR-001 + ADR-005.
- hardening-flask-config: production validation, CORS allowlist
policy, JWT cookie hardening, per-site deploy isolation per ADR-004.
CLAUDE.md updated to reflect the post-Phase-5 state. No tests added
this commit; the Alembic baseline is exercised by the existing
db.create_all-based test suite (tests do not touch the migration
runner; that's by design until per-plugin migrations land).
Test count unchanged: 101 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
68
docs/adr/ADR-003-plugin-distribution.md
Normal file
68
docs/adr/ADR-003-plugin-distribution.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# ADR-003: Plugin distribution model
|
||||
|
||||
- **Status:** ACCEPTED
|
||||
- **Date:** 2026-05-08
|
||||
- **Deciders:** cproudlock
|
||||
|
||||
## Context
|
||||
|
||||
Sister sites adopting shopdb-flask need a way to:
|
||||
|
||||
1. Install the framework
|
||||
2. Pick which plugins they want
|
||||
3. Build their own plugins for site-specific equipment
|
||||
4. Receive updates to both framework and plugins
|
||||
|
||||
Today, every plugin lives in the `plugins/` directory of the framework repo. There is no separation between framework code and plugin code, no install / uninstall, and no way for a site to develop a plugin without forking the whole repo.
|
||||
|
||||
Three viable distribution models:
|
||||
|
||||
| Model | How a site installs a plugin |
|
||||
|---|---|
|
||||
| **In-tree only** | Fork the framework repo, add plugin under `plugins/`, run their own deploy. No separation. |
|
||||
| **Pip-installable plugins** | Each plugin published as a Python package. Site does `pip install shopdb-printers shopdb-network` etc. Discovery via Python entry points. Framework loads any installed plugin that registers itself. |
|
||||
| **Git-based plugins** | Each plugin lives in its own git repo. Site clones / submodules into `plugins/<name>/`. Loader picks them up from the directory. |
|
||||
|
||||
## Decision
|
||||
|
||||
**PROPOSED:** Use a **hybrid model** with two clearly-labeled paths.
|
||||
|
||||
1. **Bundled plugins**: a small set of plugins ships with the framework, in-tree at `plugins/`. These are the reference implementations and the default install (printers, computers, network, equipment, usb, notifications). A site that wants only what's bundled needs no extra work.
|
||||
2. **External plugins**: sister sites or third parties build plugins in their own git repos. The site running the framework drops the plugin into `plugins/<name>/` (clone, submodule, or symlink) and runs `flask plugin install <name>`. No pip packaging required for v1.
|
||||
|
||||
Pip-installable plugins (Python entry-point discovery) are deferred to v2. The complexity is not justified until at least two sites are running their own plugins.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- v1 is simple: filesystem-based discovery (already implemented in `shopdb/plugins/loader.py`), works for both bundled and external plugins.
|
||||
- Sites can develop plugins without changing the framework repo.
|
||||
- The `plugins/` directory is already the canonical location, so no architectural change is needed.
|
||||
|
||||
### Negative / cost
|
||||
|
||||
- No automatic update path for external plugins. Sites must `git pull` in each plugin directory manually. Acceptable for v1; revisit when plugin count grows.
|
||||
- Multiple plugin authors writing in parallel can collide on namespace (e.g., two plugins both registering an `AssetType` named "Equipment"). Need a naming policy: plugin names and asset-type names should be prefixed with the site or org if not in the bundled set.
|
||||
|
||||
### Neutral
|
||||
|
||||
- The existing in-tree pattern keeps working. This decision just formalizes it and clarifies the path for outside-the-tree plugins.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
1. **Pip-installable from day one.** Cleaner for the long term but adds packaging, entry-point registration, and CI steps. Premature for current scale (one site running, no sister-site plugins yet).
|
||||
2. **In-tree only forever.** Forces every site to fork. Doesn't scale beyond two or three sites.
|
||||
3. **Submodules only.** Forces git-submodule discipline on every adopting site. Submodules are notoriously fiddly. Rejected.
|
||||
|
||||
## Open questions
|
||||
|
||||
- For external plugins, should there be a manifest field (`source_url`) declaring where the plugin can be cloned from, so `flask plugin install` could pull it for the site? Defer; manual clone is fine for v1.
|
||||
- Naming convention for non-bundled plugin directory names: prefix with site? (`gea-wjsf-shipping`)? Adopt if and when we hit a name collision.
|
||||
|
||||
## References
|
||||
|
||||
- `shopdb/plugins/loader.py` (filesystem discovery)
|
||||
- `shopdb/plugins/cli.py` (plugin install / uninstall command)
|
||||
- ADR-001 (defines what plugins target)
|
||||
- ADR-002 (defines plugin version compatibility)
|
||||
Reference in New Issue
Block a user