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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/postgres-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ name: Postgres tests
# `@pytest.mark.postgres` tests that SQLite cannot run. Parallel under xdist,
# one database per worker (every test resets its schema), but still slower than
# SQLite, so it is not part of the PR aggregate: it runs nightly, on demand, and
# on PRs that touch the database/tenancy layers (#508).
# on PRs that touch database/tenancy or concurrency-sensitive modules (#508, #599).

on:
schedule:
Expand All @@ -16,6 +16,9 @@ on:
- "framework/db/**"
- "framework/testing/**"
- "modules/tenants/**"
- "modules/users/**"
- "modules/background_tasks/**"
- "modules/file_storage/**"
- "host/migrations/**"
- ".github/workflows/postgres-tests.yml"

Expand All @@ -27,6 +30,7 @@ permissions:
contents: read

env:
NODE_VERSION: "24"
UV_SYNC_FLAGS: --locked
UV_CACHE_GLOB: |
uv.lock
Expand All @@ -39,7 +43,7 @@ jobs:
python-tests-pg:
name: Python tests (Postgres)
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 30
services:
postgres:
image: postgres:16-alpine
Expand Down Expand Up @@ -72,4 +76,9 @@ jobs:
enable-cache: true
cache-dependency-glob: ${{ env.UV_CACHE_GLOB }}
- run: make install-py
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- run: npm ci
- run: make test-py-pg
27 changes: 24 additions & 3 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
python-lint:
name: Python lint & format
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v9.0.0
Expand All @@ -50,6 +51,8 @@ jobs:
# Package metadata, READMEs, release matrix, and the hand-maintained ty
# extra-paths / pytest testpaths lists (#520) must match what is on disk.
- run: make ci-check-metadata
- run: make ci-check-hardcoded-strings
- run: make ci-check-ci-parity
- run: make ci-check-locales
# Backticked repo paths in docs/, CLAUDE.md, README.md and skills must exist (#519).
- run: make ci-doc-paths
Expand All @@ -67,6 +70,7 @@ jobs:
docs-build:
name: Docs site build
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
Expand All @@ -79,6 +83,7 @@ jobs:
python-typecheck:
name: Python typecheck
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v9.0.0
Expand All @@ -91,6 +96,7 @@ jobs:
python-tests:
name: Python tests
runs-on: ubuntu-latest
timeout-minutes: 30
# The invalidation suite covers what a fake Redis client cannot — a real
# subscription, and two worker processes talking through one broker. The
# runner image does not ship redis-server, so the broker comes from a service
Expand All @@ -115,6 +121,11 @@ jobs:
enable-cache: true
cache-dependency-glob: ${{ env.UV_CACHE_GLOB }}
- run: make install-py
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- run: npm ci
# Tee'd so the next step can put the totals line (count + wall time) on
# the run's summary page: a suite creeping back toward the serial ~24 min
# of #524 should be visible without opening the log.
Expand All @@ -133,6 +144,7 @@ jobs:
js-tests:
name: JS tests (Vitest)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
Expand All @@ -145,6 +157,7 @@ jobs:
js-lint:
name: JS lint & format
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
Expand All @@ -160,6 +173,7 @@ jobs:
js-typecheck:
name: JS typecheck
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
Expand All @@ -182,6 +196,7 @@ jobs:
js-build:
name: JS build (Vite)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
Expand All @@ -199,9 +214,8 @@ jobs:
- run: make gen-pages
- run: make build
# These drive a real gen-pages + vite build to prove module-shipped CSS
# and cross-module npm-name imports actually resolve. They self-skip
# without node_modules, so the `Python tests` job (make install-py only)
# silently skips them — this is the one job that can really run them.
# and cross-module npm-name imports actually resolve. Missing build
# prerequisites fail on CI rather than silently skipping the guards.
- name: Module asset build guards
run: make ci-css-build

