diff --git a/.github/workflows/postgres-tests.yml b/.github/workflows/postgres-tests.yml index 9c36d23c5..b67a066eb 100644 --- a/.github/workflows/postgres-tests.yml +++ b/.github/workflows/postgres-tests.yml @@ -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: @@ -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" @@ -27,6 +30,7 @@ permissions: contents: read env: + NODE_VERSION: "24" UV_SYNC_FLAGS: --locked UV_CACHE_GLOB: | uv.lock @@ -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 @@ -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 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f7f9ddbb0..802538de6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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. @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -253,6 +269,7 @@ jobs: e2e-smoke: name: E2E smoke (Playwright) runs-on: ubuntu-latest + timeout-minutes: 30 services: redis: image: redis:7-alpine @@ -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. @@ -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 @@ -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 @@ -468,6 +488,7 @@ jobs: pr-checks: name: PR checks runs-on: ubuntu-latest + timeout-minutes: 15 needs: - python-lint - python-typecheck diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0870f6e56..cd398fdd7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 @@ -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: @@ -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: @@ -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: @@ -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 @@ -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). @@ -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: diff --git a/CHANGELOG.md b/CHANGELOG.md index a049f3fcd..db0fc4ac3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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=` and autogenerates against + `