feat: build Signage on its own from the published Ekklesia 0.2.0 packages
Signage no longer needs an Ekklesia checkout. A clean clone installs with `pnpm install --frozen-lockfile` and runs lint, typecheck, test, build, and validate by itself, taking Ekklesia's module-development packages from Ekklesia's public registry (ADRs 0053 and 0054). - Dependencies: every `workspace:*` becomes `^0.2.0`. @ekklesia/module-pack is added as a devDependency pinned exactly to 0.2.0, as ADR 0053 requires; it is not invoked yet. No @ekklesia/contracts dependency. drizzle-orm stays a direct dependency, satisfying the adapter's optional peer. - .npmrc points the @ekklesia scope at https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/ with no credential, because reading needs none. - pnpm-workspace.yaml holds this repository's pnpm settings only: - `allowBuilds: esbuild: true`, as the platform's standalone fixture does; - exact-version minimumReleaseAgeExclude entries for the 0.2.0 train, which pnpm 11 otherwise holds back on its release day; - the platform's narrowly scoped override for GHSA-67mh-4wv8-2f99 (esbuild via drizzle-kit), which Signage inherited while it built inside Ekklesia's workspace. - engines.node `>=24.11.0 <25` and packageManager `[email protected].0`, both from Ekklesia 0.2.0. Node 24.18.0 goes in .node-version for CI, as in Ekklesia. - biome.json carries the platform's Biome configuration, which `biome lint` used to pick up from the workspace root, so lint rules are unchanged. - pnpm-lock.yaml is Signage's own: registry tarballs with integrity hashes, and no workspace or file links. - `prepack` runs this package's own build instead of the workspace root's turbo. - `pnpm validate` (scripts/validate-module.mjs) checks the built package and its migrations with the published SDK parsers only: - the manifest, its identity, and its entrypoints; - that the built runtime's id, version, and database ABI match the manifest; - that the built schema metadata is plain, namespaced, and has module-internal foreign keys; - the SDK compatibility range; - that the migration journal and policy agree. Core's own `modules:validate` and `migrations:validate` are not published. What only an Ekklesia installation can check is named in the script. Removed: ci/assemble.sh, ci/platform.env, ci/pnpm-lock.yaml, ci/update-lock.sh. The lint, typecheck, test, and build jobs check out only this repository; build also runs `pnpm validate`. The integration job stays disabled. Its first step is now an explicit placeholder rather than the deleted assembly, and its explanation says it returns when Signage is a canonical linux-x64-glibc Module Package installed into Ekklesia's generic image. No source, behaviour, schema, or version change: Signage stays 0.1.2. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fefe34be03
commit
33bb146349
+53
-73
@@ -1,8 +1,9 @@
|
||||
name: CI
|
||||
|
||||
# Signage builds, tests, and runs only inside an Ekklesia workspace, so every job first assembles it
|
||||
# into the platform commit pinned in ci/platform.env (ci/assemble.sh) and installs that workspace
|
||||
# with --frozen-lockfile from ci/pnpm-lock.yaml.
|
||||
# Signage is a standalone repository: every job checks out only this repository, installs it with
|
||||
# --frozen-lockfile from its own pnpm-lock.yaml, and takes Ekklesia's module-development packages
|
||||
# (@ekklesia/*) from Ekklesia's public package registry (.npmrc; platform ADRs 0053 and 0054). No job
|
||||
# clones Ekklesia.
|
||||
#
|
||||
# The same trust rules as the platform's own CI (docs/development/security.md there):
|
||||
# * every action is pinned by a full commit SHA;
|
||||
@@ -11,10 +12,8 @@ name: CI
|
||||
# * there is no `pull_request_target`;
|
||||
# * the self-hosted runner executes trusted, reviewed code only.
|
||||
#
|
||||
# The server is Gitea 1.24, which ignores `permissions`, `concurrency`, and `timeout-minutes`
|
||||
# (https://docs.gitea.com/1.24/usage/actions/comparison). They are declared so the workflow is already
|
||||
# correct on a release that honours them; on 1.24 they bound nothing, and `permissions` does not
|
||||
# restrict the job token.
|
||||
# The server is Gitea 1.27, which honours `concurrency` cancellation; `permissions` and
|
||||
# `timeout-minutes` are declared so the workflow is correct wherever they are enforced.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -42,24 +41,21 @@ jobs:
|
||||
# in .git/config for a later step to read.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Assemble Signage into the pinned platform
|
||||
run: bash ci/assemble.sh
|
||||
|
||||
- name: Set up Node.js at the platform's version
|
||||
- name: Set up Node.js at the repository's version
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: build/workspace/.node-version
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Install the assembled workspace (frozen lockfile)
|
||||
working-directory: build/workspace
|
||||
# pnpm at exactly the `packageManager` version, and @ekklesia/* from the public registry named in
|
||||
# .npmrc: no token, and no Ekklesia checkout.
|
||||
- name: Install (frozen lockfile)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack install
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
working-directory: build/workspace
|
||||
run: pnpm --filter @ekklesia/signage lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Check that CI cleans up every stack it starts
|
||||
run: bash ci/check-cleanup.sh
|
||||
@@ -76,28 +72,21 @@ jobs:
|
||||
# in .git/config for a later step to read.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Assemble Signage into the pinned platform
|
||||
run: bash ci/assemble.sh
|
||||
|
||||
- name: Set up Node.js at the platform's version
|
||||
- name: Set up Node.js at the repository's version
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: build/workspace/.node-version
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Install the assembled workspace (frozen lockfile)
|
||||
working-directory: build/workspace
|
||||
# pnpm at exactly the `packageManager` version, and @ekklesia/* from the public registry named in
|
||||
# .npmrc: no token, and no Ekklesia checkout.
|
||||
- name: Install (frozen lockfile)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack install
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build the platform packages Signage depends on
|
||||
working-directory: build/workspace
|
||||
run: pnpm exec turbo run build --filter='@ekklesia/signage^...'
|
||||
|
||||
- name: Typecheck the server, browser, and browser-test projects
|
||||
working-directory: build/workspace
|
||||
run: pnpm --filter @ekklesia/signage typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
test:
|
||||
name: test
|
||||
@@ -111,28 +100,21 @@ jobs:
|
||||
# in .git/config for a later step to read.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Assemble Signage into the pinned platform
|
||||
run: bash ci/assemble.sh
|
||||
|
||||
- name: Set up Node.js at the platform's version
|
||||
- name: Set up Node.js at the repository's version
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: build/workspace/.node-version
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Install the assembled workspace (frozen lockfile)
|
||||
working-directory: build/workspace
|
||||
# pnpm at exactly the `packageManager` version, and @ekklesia/* from the public registry named in
|
||||
# .npmrc: no token, and no Ekklesia checkout.
|
||||
- name: Install (frozen lockfile)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack install
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build the platform packages Signage depends on
|
||||
working-directory: build/workspace
|
||||
run: pnpm exec turbo run build --filter='@ekklesia/signage^...'
|
||||
|
||||
- name: Unit, browser, and display contract tests
|
||||
working-directory: build/workspace
|
||||
run: pnpm --filter @ekklesia/signage test
|
||||
run: pnpm test
|
||||
|
||||
build:
|
||||
name: build
|
||||
@@ -146,48 +128,44 @@ jobs:
|
||||
# in .git/config for a later step to read.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Assemble Signage into the pinned platform
|
||||
run: bash ci/assemble.sh
|
||||
|
||||
- name: Set up Node.js at the platform's version
|
||||
- name: Set up Node.js at the repository's version
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: build/workspace/.node-version
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Install the assembled workspace (frozen lockfile)
|
||||
working-directory: build/workspace
|
||||
# pnpm at exactly the `packageManager` version, and @ekklesia/* from the public registry named in
|
||||
# .npmrc: no token, and no Ekklesia checkout.
|
||||
- name: Install (frozen lockfile)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack install
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# The same gates the platform's image build runs against exactly this module set: its boundary
|
||||
# rules (which apply to modules), the manifest, and the migrations.
|
||||
- name: Platform gates (boundaries, module manifest, migrations)
|
||||
working-directory: build/workspace
|
||||
run: |
|
||||
pnpm boundaries
|
||||
pnpm modules:validate
|
||||
pnpm migrations:validate
|
||||
- name: Build the server and both browser artifacts
|
||||
run: pnpm build
|
||||
|
||||
- name: Build Signage and the platform it depends on
|
||||
working-directory: build/workspace
|
||||
run: pnpm exec turbo run build --filter='@ekklesia/signage...'
|
||||
# The built package and its migrations, read the way the platform reads them, with only the
|
||||
# published SDK parsers (scripts/validate-module.mjs says what only Ekklesia itself can check).
|
||||
- name: Validate the built module and its migrations
|
||||
run: pnpm validate
|
||||
|
||||
# Signage inside Ekklesia, end to end, in a real browser: the platform's own Dockerfile builds the
|
||||
# deployment image with Signage in it, and the platform's split-topology deployment smoke runs
|
||||
# against it, pairing a kiosk display and running the scenarios in ci/display-scenarios. A
|
||||
# disposable database and object store with generated secrets; nothing is pushed or logged in to.
|
||||
#
|
||||
# DISABLED while Signage is pinned to Ekklesia 0.2.0. From that release on, the platform's image is
|
||||
# the one generic image (platform ADR 0048): deploy/Dockerfile keeps `modules/` out of its build
|
||||
# context and refuses one that has it, and the deployment smoke installs modules only as canonical
|
||||
# Module Packages (ADR 0049). Signage is not one yet: its static sharp dependency needs the native
|
||||
# linux-x64-glibc package flow, which is deliberately not part of the 0.2.0 compatibility change.
|
||||
# Run against 0.2.0, this job would build an image without Signage and drive the module template's
|
||||
# display instead, so it is switched off rather than left reporting on something else. The job and
|
||||
# ci/integration.sh stay as they are; the job returns once Signage is built as a Module Package and
|
||||
# installed into the generic image, and `if: false` is removed in that change.
|
||||
# DISABLED. From Ekklesia 0.2.0 on, the platform's image is the one generic image (platform ADR
|
||||
# 0048): deploy/Dockerfile keeps `modules/` out of its build context and refuses one that has it,
|
||||
# and the deployment smoke installs modules only as canonical Module Packages (ADR 0049). Signage is
|
||||
# not one yet: its static sharp dependency needs the native linux-x64-glibc package flow. Run as it
|
||||
# was, this job would build an image without Signage and drive the module template's display
|
||||
# instead, so it is switched off rather than left reporting on something else.
|
||||
#
|
||||
# It returns when Signage is built as a canonical linux-x64-glibc Module Package and installed into
|
||||
# Ekklesia's generic image; that change removes `if: false` and rewrites these steps. They still
|
||||
# expect the platform tree ci/assemble.sh used to build at build/workspace, and that assembly is
|
||||
# gone now that Signage builds on its own. The platform's deployment smoke and stack sweep will
|
||||
# still come from Ekklesia, at the release Signage targets, because they are the platform's to run.
|
||||
integration:
|
||||
name: integration
|
||||
if: false
|
||||
@@ -207,8 +185,10 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Assemble Signage into the pinned platform
|
||||
run: bash ci/assemble.sh
|
||||
- name: Fetch Ekklesia's deployment smoke and build Signage's Module Package
|
||||
run: |
|
||||
echo "integration: rewritten when Signage is a canonical linux-x64-glibc Module Package." >&2
|
||||
exit 1
|
||||
|
||||
# A job the runner killed (cancelled, or past the runner's own time limit) never ran its
|
||||
# cleanup. Anything a CI job left on this shared daemon more than three hours ago goes first.
|
||||
@@ -218,8 +198,8 @@ jobs:
|
||||
- name: Build the image and run the display scenarios
|
||||
run: bash ci/integration.sh
|
||||
|
||||
# Whatever happened above, this job's own stacks do not outlive it. If assembly failed, nothing
|
||||
# was started.
|
||||
# Whatever happened above, this job's own stacks do not outlive it. If the first step failed,
|
||||
# nothing was started.
|
||||
- name: Remove this job's CI stacks
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
dist/
|
||||
.turbo/
|
||||
coverage/
|
||||
*.log
|
||||
.DS_Store
|
||||
build/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
24.18.0
|
||||
@@ -0,0 +1,3 @@
|
||||
# Ekklesia's module-development packages (@ekklesia/*) come from Ekklesia's public package registry
|
||||
# (platform ADRs 0053 and 0054). Reading needs no account and no token; never put a credential here.
|
||||
@ekklesia:registry=https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/
|
||||
@@ -51,70 +51,71 @@ Mounting requires an authenticated module descriptor, and Core does not yet cach
|
||||
cold start depends on a Core-provided authorized offline display descriptor or lease; until that
|
||||
exists, this module does not claim it. See ADR 0006.
|
||||
|
||||
## Developing against Ekklesia
|
||||
## Developing Signage
|
||||
|
||||
Signage is developed **inside the authoritative Ekklesia workspace**, checked out at the path it will
|
||||
eventually be installed at:
|
||||
Signage builds and tests on its own. You need no Ekklesia checkout and no Ekklesia account:
|
||||
|
||||
- **Node.js** in the range `engines.node` declares (`>=24.11.0 <25`); `.node-version` names the
|
||||
version CI uses.
|
||||
- **pnpm** at the version `packageManager` pins, through Corepack (`corepack enable`).
|
||||
|
||||
```bash
|
||||
git clone <ekklesia> ekklesia
|
||||
git clone <this repository> ekklesia/modules/signage
|
||||
cd ekklesia
|
||||
pnpm install
|
||||
git clone <this repository> signage
|
||||
cd signage
|
||||
corepack enable
|
||||
pnpm install --frozen-lockfile
|
||||
```
|
||||
|
||||
`modules/*` is an Ekklesia workspace glob, so the checkout is picked up with no registry edit,
|
||||
no second copy of Ekklesia, and no vendored SDK. `@ekklesia/module-sdk`, `@ekklesia/module-database`,
|
||||
`@ekklesia/module-ui-sdk`, `@ekklesia/design-tokens`, `@ekklesia/ui`, and `@ekklesia/module-ui-build`
|
||||
all resolve through the workspace as `workspace:*` dependencies.
|
||||
Ekklesia's module-development packages (`@ekklesia/module-sdk`, `@ekklesia/module-database`,
|
||||
`@ekklesia/module-ui-sdk`, `@ekklesia/ui`, `@ekklesia/design-tokens`, `@ekklesia/module-ui-build`,
|
||||
`@ekklesia/module-pack`, `@ekklesia/typescript-config`) come from Ekklesia's public package registry.
|
||||
The committed `.npmrc` points the `@ekklesia` scope at it, and reading it needs **no token** and no
|
||||
login (platform ADRs 0053 and 0054); never add a credential to that file. Everything else comes from
|
||||
the public npm registry. `package.json` takes the Ekklesia packages as `^0.2.0` ranges, except
|
||||
`@ekklesia/module-pack`, which is pinned exactly; `pnpm-lock.yaml` pins every package's exact version
|
||||
and integrity, so always install with `--frozen-lockfile` unless you mean to change a dependency.
|
||||
|
||||
Two consequences worth knowing:
|
||||
`pnpm-workspace.yaml` holds this repository's own pnpm settings: it allows esbuild's install script,
|
||||
exempts exactly the Ekklesia package train in the lockfile from pnpm's minimum release age, and keeps
|
||||
one narrowly scoped advisory override. It does not make Signage part of any other workspace.
|
||||
|
||||
- Ekklesia uses a shared workspace lockfile, so installing here writes an importer entry for
|
||||
`modules/signage` into Ekklesia's `pnpm-lock.yaml`. That entry belongs to whoever maintains the
|
||||
Ekklesia checkout; it is not committed from this repository.
|
||||
- Signage requires Ekklesia 0.2: manifest version 8 and the Module Database ABI v1 (platform ADR
|
||||
0050), on top of the prerequisites for shared design tokens, module stylesheets, browser
|
||||
contributions, bounded streaming request bodies, and display offline code (ADRs 0039–0043). An
|
||||
older Ekklesia checkout cannot build or validate this module.
|
||||
- Signage reaches PostgreSQL only through the ABI v1 executor the platform hands it, with its own
|
||||
Drizzle tables and the module-side adapter from `@ekklesia/module-database/drizzle`. Core receives
|
||||
SQL text, bound values, and plain schema metadata, never a Drizzle object.
|
||||
Signage requires Ekklesia 0.2: manifest version 8 and the Module Database ABI v1 (platform ADR 0050).
|
||||
It reaches PostgreSQL only through the ABI v1 executor the platform hands it, with its own Drizzle
|
||||
tables and the module-side adapter from `@ekklesia/module-database/drizzle`. Core receives SQL text,
|
||||
bound values, and plain schema metadata, never a Drizzle object.
|
||||
|
||||
### Commands
|
||||
|
||||
Run from the **Ekklesia repository root**:
|
||||
|
||||
```bash
|
||||
pnpm --filter @ekklesia/signage build # tsc + the canonical module UI build
|
||||
pnpm --filter @ekklesia/signage test # unit and browser-artifact tests
|
||||
pnpm --filter @ekklesia/signage typecheck # node, browser, and browser-test projects
|
||||
pnpm --filter @ekklesia/signage lint
|
||||
|
||||
pnpm boundaries # four-layer boundary enforcement
|
||||
pnpm modules:validate # static manifest, entrypoint, and schema validation
|
||||
pnpm migrations:validate # migration journal, policy, ordering, and checksums
|
||||
pnpm verify # the full Docker-free gate
|
||||
pnpm lint # Biome, with this repository's biome.json
|
||||
pnpm typecheck # node, browser, and browser-test projects
|
||||
pnpm test # unit, browser-artifact, and display contract tests (vitest.config.ts)
|
||||
pnpm build # tsc, then the canonical module UI build of the primary and display artifacts
|
||||
pnpm validate # after build: the built package and its migrations, as the platform reads them
|
||||
```
|
||||
|
||||
Signage's tests are its own: `pnpm test` here runs `vitest` against this package's `vitest.config.ts`,
|
||||
which includes `.test.tsx` as well as `.test.ts`. The workspace-root config includes only `.test.ts`,
|
||||
so a browser test added here without that config would be silently skipped.
|
||||
CI runs exactly these, from a clean checkout.
|
||||
|
||||
Never run `pnpm migrations:apply` against a persistent database while developing this module. Signage
|
||||
reads no environment variable, constructs no database client, configures no object storage, hardcodes
|
||||
no Ekklesia URL, opens no socket of its own, and applies no migration: every one of those reaches it
|
||||
`pnpm validate` uses only the published SDK parsers. It cannot check what only an Ekklesia
|
||||
installation knows: compatibility with that installation, other installed modules' capabilities and
|
||||
dependencies, or a migration's checksum against a database's applied history. Ekklesia checks those
|
||||
when Signage is installed.
|
||||
|
||||
Never run a migration against a persistent database while developing this module. Signage reads no
|
||||
environment variable, constructs no database client, configures no object storage, hardcodes no
|
||||
Ekklesia URL, opens no socket of its own, and applies no migration: every one of those reaches it
|
||||
through the host.
|
||||
|
||||
### Changing the schema
|
||||
|
||||
```bash
|
||||
pnpm --filter @ekklesia/signage migration:generate # drizzle-kit, into drizzle/
|
||||
pnpm migration:generate # drizzle-kit, into drizzle/
|
||||
```
|
||||
|
||||
`tests/schema.test.ts` pins the checksum of the generated SQL, so a regenerated migration fails the
|
||||
suite until the pin is updated deliberately. `drizzle/meta/_ekklesia.json` declares the migration
|
||||
non-destructive; `pnpm migrations:validate` enforces that.
|
||||
non-destructive: `tests/schema.test.ts` asserts it, and Ekklesia's own migration validation enforces
|
||||
it when Signage is packaged and installed.
|
||||
|
||||
### Verifying what only a browser can show
|
||||
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.5.3/schema.json",
|
||||
"files": {
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/.pnpm-store",
|
||||
"!**/.turbo",
|
||||
"!**/coverage",
|
||||
"!**/dist",
|
||||
"!**/node_modules",
|
||||
"!pnpm-lock.yaml"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"preset": "recommended"
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double",
|
||||
"semicolons": "always",
|
||||
"trailingCommas": "all"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Assembles Signage into the Ekklesia platform it is tested against: the platform at the commit pinned
|
||||
# in ci/platform.env, this checkout at modules/signage, and ci/pnpm-lock.yaml as the workspace
|
||||
# lockfile. Signage cannot build or run outside an Ekklesia workspace, so every CI job starts here.
|
||||
#
|
||||
# bash ci/assemble.sh [--allow-missing-lock] [output directory]
|
||||
#
|
||||
# The output directory (default build/workspace) is deleted and recreated. The platform is fetched
|
||||
# by its `main` branch and the pinned commit must be on it, so CI only ever runs reviewed platform
|
||||
# code; the checkout is then by commit, so nothing after it depends on a ref the network can move.
|
||||
# Every .git directory is removed afterwards.
|
||||
#
|
||||
# --allow-missing-lock keeps the platform's own lockfile. Only ci/update-lock.sh uses it.
|
||||
set -Eeuo pipefail
|
||||
|
||||
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
allow_missing_lock=0
|
||||
if [[ "${1:-}" == "--allow-missing-lock" ]]; then
|
||||
allow_missing_lock=1
|
||||
shift
|
||||
fi
|
||||
out="${1:-$root/build/workspace}"
|
||||
|
||||
fail() {
|
||||
echo "assemble: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
for command in git tar; do
|
||||
command -v "$command" >/dev/null || fail "$command is required."
|
||||
done
|
||||
|
||||
# Read the two pins without sourcing the file: it is data, not a script.
|
||||
pin() {
|
||||
local value
|
||||
value="$(sed -n "s/^$1=//p" "$root/ci/platform.env")"
|
||||
[[ -n "$value" ]] || fail "ci/platform.env has no $1."
|
||||
printf '%s' "$value"
|
||||
}
|
||||
repository="$(pin EKKLESIA_PLATFORM_REPOSITORY)"
|
||||
commit="$(pin EKKLESIA_PLATFORM_COMMIT)"
|
||||
[[ "$commit" =~ ^[0-9a-f]{40}$ ]] || fail "EKKLESIA_PLATFORM_COMMIT must be a full commit SHA."
|
||||
|
||||
rm -rf "$out"
|
||||
mkdir -p "$(dirname -- "$out")"
|
||||
git init --quiet "$out"
|
||||
git -C "$out" remote add origin "$repository"
|
||||
git -C "$out" fetch --quiet --no-tags origin "+refs/heads/main:refs/remotes/origin/main" ||
|
||||
fail "cannot fetch the platform's main from ${repository}."
|
||||
git -C "$out" cat-file -e "${commit}^{commit}" 2>/dev/null ||
|
||||
fail "the pinned platform commit ${commit} is not on the platform's main."
|
||||
git -C "$out" merge-base --is-ancestor "$commit" refs/remotes/origin/main ||
|
||||
fail "the pinned platform commit ${commit} is not on the platform's main."
|
||||
git -C "$out" -c advice.detachedHead=false checkout --quiet --detach "$commit"
|
||||
[[ "$(git -C "$out" rev-parse HEAD)" == "$commit" ]] || fail "the platform did not check out at ${commit}."
|
||||
rm -rf "$out/.git"
|
||||
echo "assemble: platform ${commit}"
|
||||
|
||||
[[ ! -e "$out/modules/signage" ]] || fail "the platform checkout already has modules/signage."
|
||||
mkdir -p "$out/modules/signage"
|
||||
# Exactly the files this checkout tracks, as committed or as changed in the working tree. Nothing
|
||||
# installed, built, or ignored, and not the CI scaffolding, which is not part of the module.
|
||||
git -C "$root" ls-files -z --cached --others --exclude-standard -- . ':!ci' ':!.gitea' |
|
||||
tar -C "$root" --null --files-from=- --create --file=- |
|
||||
tar -C "$out/modules/signage" --extract --file=-
|
||||
echo "assemble: signage $(git -C "$root" rev-parse HEAD 2>/dev/null || echo "(working tree)")"
|
||||
|
||||
if [[ -f "$root/ci/pnpm-lock.yaml" ]]; then
|
||||
cp "$root/ci/pnpm-lock.yaml" "$out/pnpm-lock.yaml"
|
||||
elif ((allow_missing_lock == 0)); then
|
||||
fail "ci/pnpm-lock.yaml is missing; run bash ci/update-lock.sh."
|
||||
fi
|
||||
+6
-2
@@ -5,13 +5,17 @@
|
||||
# store, generated secrets, and an explicit migration apply against that fresh database only; nothing
|
||||
# is pushed, published, or logged in to. The image is removed afterwards.
|
||||
#
|
||||
# bash ci/integration.sh (after bash ci/assemble.sh)
|
||||
# bash ci/integration.sh
|
||||
#
|
||||
# NOT RUN: the integration job is disabled (.gitea/workflows/ci.yml says why). This script still
|
||||
# expects Ekklesia's deployment tree at build/workspace, which the removed ci/assemble.sh used to
|
||||
# build; it is rewritten when Signage is installed into the generic image as a Module Package.
|
||||
set -Eeuo pipefail
|
||||
|
||||
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
workspace="$root/build/workspace"
|
||||
[[ -f "$workspace/deploy/Dockerfile" ]] || {
|
||||
echo "integration: run bash ci/assemble.sh first." >&2
|
||||
echo "integration: Ekklesia's deployment tree is not at build/workspace." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# The Ekklesia platform Signage's CI builds and tests against: one reviewed commit on the platform's
|
||||
# `main`, never a moving ref. ci/assemble.sh refuses a commit that is not on `main`.
|
||||
#
|
||||
# Move it deliberately: change the commit, run `bash ci/update-lock.sh`, and commit both.
|
||||
EKKLESIA_PLATFORM_REPOSITORY=https://git.riverofglorymn.org/River_of_Glory_Church/Ekklesia.git
|
||||
EKKLESIA_PLATFORM_COMMIT=1788d17f0ab618ebc41550728da865d14cdd7a75
|
||||
Generated
-4503
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerates ci/pnpm-lock.yaml: the lockfile of Signage assembled into the pinned platform. Run it
|
||||
# after changing Signage's dependencies or ci/platform.env, and commit the result. CI installs with
|
||||
# --frozen-lockfile, so a stale lockfile fails there rather than resolving anything new.
|
||||
#
|
||||
# bash ci/update-lock.sh
|
||||
#
|
||||
# Uses the platform's own pinned pnpm (its packageManager field) through Corepack.
|
||||
set -Eeuo pipefail
|
||||
|
||||
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
bash "$root/ci/assemble.sh" --allow-missing-lock "$work/workspace"
|
||||
cd "$work/workspace"
|
||||
corepack enable >/dev/null 2>&1 || true
|
||||
corepack install >/dev/null
|
||||
pnpm install --lockfile-only
|
||||
cp pnpm-lock.yaml "$root/ci/pnpm-lock.yaml"
|
||||
echo "update-lock: wrote ci/pnpm-lock.yaml"
|
||||
+15
-9
@@ -3,6 +3,10 @@
|
||||
"version": "0.1.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "[email protected]",
|
||||
"engines": {
|
||||
"node": ">=24.11.0 <25"
|
||||
},
|
||||
"description": "Ekklesia's visual programming, timeline, synchronization, and display-delivery module.",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -23,16 +27,17 @@
|
||||
"build": "tsc --project tsconfig.build.json && ekklesia-build-module-ui",
|
||||
"lint": "biome lint .",
|
||||
"migration:generate": "drizzle-kit generate --config=drizzle.config.ts",
|
||||
"prepack": "pnpm --workspace-root exec turbo run build --filter=@ekklesia/signage",
|
||||
"prepack": "pnpm run build",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit --project tsconfig.json && tsc --noEmit --project tsconfig.ui.json && tsc --noEmit --project tsconfig.ui-test.json"
|
||||
"typecheck": "tsc --noEmit --project tsconfig.json && tsc --noEmit --project tsconfig.ui.json && tsc --noEmit --project tsconfig.ui-test.json",
|
||||
"validate": "node scripts/validate-module.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ekklesia/design-tokens": "workspace:*",
|
||||
"@ekklesia/module-database": "workspace:*",
|
||||
"@ekklesia/module-sdk": "workspace:*",
|
||||
"@ekklesia/module-ui-sdk": "workspace:*",
|
||||
"@ekklesia/ui": "workspace:*",
|
||||
"@ekklesia/design-tokens": "^0.2.0",
|
||||
"@ekklesia/module-database": "^0.2.0",
|
||||
"@ekklesia/module-sdk": "^0.2.0",
|
||||
"@ekklesia/module-ui-sdk": "^0.2.0",
|
||||
"@ekklesia/ui": "^0.2.0",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8",
|
||||
@@ -40,8 +45,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.3",
|
||||
"@ekklesia/module-ui-build": "workspace:*",
|
||||
"@ekklesia/typescript-config": "workspace:*",
|
||||
"@ekklesia/module-pack": "0.2.0",
|
||||
"@ekklesia/module-ui-build": "^0.2.0",
|
||||
"@ekklesia/typescript-config": "^0.2.0",
|
||||
"@types/node": "24.13.3",
|
||||
"@types/react": "19.2.18",
|
||||
"@types/react-dom": "19.2.4",
|
||||
|
||||
Generated
+2556
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
# This repository's own pnpm settings. Signage is a single standalone package, not part of any other
|
||||
# workspace; pnpm reads its settings from this file.
|
||||
|
||||
# esbuild, which the module UI build and the module packager run, has an install script. pnpm runs
|
||||
# none it was not told to.
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
|
||||
# pnpm holds back any package younger than its minimum release age, and refuses a lockfile that names
|
||||
# one. A new Ekklesia package train is that young on its release day. These exact versions, and only
|
||||
# these, are the train this lockfile pins (published by Ekklesia's release workflow, ADR 0053), so
|
||||
# they are exempt by version; a train bump replaces them, and everything else keeps the policy.
|
||||
minimumReleaseAgeExclude:
|
||||
- '@ekklesia/[email protected]'
|
||||
- '@ekklesia/[email protected]'
|
||||
- '@ekklesia/[email protected]'
|
||||
- '@ekklesia/[email protected]'
|
||||
- '@ekklesia/[email protected]'
|
||||
- '@ekklesia/[email protected]'
|
||||
- '@ekklesia/[email protected]'
|
||||
- '@ekklesia/[email protected]'
|
||||
- '@ekklesia/[email protected]'
|
||||
|
||||
# The narrowly scoped advisory remediation Signage had while it was built inside Ekklesia's workspace,
|
||||
# kept now that it is not. GHSA-67mh-4wv8-2f99 (moderate): the vulnerable esbuild 0.18.20 is reached
|
||||
# only through the deprecated @esbuild-kit/core-utils (drizzle-kit -> @esbuild-kit/esm-loader), whose
|
||||
# pinned range predates the fix. Scoped to that one edge, exactly as Ekklesia's own workspace does,
|
||||
# so drizzle-kit's direct esbuild and Vite's are not dragged to another version.
|
||||
overrides:
|
||||
"@esbuild-kit/core-utils>esbuild": "0.25.12"
|
||||
@@ -0,0 +1,145 @@
|
||||
// Validates the built Signage package and its migrations the way the platform reads them, with only
|
||||
// Ekklesia's published module-development packages: no Ekklesia checkout, no database.
|
||||
//
|
||||
// pnpm build && pnpm validate
|
||||
//
|
||||
// What it checks:
|
||||
// - the manifest parses, and its id and version match package.json and the built runtime;
|
||||
// - every declared entrypoint exists in the built package;
|
||||
// - the built runtime declares the same Module Database ABI as the manifest (Core refuses a
|
||||
// mismatch);
|
||||
// - the built schema entrypoint's only export is plain ABI v1 metadata for this module's own,
|
||||
// namespaced tables, and every foreign key targets one of them;
|
||||
// - the installed module SDK satisfies the manifest's `compatibility.moduleSdk`;
|
||||
// - the migration journal and policy parse, name the same migrations in the same order, and every
|
||||
// migration's SQL is present.
|
||||
//
|
||||
// What it cannot check, because only Ekklesia itself can: compatibility with a particular platform
|
||||
// installation, capability and module dependencies between installed modules, and a migration's
|
||||
// checksum against a database's applied history. The module packager and the platform's package
|
||||
// verifier re-check all of the above when Signage is built as a Module Package.
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { defineModuleSchemaMetadata, moduleTablePrefix } from "@ekklesia/module-database";
|
||||
import {
|
||||
parseDrizzleMigrationJournal,
|
||||
parseModuleManifest,
|
||||
parseModuleMigrationPolicy,
|
||||
} from "@ekklesia/module-sdk";
|
||||
|
||||
const root = fileURLToPath(new URL("..", import.meta.url));
|
||||
const failures = [];
|
||||
const passes = [];
|
||||
|
||||
function check(condition, description) {
|
||||
(condition ? passes : failures).push(description);
|
||||
}
|
||||
|
||||
async function json(relative) {
|
||||
return JSON.parse(await readFile(new URL(relative, pathToFileURL(root)), "utf8"));
|
||||
}
|
||||
|
||||
async function exists(relative) {
|
||||
try {
|
||||
return (await stat(new URL(relative, pathToFileURL(root)))).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** A caret range, `^X.Y.Z`, as the platform's manifests write them. Anything else is refused here
|
||||
* rather than half understood. */
|
||||
function satisfiesCaret(version, range) {
|
||||
const wanted = /^\^(\d+)\.(\d+)\.(\d+)$/u.exec(range);
|
||||
const actual = /^(\d+)\.(\d+)\.(\d+)$/u.exec(version);
|
||||
if (!wanted || !actual) {
|
||||
return false;
|
||||
}
|
||||
const [w, a] = [wanted.slice(1).map(Number), actual.slice(1).map(Number)];
|
||||
if (w[0] !== a[0] || (w[0] === 0 && w[1] !== a[1])) {
|
||||
return false;
|
||||
}
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
if (a[index] !== w[index]) {
|
||||
return a[index] > w[index];
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const packageJson = await json("package.json");
|
||||
const manifest = parseModuleManifest(await json("ekklesia.module.json"));
|
||||
check(true, "ekklesia.module.json is a valid manifest");
|
||||
check(packageJson.version === manifest.version, "package.json and the manifest name one version");
|
||||
check(packageJson.type === "module", "the package is ESM");
|
||||
check(packageJson.ekklesia?.layer === "module", "the package carries the module layer marker");
|
||||
|
||||
const { server, schema, migrations, ui, display } = manifest.entrypoints;
|
||||
for (const entrypoint of [server, schema, ui, display?.artifact].filter(Boolean)) {
|
||||
check(await exists(entrypoint), `the built package has ${entrypoint}`);
|
||||
}
|
||||
|
||||
const runtime = (await import(new URL(server, pathToFileURL(root)).href)).default;
|
||||
check(runtime?.id === manifest.id, "the built runtime's id is the manifest's");
|
||||
check(runtime?.version === manifest.version, "the built runtime's version is the manifest's");
|
||||
check(
|
||||
JSON.stringify(runtime?.database ?? null) === JSON.stringify(manifest.database ?? null),
|
||||
"the built runtime declares the manifest's database ABI",
|
||||
);
|
||||
|
||||
if (schema) {
|
||||
const namespace = await import(new URL(schema, pathToFileURL(root)).href);
|
||||
check(
|
||||
Object.keys(namespace).join() === "default",
|
||||
"the schema entrypoint exports nothing but its metadata",
|
||||
);
|
||||
const metadata = defineModuleSchemaMetadata(namespace.default);
|
||||
check(
|
||||
JSON.stringify(JSON.parse(JSON.stringify(metadata))) === JSON.stringify(metadata),
|
||||
"the schema metadata is plain data",
|
||||
);
|
||||
check(metadata.moduleId === manifest.id, "the schema metadata is this module's");
|
||||
const prefix = moduleTablePrefix(manifest.id);
|
||||
const owned = new Set(metadata.tables.map((table) => table.name));
|
||||
check(
|
||||
metadata.tables.every((table) => table.name.startsWith(prefix)),
|
||||
`every table is in the ${prefix} namespace`,
|
||||
);
|
||||
check(
|
||||
metadata.tables.every((table) => table.foreignKeys.every((key) => owned.has(key.table))),
|
||||
"every foreign key targets one of this module's own tables",
|
||||
);
|
||||
}
|
||||
|
||||
const sdkVersion = (await json("node_modules/@ekklesia/module-sdk/package.json")).version;
|
||||
check(
|
||||
satisfiesCaret(sdkVersion, manifest.compatibility.moduleSdk),
|
||||
`the installed module SDK ${sdkVersion} satisfies compatibility.moduleSdk ${manifest.compatibility.moduleSdk}`,
|
||||
);
|
||||
|
||||
if (migrations) {
|
||||
const directory = migrations.endsWith("/") ? migrations : `${migrations}/`;
|
||||
const journal = parseDrizzleMigrationJournal(await json(`${directory}meta/_journal.json`));
|
||||
const policy = parseModuleMigrationPolicy(await json(`${directory}meta/_ekklesia.json`));
|
||||
check(policy.moduleId === manifest.id, "the migration policy is this module's");
|
||||
check(
|
||||
journal.entries.map((entry) => entry.tag).join() ===
|
||||
policy.entries.map((entry) => entry.tag).join(),
|
||||
"the migration journal and policy name the same migrations in the same order",
|
||||
);
|
||||
for (const entry of journal.entries) {
|
||||
check(await exists(`${directory}${entry.tag}.sql`), `${entry.tag}.sql is present`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const description of passes) {
|
||||
console.log(`ok ${description}`);
|
||||
}
|
||||
for (const description of failures) {
|
||||
console.log(`FAIL ${description}`);
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
console.error(`validate: ${failures.length} check(s) failed.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`validate: ${manifest.id}@${manifest.version} passed ${passes.length} checks.`);
|
||||
+3
-3
@@ -3,9 +3,9 @@ import { defineConfig } from "vitest/config";
|
||||
/**
|
||||
* Signage's own test configuration.
|
||||
*
|
||||
* It exists because this module's browser tests are `.tsx` — they mount real React — and the
|
||||
* repository root's configuration includes only `.test.ts`. Without this file those tests are
|
||||
* silently not run, which is the worst possible failure mode for a test suite.
|
||||
* It names both `.test.ts` and `.test.tsx` explicitly, because this module's browser tests are `.tsx`
|
||||
* — they mount real React — and a test that is silently not run is the worst possible failure mode
|
||||
* for a test suite. An empty run fails rather than passes.
|
||||
*/
|
||||
export default defineConfig({
|
||||
test: {
|
||||
|
||||
Reference in New Issue
Block a user