Expand All @@ -212,6 +226,7 @@ jobs:
migrations-roundtrip-pg:
name: Migrations round trip (Postgres)
runs-on: ubuntu-latest
timeout-minutes: 30
services:
postgres:
image: postgres:16-alpine
Expand Down Expand Up @@ -241,6 +256,7 @@ jobs:
doctor:
name: Module diagnostics (make doctor)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v9.0.0
Expand All @@ -253,6 +269,7 @@ jobs:
e2e-smoke:
name: E2E smoke (Playwright)
runs-on: ubuntu-latest
timeout-minutes: 30
services:
redis:
image: redis:7-alpine
Expand Down Expand Up @@ -358,6 +375,7 @@ jobs:
perf-guards:
name: Perf guards (Playwright)
runs-on: ubuntu-latest
timeout-minutes: 30
env:
# SQLite keeps the job self-contained — these guards measure asset
# delivery, which does not depend on row volumes.
Expand Down Expand Up @@ -437,6 +455,7 @@ jobs:
file-size-check:
name: File size (300-line cap)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v9.0.0
Expand All @@ -455,6 +474,7 @@ jobs:
package-build:
name: Package build (release dry-run)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v9.0.0
Expand All @@ -468,6 +488,7 @@ jobs:
pr-checks:
name: PR checks
runs-on: ubuntu-latest
timeout-minutes: 15
needs:
- python-lint
- python-typecheck
Expand Down
43 changes: 41 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ on:
description: "Explicit version override (e.g., 1.2.3)"
required: false
type: string
pending_publishers:
description: "Comma-separated simple_module_* names you confirm have PyPI pending publishers"
required: false
type: string
default: ""

permissions:
contents: write
Expand All @@ -27,6 +32,7 @@ jobs:
# framework/core/pyproject.toml by the chosen level.
resolve:
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
version: ${{ steps.compute.outputs.version }}
steps:
Expand Down Expand Up @@ -64,15 +70,35 @@ jobs:

# The full PR suite (the `pr-checks` aggregate) runs against the dispatched
# commit; `build` needs it, so nothing is built or published from a red commit.
# Reusable workflow calls cannot set timeout-minutes; pr.yml bounds each job.
checks:
uses: ./.github/workflows/pr.yml

# Public PyPI metadata cannot verify publisher configuration. Block absent
# projects unless the operator confirms their pending publishers.
preflight:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v9.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Check PyPI projects
env:
PENDING_PUBLISHERS: ${{ inputs.pending_publishers }}
run: >-
uv run --no-project --with tomlkit python -m scripts.release_preflight
--pending-publishers "$PENDING_PUBLISHERS"

# Bump versions in the working tree (no commit yet), build artifacts, upload.
# The commit + tag only hit origin in `finalize`, after publishes succeed,
# so a failed build never leaves an orphan tag to clean up.
build:
needs: [resolve, checks]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
Expand Down Expand Up @@ -112,8 +138,9 @@ jobs:
retention-days: 14

publish-pypi:
needs: build
needs: [build, preflight]
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
id-token: write # mint OIDC token for PyPI Trusted Publishing
strategy:
Expand Down Expand Up @@ -166,10 +193,12 @@ jobs:
- uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: to-publish
skip-existing: true

publish-npm:
needs: build
needs: [build, preflight]
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read # explicit so future cache:/checkout: steps don't 403
id-token: write # mint OIDC token for npm Trusted Publishing
Expand Down Expand Up @@ -207,6 +236,15 @@ jobs:
echo "::error::refusing to publish ${files[0]}: package name is '$name', expected '$expected'"
exit 1
fi
version=$(tar -xzOf "${files[0]}" package/package.json | python3 -c 'import json,sys; print(json.load(sys.stdin)["version"])')
status=$(curl --silent --show-error --connect-timeout 10 --max-time 30 \
--output /dev/null --write-out '%{http_code}' \
"https://registry.npmjs.org/@simple-module-py%2F${{ matrix.package }}/${version}")
case "$status" in
200) echo "${name}@${version} already exists; skipping"; exit 0 ;;
404) ;; # Only a missing exact version is safe to publish.
*) echo "::error::npm registry returned HTTP ${status} for ${name}@${version}"; exit 1 ;;
esac
# Prefix with ./ so npm treats the arg as a file path, not a
# GitHub `user/repo` shorthand (which it does for any bare
# single-slash arg, even when the tarball exists on disk).
Expand All @@ -216,6 +254,7 @@ jobs:
finalize:
needs: [resolve, publish-pypi, publish-npm]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ All notable changes to this project are documented in this file. The format is b
installed, otherwise run the durable handlers inline in `db`'s transaction (#583).

