ci: build, test, and run Signage inside a pinned Ekklesia on every change
CI / lint (pull_request) Successful in 12s
CI / typecheck (pull_request) Successful in 15s
CI / test (pull_request) Successful in 19s
CI / build (pull_request) Successful in 18s
CI / integration (pull_request) Successful in 3m34s

Signage had no CI. Its tests passed while three defects shipped that
appear only inside Ekklesia, in a real browser. Every job now assembles
Signage into a pinned platform commit, as an instance does, and
installs that workspace with --frozen-lockfile:
- lint, typecheck, test: Signage's own checks;
- build: the platform's gates against exactly this module set (boundary
  rules, module manifest, migrations), then the build;
- integration: the deployment image from the platform's Dockerfile,
  then the platform's split-topology smoke. It pairs a kiosk display,
  restarts it on the same profile, and runs the scenarios in
  ci/display-scenarios.

The supporting files:
- ci/platform.env pins one platform commit. ci/assemble.sh fetches the
  platform's main and refuses a pinned commit that is not on it, so CI
  only runs reviewed platform code.
- ci/pnpm-lock.yaml is the assembled workspace's lockfile, regenerated
  with ci/update-lock.sh.

Same trust rules as the platform: actions pinned by SHA,
persist-credentials false, no secrets, no pull_request_target.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-05 19:11:53 -05:00
co-authored by Claude Opus 5.5
parent 5e418bf941
commit 3073baeb8b
8 changed files with 4742 additions and 0 deletions
Executable
+72
View File
@@ -0,0 +1,72 @@
#!/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
+4
View File
@@ -0,0 +1,4 @@
# The platform's display-device scenarios this repository's CI runs against Signage, one per line
# (scripts/lib/deploy-smoke-display.mjs in the pinned platform). Pairing a kiosk and restarting it on
# the same profile always run first. A change that makes another scenario pass adds it here.
two-pages
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Signage inside Ekklesia, end to end: builds the deployment image from the assembled workspace with
# the platform's own Dockerfile, then runs the platform's split-topology deployment smoke against it
# with the display-device scenarios listed in ci/display-scenarios. A disposable PostgreSQL and object
# 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)
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
exit 1
}
image="signage-ci:${GITHUB_SHA:-local}-${GITHUB_RUN_ID:-0}-${GITHUB_RUN_ATTEMPT:-1}"
trap 'docker image rm --force "$image" >/dev/null 2>&1 || true' EXIT
scenarios="$(sed -e 's/#.*//' -e '/^[[:space:]]*$/d' "$root/ci/display-scenarios" | tr '\n' ' ')"
[[ -n "${scenarios// /}" ]] || {
echo "integration: ci/display-scenarios lists no scenario." >&2
exit 1
}
DOCKER_BUILDKIT=1 docker build --quiet --file "$workspace/deploy/Dockerfile" --tag "$image" \
"$workspace" >/dev/null
echo "integration: built the deployment image with Signage; scenarios: ${scenarios}"
cd "$workspace"
EKKLESIA_SMOKE_TOPOLOGY=split \
EKKLESIA_SMOKE_IMAGE="$image" \
EKKLESIA_SMOKE_DISPLAY_DEVICE=1 \
EKKLESIA_SMOKE_DISPLAY_SCENARIOS="$scenarios" \
EKKLESIA_SMOKE_WATCH_MS=120000 \
EKKLESIA_SMOKE_RECYCLE_WATCH_MS=120000 \
bash scripts/smoke-deploy.sh
+6
View File
@@ -0,0 +1,6 @@
# 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=f50e8f42018c15698709739e2ba54771e97df08a
+4403
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
#!/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"