2 Commits
Author SHA1 Message Date
The_River e7f7404d35 Merge pull request 'feat: build Signage on its own from the published Ekklesia 0.2.0 packages' (#11) from feat/standalone-build into main
CI / lint (push) Successful in 10s
CI / typecheck (push) Successful in 10s
CI / test (push) Successful in 15s
CI / build (push) Successful in 12s
CI / integration (push) Skipped
Reviewed-on: https://git.riverofglorymn.org/River_of_Glory_Church/Ekklesia_Signage/pulls/11
Reviewed-by: The_River <[email protected]>
2026-10-09 02:18:39 +00:00
ekklesiaandClaude Opus 5.5 33bb146349 feat: build Signage on its own from the published Ekklesia 0.2.0 packages
CI / lint (pull_request) Successful in 11s
CI / typecheck (pull_request) Successful in 11s
CI / test (pull_request) Successful in 17s
CI / build (pull_request) Successful in 12s
CI / integration (pull_request) Skipped
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]>
2026-10-08 20:49:17 -05:00
16 changed files with 2888 additions and 4731 deletions
+53 -73
View File
@@ -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
View File
@@ -1,7 +1,7 @@
node_modules/
.pnpm-store/
dist/
.turbo/
coverage/
*.log
.DS_Store
build/
+1
View File
@@ -0,0 +1 @@
24.18.0
+3
View File
@@ -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/
+42 -41
View File
@@ -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
View File
@@ -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"
}
}
}
-72
View File
@@ -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
View File
@@ -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
}
-6
View File
@@ -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
-4503
View File
File diff suppressed because it is too large Load Diff
-21
View File
@@ -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
View File
@@ -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",
+2556
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -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"
+145
View File
@@ -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
View File
@@ -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: {