### Changed
- Packaging: **Behaviour change.** Published packages now cap their third-party runtime
dependencies (next major for 1.x+ libraries, next minor for 0.x ones such as `sqlmodel`), so
an install can no longer resolve an untested major release (#600).
- `make migration` now requires `LABEL=<branch label>` and autogenerates against
`<label>@head`, instead of failing with "Multiple heads" (#598).
- Release: a pre-flight job lists any package that is not yet on PyPI (needs a pending
publisher) before anything is published, and publish legs skip versions that already exist,
so a half-failed release can be re-run (#599).
- `make test-py` / `make test-py-pg` now default to 4 pytest-xdist workers instead of
`-n auto`, so concurrent runs on many-core machines don't exhaust memory;
`PYTEST_WORKERS=auto` restores the old behaviour.
Expand All @@ -35,6 +43,8 @@ All notable changes to this project are documented in this file. The format is b
tracked for removal in 0.1.0 (#594).

### Fixed
- Users: the default user role is created at startup when missing, so an app scaffolded with
`smpy new` (schema baseline, no data migrations) can register users (#600).
- Tenants: deleting a tenant now tears down its data. `TenantDeleted` (now defined in
`simple_module_core.tenant_events`, still importable from `tenants.contracts.events`) is
published durably, and file_storage (files, upload sessions, then backend objects after
Expand Down
27 changes: 15 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ Everything goes through `make`. Full list: `docs/reference/make-commands.md`.
| `make install` | `uv sync --all-packages` + `npm install` |
| `make dev` | Shared Docker stack up + `gen-pages` + API (:8000) and Vite (:5050) |
| `make test` / `make test-py` / `make test-js` | Pytest (SQLite, e2e/perf excluded; parallel via `pytest-xdist`, `-n $(PYTEST_WORKERS) --dist loadgroup`, default 4 workers, `PYTEST_WORKERS=0` for serial) then Vitest |
| `make test-py-pg` | Pytest on Postgres (`SM_TEST_PG_URL`, default db `sm_test`); parallel like `test-py`, each xdist worker creates its own `sm_test_gw<N>` database (role needs `CREATEDB`); not in `pr-checks` |
| `make test-py-pg` | Pytest on Postgres (`SM_TEST_PG_URL`, default db `sm_test`); parallel like `test-py` (default 4 workers), each xdist worker creates its own `sm_test_gw<N>` database (role needs `CREATEDB`); not in `pr-checks` |
| `make test-e2e` | Playwright smoke tests against a running `make dev` |
| `make lint` | Ruff format + Ruff + `ty` + Biome + per-workspace `tsc` + file-size cap + hardcoded-string check + untranslated-string check + package metadata/README checks |
| `make doctor` | Module diagnostics (`SM0xx`); a dev boot runs the same plus `SM012`/`SM025` |
| `make migrate` / `make downgrade` / `make migration-history` | Alembic `upgrade heads` / `-1` / history |
| `make migration msg="..."` | Autogenerate; **currently fails** ("Multiple heads") — use the recipe in § Database |
| `make migration LABEL=<branch_label> msg="..."` | Autogenerate on the selected branch (`--head <branch_label>@head`); `LABEL` is required |
| `make migrations-roundtrip-pg` | Postgres portability gate (upgrade → downgrade base → upgrade → `alembic check`); CI runs it |
| `make gen-pages` | Regenerate `host/client_app/modules.{manifest.json,generated.ts,generated.css}` (gitignored; needed before `tsc`/Vite) |
| `make gen-i18n` | Regenerate `packages/i18n/src/{keys.generated,generated-resources}.ts` (committed; CI diffs them) |
Expand Down Expand Up @@ -101,7 +101,8 @@ Run what CI runs, in this order; a red push costs a review cycle.
changes). Every feature/fix PR in recent history does this.

CI (`.github/workflows/pr.yml`, aggregate job `pr-checks`): python-lint (includes
`make ci-check-metadata` and the `make ci-i18n-fresh` diff and `make ci-check-no-double-cast`), python-typecheck,
`make ci-check-metadata`, `make ci-check-hardcoded-strings`, `make ci-check-ci-parity`,
the `make ci-i18n-fresh` diff and `make ci-check-no-double-cast`), python-typecheck,
python-tests (Redis service), js-tests, js-lint (`biome ci --error-on-warnings`,
plus the untranslated check), js-typecheck (plus `make ci-api-types-fresh`), js-build (`make ci-css-build`),
migrations-roundtrip-pg, doctor (`make ci-doctor`: migrate a throwaway SQLite DB,
Expand All @@ -110,9 +111,9 @@ calls the same `make ci-*` targets as `make lint`, with `UV_SYNC_FLAGS=--locked`
so a stale `uv.lock` fails (commit `uv.lock` whenever a `pyproject.toml` changes;
`make install` stays unlocked). `.github/workflows/postgres-tests.yml` runs
`make test-py-pg` nightly, on demand and on PRs touching `framework/db`,
`framework/testing`, `modules/tenants` or `host/migrations`; it is not in
`pr-checks`. **Local-only checks** (in `make lint`, not in CI, reviewers still
reject on them): hardcoded strings. `make ci-css-build` is in CI but not in
`framework/testing`, `modules/{tenants,users,background_tasks,file_storage}` or
`host/migrations`; it is not in
`pr-checks`. `make ci-css-build` is in CI but not in
`make lint` (slow, rewrites generated files). e2e-smoke and perf-guards enable a
hardcoded `SM_MODULES_ENABLED` JSON list; a new module is not exercised there
until added.
Expand Down Expand Up @@ -165,9 +166,9 @@ every Tailwind utility, so keep tokens in `theme.css` and rules in `styles.css`
**Lifecycle hooks** on `ModuleBase` (all no-op by default; order and examples in
`docs/framework/lifecycle.md`): `register_settings`, `register_menu_items`,
`register_permissions`, `register_feature_flags`, `register_event_handlers`,
`register_invalidations`, `register_health_checks`, `register_setup_steps`,
`register_invalidations`, `register_health_checks`,
`register_public_routes`, `register_body_limits`, `register_cors_policies`,
`register_rate_limits`,
`register_rate_limits`, `register_setup_steps`,
`register_csp_sources`,
`register_design_packs`,
`register_audit_links`, `register_resource_types`, `register_exception_handlers`, `register_middleware`,
Expand Down Expand Up @@ -493,8 +494,9 @@ app-level fixtures to every test directory; the root `conftest.py` is thin.

- `settings`: in-memory SQLite, `multi_tenant=True`, `auth_provider="users"`.
`SM_TEST_DATABASE_URL=postgresql+asyncpg://…` switches the whole suite to
Postgres with a per-test schema reset (`make test-py-pg` does this, with
`-p no:anyio` because an asyncpg connection cannot cross event loops).
Postgres with empty tables per test (schema rebuilt only when changed).
`make test-py-pg` runs 4 workers by default, each with its own database, and
uses `-p no:anyio` because an asyncpg connection cannot cross event loops.
- `db_state`, `engine`, `db_session`: fresh `DatabaseState` per test;
`db_session` creates all module tables and stamps `alembic_version` at heads.
- `app`: `create_app(settings)` with lifespan, seeds `admin@test`; `client` /
Expand Down Expand Up @@ -570,5 +572,6 @@ When conventions are unclear, read these instead of reverse-engineering code:
(entry points, migrations, API stability), `docs/framework/*.md` (lifecycle,
middleware, settings, events, i18n, tenancy), `docs/database/*.md`,
`docs/testing/*.md`, `docs/reference/*.md` (make commands, env vars,
diagnostic codes), `docs/plans/` (dated design docs; the newest is the intended
state).
diagnostic codes), `docs/superpowers/specs/` (canonical design docs) and
`docs/superpowers/plans/` (implementation plans). `docs/plans/` is historical;
follow status banners and current reference docs rather than dates alone.
Loading
Loading