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]>
This commit is contained in:
ekklesia
2026-10-08 20:49:17 -05:00
co-authored by Claude Opus 5.5
parent fefe34be03
commit 33bb146349
16 changed files with 2888 additions and 4731 deletions
+53 -73
View File
@@ -1,8 +1,9 @@
name: CI name: CI
# Signage builds, tests, and runs only inside an Ekklesia workspace, so every job first assembles it # Signage is a standalone repository: every job checks out only this repository, installs it with
# into the platform commit pinned in ci/platform.env (ci/assemble.sh) and installs that workspace # --frozen-lockfile from its own pnpm-lock.yaml, and takes Ekklesia's module-development packages
# with --frozen-lockfile from ci/pnpm-lock.yaml. # (@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): # The same trust rules as the platform's own CI (docs/development/security.md there):
# * every action is pinned by a full commit SHA; # * every action is pinned by a full commit SHA;
@@ -11,10 +12,8 @@ name: CI
# * there is no `pull_request_target`; # * there is no `pull_request_target`;
# * the self-hosted runner executes trusted, reviewed code only. # * the self-hosted runner executes trusted, reviewed code only.
# #
# The server is Gitea 1.24, which ignores `permissions`, `concurrency`, and `timeout-minutes` # The server is Gitea 1.27, which honours `concurrency` cancellation; `permissions` and
# (https://docs.gitea.com/1.24/usage/actions/comparison). They are declared so the workflow is already # `timeout-minutes` are declared so the workflow is correct wherever they are enforced.
# correct on a release that honours them; on 1.24 they bound nothing, and `permissions` does not
# restrict the job token.
on: on:
pull_request: pull_request:
@@ -42,24 +41,21 @@ jobs:
# in .git/config for a later step to read. # in .git/config for a later step to read.
persist-credentials: false persist-credentials: false
- name: Assemble Signage into the pinned platform - name: Set up Node.js at the repository's version
run: bash ci/assemble.sh
- name: Set up Node.js at the platform's version
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: build/workspace/.node-version node-version-file: .node-version
- name: Install the assembled workspace (frozen lockfile) # pnpm at exactly the `packageManager` version, and @ekklesia/* from the public registry named in
working-directory: build/workspace # .npmrc: no token, and no Ekklesia checkout.
- name: Install (frozen lockfile)
run: | run: |
corepack enable corepack enable
corepack install corepack install
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
- name: Lint - name: Lint
working-directory: build/workspace run: pnpm lint
run: pnpm --filter @ekklesia/signage lint
- name: Check that CI cleans up every stack it starts - name: Check that CI cleans up every stack it starts
run: bash ci/check-cleanup.sh run: bash ci/check-cleanup.sh
@@ -76,28 +72,21 @@ jobs:
# in .git/config for a later step to read. # in .git/config for a later step to read.
persist-credentials: false persist-credentials: false
- name: Assemble Signage into the pinned platform - name: Set up Node.js at the repository's version
run: bash ci/assemble.sh
- name: Set up Node.js at the platform's version
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: build/workspace/.node-version node-version-file: .node-version
- name: Install the assembled workspace (frozen lockfile) # pnpm at exactly the `packageManager` version, and @ekklesia/* from the public registry named in
working-directory: build/workspace # .npmrc: no token, and no Ekklesia checkout.
- name: Install (frozen lockfile)
run: | run: |
corepack enable corepack enable
corepack install corepack install
pnpm install --frozen-lockfile 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 - name: Typecheck the server, browser, and browser-test projects
working-directory: build/workspace run: pnpm typecheck
run: pnpm --filter @ekklesia/signage typecheck
test: test:
name: test name: test
@@ -111,28 +100,21 @@ jobs:
# in .git/config for a later step to read. # in .git/config for a later step to read.
persist-credentials: false persist-credentials: false
- name: Assemble Signage into the pinned platform - name: Set up Node.js at the repository's version
run: bash ci/assemble.sh
- name: Set up Node.js at the platform's version
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: build/workspace/.node-version node-version-file: .node-version
- name: Install the assembled workspace (frozen lockfile) # pnpm at exactly the `packageManager` version, and @ekklesia/* from the public registry named in
working-directory: build/workspace # .npmrc: no token, and no Ekklesia checkout.
- name: Install (frozen lockfile)
run: | run: |
corepack enable corepack enable
corepack install corepack install
pnpm install --frozen-lockfile 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 - name: Unit, browser, and display contract tests
working-directory: build/workspace run: pnpm test
run: pnpm --filter @ekklesia/signage test
build: build:
name: build name: build
@@ -146,48 +128,44 @@ jobs:
# in .git/config for a later step to read. # in .git/config for a later step to read.
persist-credentials: false persist-credentials: false
- name: Assemble Signage into the pinned platform - name: Set up Node.js at the repository's version
run: bash ci/assemble.sh
- name: Set up Node.js at the platform's version
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: build/workspace/.node-version node-version-file: .node-version
- name: Install the assembled workspace (frozen lockfile) # pnpm at exactly the `packageManager` version, and @ekklesia/* from the public registry named in
working-directory: build/workspace # .npmrc: no token, and no Ekklesia checkout.
- name: Install (frozen lockfile)
run: | run: |
corepack enable corepack enable
corepack install corepack install
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
# The same gates the platform's image build runs against exactly this module set: its boundary - name: Build the server and both browser artifacts
# rules (which apply to modules), the manifest, and the migrations. run: pnpm build
- name: Platform gates (boundaries, module manifest, migrations)
working-directory: build/workspace
run: |
pnpm boundaries
pnpm modules:validate
pnpm migrations:validate
- name: Build Signage and the platform it depends on # The built package and its migrations, read the way the platform reads them, with only the
working-directory: build/workspace # published SDK parsers (scripts/validate-module.mjs says what only Ekklesia itself can check).
run: pnpm exec turbo run build --filter='@ekklesia/signage...' - 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 # 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 # 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 # 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. # 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 # DISABLED. From Ekklesia 0.2.0 on, the platform's image is the one generic image (platform ADR
# the one generic image (platform ADR 0048): deploy/Dockerfile keeps `modules/` out of its build # 0048): deploy/Dockerfile keeps `modules/` out of its build context and refuses one that has it,
# context and refuses one that has it, and the deployment smoke installs modules only as canonical # and the deployment smoke installs modules only as canonical Module Packages (ADR 0049). Signage is
# Module Packages (ADR 0049). Signage is not one yet: its static sharp dependency needs the native # not one yet: its static sharp dependency needs the native linux-x64-glibc package flow. Run as it
# linux-x64-glibc package flow, which is deliberately not part of the 0.2.0 compatibility change. # was, this job would build an image without Signage and drive the module template's display
# Run against 0.2.0, this job would build an image without Signage and drive the module template's # instead, so it is switched off rather than left reporting on something else.
# 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 # It returns when Signage is built as a canonical linux-x64-glibc Module Package and installed into
# installed into the generic image, and `if: false` is removed in that change. # 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: integration:
name: integration name: integration
if: false if: false
@@ -207,8 +185,10 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Assemble Signage into the pinned platform - name: Fetch Ekklesia's deployment smoke and build Signage's Module Package
run: bash ci/assemble.sh 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 # 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. # 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 - name: Build the image and run the display scenarios
run: bash ci/integration.sh run: bash ci/integration.sh
# Whatever happened above, this job's own stacks do not outlive it. If assembly failed, nothing # Whatever happened above, this job's own stacks do not outlive it. If the first step failed,
# was started. # nothing was started.
- name: Remove this job's CI stacks - name: Remove this job's CI stacks
if: always() if: always()
run: | run: |
+1 -1
View File
@@ -1,7 +1,7 @@
node_modules/ node_modules/
.pnpm-store/
dist/ dist/
.turbo/ .turbo/
coverage/ coverage/
*.log *.log
.DS_Store .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 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. 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 Signage builds and tests on its own. You need no Ekklesia checkout and no Ekklesia account:
eventually be installed at:
- **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 ```bash
git clone <ekklesia> ekklesia git clone <this repository> signage
git clone <this repository> ekklesia/modules/signage cd signage
cd ekklesia corepack enable
pnpm install pnpm install --frozen-lockfile
``` ```
`modules/*` is an Ekklesia workspace glob, so the checkout is picked up with no registry edit, Ekklesia's module-development packages (`@ekklesia/module-sdk`, `@ekklesia/module-database`,
no second copy of Ekklesia, and no vendored SDK. `@ekklesia/module-sdk`, `@ekklesia/module-database`, `@ekklesia/module-ui-sdk`, `@ekklesia/ui`, `@ekklesia/design-tokens`, `@ekklesia/module-ui-build`,
`@ekklesia/module-ui-sdk`, `@ekklesia/design-tokens`, `@ekklesia/ui`, and `@ekklesia/module-ui-build` `@ekklesia/module-pack`, `@ekklesia/typescript-config`) come from Ekklesia's public package registry.
all resolve through the workspace as `workspace:*` dependencies. 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 Signage requires Ekklesia 0.2: manifest version 8 and the Module Database ABI v1 (platform ADR 0050).
`modules/signage` into Ekklesia's `pnpm-lock.yaml`. That entry belongs to whoever maintains the It reaches PostgreSQL only through the ABI v1 executor the platform hands it, with its own Drizzle
Ekklesia checkout; it is not committed from this repository. tables and the module-side adapter from `@ekklesia/module-database/drizzle`. Core receives SQL text,
- Signage requires Ekklesia 0.2: manifest version 8 and the Module Database ABI v1 (platform ADR bound values, and plain schema metadata, never a Drizzle object.
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.
### Commands ### Commands
Run from the **Ekklesia repository root**:
```bash ```bash
pnpm --filter @ekklesia/signage build # tsc + the canonical module UI build pnpm lint # Biome, with this repository's biome.json
pnpm --filter @ekklesia/signage test # unit and browser-artifact tests pnpm typecheck # node, browser, and browser-test projects
pnpm --filter @ekklesia/signage typecheck # node, browser, and browser-test projects pnpm test # unit, browser-artifact, and display contract tests (vitest.config.ts)
pnpm --filter @ekklesia/signage lint 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
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
``` ```
Signage's tests are its own: `pnpm test` here runs `vitest` against this package's `vitest.config.ts`, CI runs exactly these, from a clean checkout.
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.
Never run `pnpm migrations:apply` against a persistent database while developing this module. Signage `pnpm validate` uses only the published SDK parsers. It cannot check what only an Ekklesia
reads no environment variable, constructs no database client, configures no object storage, hardcodes installation knows: compatibility with that installation, other installed modules' capabilities and
no Ekklesia URL, opens no socket of its own, and applies no migration: every one of those reaches it 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. through the host.
### Changing the schema ### Changing the schema
```bash ```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 `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 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 ### 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 # 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. # 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 set -Eeuo pipefail
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
workspace="$root/build/workspace" workspace="$root/build/workspace"
[[ -f "$workspace/deploy/Dockerfile" ]] || { [[ -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 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", "version": "0.1.2",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "[email protected]",
"engines": {
"node": ">=24.11.0 <25"
},
"description": "Ekklesia's visual programming, timeline, synchronization, and display-delivery module.", "description": "Ekklesia's visual programming, timeline, synchronization, and display-delivery module.",
"files": [ "files": [
"dist", "dist",
@@ -23,16 +27,17 @@
"build": "tsc --project tsconfig.build.json && ekklesia-build-module-ui", "build": "tsc --project tsconfig.build.json && ekklesia-build-module-ui",
"lint": "biome lint .", "lint": "biome lint .",
"migration:generate": "drizzle-kit generate --config=drizzle.config.ts", "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", "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": { "dependencies": {
"@ekklesia/design-tokens": "workspace:*", "@ekklesia/design-tokens": "^0.2.0",
"@ekklesia/module-database": "workspace:*", "@ekklesia/module-database": "^0.2.0",
"@ekklesia/module-sdk": "workspace:*", "@ekklesia/module-sdk": "^0.2.0",
"@ekklesia/module-ui-sdk": "workspace:*", "@ekklesia/module-ui-sdk": "^0.2.0",
"@ekklesia/ui": "workspace:*", "@ekklesia/ui": "^0.2.0",
"drizzle-orm": "0.45.2", "drizzle-orm": "0.45.2",
"react": "19.2.8", "react": "19.2.8",
"react-dom": "19.2.8", "react-dom": "19.2.8",
@@ -40,8 +45,9 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.3", "@biomejs/biome": "2.5.3",
"@ekklesia/module-ui-build": "workspace:*", "@ekklesia/module-pack": "0.2.0",
"@ekklesia/typescript-config": "workspace:*", "@ekklesia/module-ui-build": "^0.2.0",
"@ekklesia/typescript-config": "^0.2.0",
"@types/node": "24.13.3", "@types/node": "24.13.3",
"@types/react": "19.2.18", "@types/react": "19.2.18",
"@types/react-dom": "19.2.4", "@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. * Signage's own test configuration.
* *
* It exists because this module's browser tests are `.tsx` — they mount real React — and the * It names both `.test.ts` and `.test.tsx` explicitly, because this module's browser tests are `.tsx`
* repository root's configuration includes only `.test.ts`. Without this file those tests are * — they mount real React — and a test that is silently not run is the worst possible failure mode
* silently not run, which is the worst possible failure mode for a test suite. * for a test suite. An empty run fails rather than passes.
*/ */
export default defineConfig({ export default defineConfig({
test: { test: {