Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86121d7931 | ||
|
|
8a7e96dd61 | ||
|
|
844bbcf2f1 | ||
|
|
88a536464a | ||
|
|
dd656eb18a | ||
|
|
e83cdc62a7 | ||
|
|
fa2a1493ea | ||
|
|
88038476b3 | ||
|
|
871d198221 | ||
|
|
c28e4b2de7 | ||
|
|
6233de0e5c | ||
|
|
6fca109375 | ||
|
|
e7f7404d35 |
+75
-30
@@ -1,9 +1,10 @@
|
||||
name: CI
|
||||
|
||||
# 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.
|
||||
# Signage is a standalone repository: every job 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). Only the integration job fetches
|
||||
# Core source: one immutable commit through the pinned Gitea pull ref in ci/platform.env, used as the
|
||||
# generic deployment harness and image source, never as a workspace dependency.
|
||||
#
|
||||
# The same trust rules as the platform's own CI (docs/development/security.md there):
|
||||
# * every action is pinned by a full commit SHA;
|
||||
@@ -14,6 +15,8 @@ name: CI
|
||||
#
|
||||
# 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.
|
||||
# Artifact actions v3 are deliberate: Gitea implements that protocol, while v4 refuses non-GitHub
|
||||
# servers. This workflow never publishes or mutates a release; publishing is a separate manual gate.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -58,7 +61,9 @@ jobs:
|
||||
run: pnpm lint
|
||||
|
||||
- name: Check that CI cleans up every stack it starts
|
||||
run: bash ci/check-cleanup.sh
|
||||
run: |
|
||||
bash ci/check-cleanup.sh
|
||||
python3 -m unittest ci/test_release_assets.py
|
||||
|
||||
typecheck:
|
||||
name: typecheck
|
||||
@@ -149,33 +154,58 @@ jobs:
|
||||
- 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. 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.
|
||||
# Build the canonical pair once. Integration downloads these exact bytes and may not repackage.
|
||||
package:
|
||||
name: package
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
sha256: ${{ steps.canonical.outputs.sha256 }}
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js at the repository's version
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Install (frozen lockfile)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack install
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build the canonical Module Package pair
|
||||
id: canonical
|
||||
run: bash ci/package.sh
|
||||
|
||||
- name: Hand the canonical pair to its consumers
|
||||
uses: actions/upload-artifact@c6a3b2bd78b3985e4b2f15397fec357f0fd808de # v3.2.2-node20
|
||||
with:
|
||||
name: signage-module-package
|
||||
path: build/module-package/
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
# The real linux-x64-glibc Signage Module Package inside Ekklesia, end to end. The pinned platform
|
||||
# builds one generic image with no bundled module source, verifies and installs Signage into
|
||||
# persistent module storage, applies its migrations, and drives its split-origin Dashboard and
|
||||
# kiosk display in real browsers. It then checks process restart, container recreation, lifecycle
|
||||
# disable/re-enable, removal, and reinstall against the same database and package.
|
||||
integration:
|
||||
name: integration
|
||||
if: false
|
||||
needs:
|
||||
- test
|
||||
- build
|
||||
- package
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
EXPECTED_PACKAGE_SHA256: ${{ needs.package.outputs.sha256 }}
|
||||
# Names this job's stacks, so its final step removes exactly these (the platform's
|
||||
# scripts/lib/ci-project.sh).
|
||||
EKKLESIA_CI_JOB_KEY: signage-integration
|
||||
@@ -185,17 +215,32 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Fetch Ekklesia's deployment smoke and build Signage's Module Package
|
||||
- name: Set up Node.js at the repository's version
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Install (frozen lockfile)
|
||||
run: |
|
||||
echo "integration: rewritten when Signage is a canonical linux-x64-glibc Module Package." >&2
|
||||
exit 1
|
||||
corepack enable
|
||||
corepack install
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Receive the canonical Module Package pair
|
||||
uses: actions/download-artifact@ad191675b41f6a5b46da9a048cb6893812da158b # v3.1.0-node20
|
||||
with:
|
||||
name: signage-module-package
|
||||
path: build/module-package/
|
||||
|
||||
- name: Fetch the pinned Ekklesia integration harness
|
||||
run: bash ci/fetch-platform.sh
|
||||
|
||||
# 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.
|
||||
- name: Sweep stale CI stacks from the runner
|
||||
run: bash build/workspace/scripts/ci-sweep.sh --stale
|
||||
run: bash build/platform/scripts/ci-sweep.sh --stale
|
||||
|
||||
- name: Build the image and run the display scenarios
|
||||
- name: Install and run the prebuilt external Signage package
|
||||
run: bash ci/integration.sh
|
||||
|
||||
# Whatever happened above, this job's own stacks do not outlive it. If the first step failed,
|
||||
@@ -203,6 +248,6 @@ jobs:
|
||||
- name: Remove this job's CI stacks
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f build/workspace/scripts/ci-sweep.sh ]; then
|
||||
bash build/workspace/scripts/ci-sweep.sh --job
|
||||
if [ -f build/platform/scripts/ci-sweep.sh ]; then
|
||||
bash build/platform/scripts/ci-sweep.sh --job
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
name: Publish Signage Release
|
||||
|
||||
# Builds, proves, and publishes one Signage release. This is deliberately separate from CI: only an
|
||||
# operator's workflow_dispatch on a selected branch can start it. The normal release ref is main.
|
||||
# Every local gate, including the full external-package integration and ADR 0055 redistribution
|
||||
# verification, finishes before the release/tag is created or an existing release is touched.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: The Signage version to publish, equal to package and module metadata.
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Never cancel a publication part way, and never run two at once. A rerun safely resumes an
|
||||
# identical partial upload, but refuses a tag, release, or asset that differs from this run.
|
||||
concurrency:
|
||||
group: publish-signage-release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release-gate:
|
||||
name: release gate
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 75
|
||||
outputs:
|
||||
sha256: ${{ steps.canonical.outputs.sha256 }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
EKKLESIA_CI_JOB_KEY: signage-publish-release
|
||||
RELEASE_VERSION: ${{ inputs.version }}
|
||||
RELEASE_TAG: v${{ inputs.version }}
|
||||
RELEASE_REVISION: ${{ github.sha }}
|
||||
steps:
|
||||
- name: Check out the operator-selected revision
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js at the repository's version
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Install the reviewed build and release tooling
|
||||
run: |
|
||||
corepack enable
|
||||
corepack install
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# Match Core's manual publisher: releases come only from reviewed main. The source version is
|
||||
# checked before the expensive gate, and the selected commit remains the immutable tag target.
|
||||
- name: Validate requested version and selected revision
|
||||
run: |
|
||||
[[ "$GITHUB_REF" == refs/heads/main ]] || {
|
||||
echo "publish: select main; got $GITHUB_REF" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
|
||||
echo "publish: version must be X.Y.Z; got $RELEASE_VERSION" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$RELEASE_REVISION" =~ ^[0-9a-f]{40}$ ]]
|
||||
[[ "$(git rev-parse HEAD)" == "$RELEASE_REVISION" ]]
|
||||
[[ -z "$(git status --porcelain)" ]]
|
||||
node --input-type=module -e '
|
||||
import { readFileSync } from "node:fs";
|
||||
const requested = process.env.RELEASE_VERSION;
|
||||
const packageVersion = JSON.parse(readFileSync("package.json", "utf8")).version;
|
||||
const moduleVersion = JSON.parse(readFileSync("ekklesia.module.json", "utf8")).version;
|
||||
if (packageVersion !== requested || moduleVersion !== requested) {
|
||||
console.error(`publish: requested ${requested}, package ${packageVersion}, module ${moduleVersion}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`publish: source metadata agrees on ${requested}`);
|
||||
'
|
||||
|
||||
# This is the only package build in the workflow. Every later step consumes this directory.
|
||||
- name: Build the canonical Module Package pair once
|
||||
id: canonical
|
||||
run: bash ci/package.sh build/release-assets
|
||||
|
||||
- name: Verify the canonical Module Package
|
||||
env:
|
||||
EXPECTED_PACKAGE_SHA256: ${{ steps.canonical.outputs.sha256 }}
|
||||
run: |
|
||||
pnpm validate
|
||||
python3 ci/release-assets.py verify-package \
|
||||
build/release-assets "$RELEASE_TAG" "$RELEASE_REVISION"
|
||||
|
||||
- name: Fetch the pinned Ekklesia integration harness
|
||||
run: bash ci/fetch-platform.sh
|
||||
|
||||
- name: Sweep stale CI stacks from the runner
|
||||
run: bash build/platform/scripts/ci-sweep.sh --stale
|
||||
|
||||
- name: Run full integration with the exact release package
|
||||
env:
|
||||
EXPECTED_PACKAGE_SHA256: ${{ steps.canonical.outputs.sha256 }}
|
||||
run: bash ci/integration.sh "$GITHUB_WORKSPACE/build/release-assets"
|
||||
|
||||
- name: Remove this publication's CI stacks
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f build/platform/scripts/ci-sweep.sh ]; then
|
||||
bash build/platform/scripts/ci-sweep.sh --job
|
||||
fi
|
||||
|
||||
- name: Build and verify required redistribution assets
|
||||
env:
|
||||
EXPECTED_PACKAGE_SHA256: ${{ steps.canonical.outputs.sha256 }}
|
||||
run: |
|
||||
package="$(find build/release-assets -maxdepth 1 -type f -name 'signage-*-linux-x64-glibc.tar.gz')"
|
||||
pnpm exec ekklesia-corresponding-source acquire \
|
||||
--package "$package" --cache "$RUNNER_TEMP/signage-corresponding-sources"
|
||||
pnpm exec ekklesia-corresponding-source build \
|
||||
--package "$package" --cache "$RUNNER_TEMP/signage-corresponding-sources" \
|
||||
--out build/release-assets
|
||||
source="${package%.tar.gz}-corresponding-source.tar.gz"
|
||||
metadata="${package%.tar.gz}.redistribution.json"
|
||||
pnpm exec ekklesia-corresponding-source verify \
|
||||
--package "$package" --source "$source" --metadata "$metadata"
|
||||
python3 ci/release-assets.py verify \
|
||||
build/release-assets "$RELEASE_TAG" "$RELEASE_REVISION"
|
||||
|
||||
# The write-authorized job receives only the completely proven bundle. Artifact actions v3
|
||||
# are required by Gitea; v4 refuses servers other than github.com.
|
||||
- name: Hand the proven release bundle to the publisher
|
||||
uses: actions/upload-artifact@c6a3b2bd78b3985e4b2f15397fec357f0fd808de # v3.2.2-node20
|
||||
with:
|
||||
name: signage-release-assets
|
||||
path: build/release-assets/
|
||||
if-no-files-found: error
|
||||
retention-days: 90
|
||||
|
||||
publish:
|
||||
name: publish
|
||||
needs: release-gate
|
||||
if: github.ref == 'refs/heads/main'
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
EXPECTED_PACKAGE_SHA256: ${{ needs.release-gate.outputs.sha256 }}
|
||||
RELEASE_VERSION: ${{ inputs.version }}
|
||||
RELEASE_TAG: v${{ inputs.version }}
|
||||
RELEASE_REVISION: ${{ github.sha }}
|
||||
steps:
|
||||
# The checked-out helper is the same reviewed revision the gate proved. Credentials are not
|
||||
# persisted; the automatic token is exposed only to the final API call below.
|
||||
- name: Check out the proven revision
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Receive the proven release bundle
|
||||
uses: actions/download-artifact@ad191675b41f6a5b46da9a048cb6893812da158b # v3.1.0-node20
|
||||
with:
|
||||
name: signage-release-assets
|
||||
path: build/release-assets/
|
||||
|
||||
- name: Recheck the received release bundle
|
||||
run: |
|
||||
python3 ci/release-assets.py verify \
|
||||
build/release-assets "$RELEASE_TAG" "$RELEASE_REVISION"
|
||||
|
||||
# Only this final step can mutate Gitea. It creates v<version> at the selected commit and a
|
||||
# prerelease if absent, then uploads the already-tested bytes. A rerun resumes only identical
|
||||
# state. Every asset is read back without Authorization and checked byte for byte.
|
||||
- name: Create the release and publish the exact tested assets
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
python3 ci/release-assets.py publish \
|
||||
build/release-assets "$RELEASE_TAG" "$RELEASE_REVISION"
|
||||
@@ -3,5 +3,7 @@ node_modules/
|
||||
dist/
|
||||
.turbo/
|
||||
coverage/
|
||||
build/
|
||||
__pycache__/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
@@ -16,7 +16,7 @@ Ekklesia and vendors none of its SDKs.
|
||||
|
||||
## Status
|
||||
|
||||
Version 0.1.2 is feature-complete for its declared scope, and Signage has run in production since
|
||||
Version 0.1.3 is feature-complete for its declared scope, and Signage has run in production since
|
||||
0.1.1. What exists today:
|
||||
|
||||
- The module package, manifest (v8), four capabilities, six permissions, ten operator settings, and
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"**",
|
||||
"!**/.pnpm-store",
|
||||
"!**/.turbo",
|
||||
"!**/build",
|
||||
"!**/coverage",
|
||||
"!**/dist",
|
||||
"!**/node_modules",
|
||||
|
||||
+58
-3
@@ -6,11 +6,12 @@
|
||||
#
|
||||
# The `integration` job is the one that starts Docker stacks. It must have its own
|
||||
# EKKLESIA_CI_JOB_KEY, sweep stale CI stacks before it starts anything, and remove its own in a final
|
||||
# `if: always()` step; and the image ci/integration.sh builds must carry the CI labels the sweep finds.
|
||||
# `if: always()` step. The pinned platform smoke owns and labels every image and container.
|
||||
set -Eeuo pipefail
|
||||
|
||||
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
workflow="$root/.gitea/workflows/ci.yml"
|
||||
publish_workflow="$root/.gitea/workflows/publish-release.yml"
|
||||
failures=0
|
||||
check() {
|
||||
if eval "$1"; then
|
||||
@@ -31,13 +32,67 @@ own="$(line_of 'ci-sweep.sh --job')"
|
||||
last_step="$(grep -n -- " - name:" <<<"$job" | tail -1 | cut -d: -f1 || true)"
|
||||
|
||||
check '[[ -n "$job" ]]' "the workflow has an integration job"
|
||||
check '! grep -q "if: false" <<<"$job"' "the integration job is enabled"
|
||||
check 'grep -q "EKKLESIA_CI_JOB_KEY:" <<<"$job"' "the integration job names its stacks with its own key"
|
||||
check '[[ -n "$stale" && -n "$run" ]] && ((stale < run))' "it sweeps stale CI stacks before it starts anything"
|
||||
check '[[ -n "$own" && -n "$run" ]] && ((own > run)) && ((own > last_step))' \
|
||||
"its last step removes its own stacks"
|
||||
check 'sed -n "${last_step},\$p" <<<"$job" | grep -q "if: always()"' "that last step always runs"
|
||||
check 'grep -q "EKKLESIA_CI_LABEL" "$root/ci/integration.sh" && grep -q "EKKLESIA_CI_CREATED_LABEL" "$root/ci/integration.sh"' \
|
||||
"the image ci/integration.sh builds carries the CI labels"
|
||||
check 'grep -q "EKKLESIA_SMOKE_MODULE_RELEASE_DIR" "$root/ci/integration.sh" && grep -q "bash scripts/smoke-deploy.sh" "$root/ci/integration.sh"' \
|
||||
"the integration delegates its external package to the platform smoke"
|
||||
check 'grep -Eq "^EKKLESIA_PLATFORM_REF=refs/(pull/[0-9]+/head|heads/main)$" "$root/ci/platform.env" && grep -Eq "^EKKLESIA_PLATFORM_COMMIT=[0-9a-f]{40}$" "$root/ci/platform.env"' \
|
||||
"the platform harness is pinned by full ref and commit"
|
||||
check 'grep -q "rev-parse refs/remotes/pinned" "$root/ci/fetch-platform.sh" && grep -q "does not resolve to EKKLESIA_PLATFORM_COMMIT" "$root/ci/fetch-platform.sh"' \
|
||||
"the fetch verifies that the pinned ref resolves to the pinned commit"
|
||||
|
||||
# Normal CI builds the canonical archive once and hands those exact bytes to integration through a
|
||||
# Gitea Actions v3 artifact. It must contain no release trigger, permission, or mutation path.
|
||||
package_job="$(awk '/^ package:$/ { inside = 1; print; next } inside && /^ [a-z][a-z-]*:$/ { exit } inside { print }' "$workflow")"
|
||||
check 'grep -q "pull_request:" "$workflow" && grep -q "push:" "$workflow"' \
|
||||
"normal CI still runs for pull requests and main pushes"
|
||||
check '! grep -q "workflow_dispatch:" "$workflow" && ! grep -q "tags:" "$workflow"' \
|
||||
"normal CI has no manual-publish or tag trigger"
|
||||
check '! grep -q "contents: write" "$workflow" && ! grep -q "github.token" "$workflow"' \
|
||||
"normal CI has no release-specific permission or token"
|
||||
check '! grep -q "release-assets.py publish" "$workflow" && ! grep -q "ekklesia-corresponding-source" "$workflow"' \
|
||||
"normal CI cannot publish or prepare release assets"
|
||||
check 'grep -q "bash ci/package.sh" <<<"$package_job" && grep -q "actions/upload-artifact@c6a3b2bd78b3985e4b2f15397fec357f0fd808de" <<<"$package_job"' \
|
||||
"the package job builds and transfers one canonical artifact"
|
||||
check 'grep -q "actions/download-artifact@ad191675b41f6a5b46da9a048cb6893812da158b" <<<"$job" && ! grep -Eq "ci/package.sh|ekklesia-pack-module" <<<"$job"' \
|
||||
"integration downloads that artifact and never rebuilds it"
|
||||
check 'grep -q "needs.package.outputs.sha256" <<<"$job" && grep -q "GITHUB_OUTPUT" "$root/ci/package.sh"' \
|
||||
"integration binds the downloaded bytes to the package job's digest"
|
||||
|
||||
# Publishing is an explicit operator action. One job validates its dispatch input and selected ref,
|
||||
# builds the package exactly once, integration-tests that directory, prepares redistribution files,
|
||||
# and only then gives the same directory to the Gitea publisher.
|
||||
gate_job="$(awk '/^ release-gate:$/ { inside = 1; print; next } inside && /^ [a-z][a-z-]*:$/ { exit } inside { print }' "$publish_workflow")"
|
||||
publish_job="$(awk '/^ publish:$/ { inside = 1; print; next } inside && /^ [a-z][a-z-]*:$/ { exit } inside { print }' "$publish_workflow")"
|
||||
check 'grep -q "workflow_dispatch:" "$publish_workflow" && ! grep -q "pull_request:" "$publish_workflow" && ! grep -q "push:" "$publish_workflow"' \
|
||||
"publishing is workflow_dispatch only and cannot run for a PR or push"
|
||||
check 'grep -q "inputs:" "$publish_workflow" && grep -q " version:" "$publish_workflow" && grep -q "required: true" "$publish_workflow"' \
|
||||
"manual publishing requires an explicit version input"
|
||||
check 'grep -q "refs/heads/main" <<<"$gate_job" && grep -q "packageVersion.*requested" <<<"$gate_job" && grep -q "moduleVersion.*requested" <<<"$gate_job"' \
|
||||
"the publish gate requires selected main and validates both source versions"
|
||||
check '[[ "$(grep -c "bash ci/package.sh" "$publish_workflow")" -eq 1 ]]' \
|
||||
"the publish workflow builds the canonical Module Package exactly once"
|
||||
check 'grep -q "ci/integration.sh.*build/release-assets" <<<"$gate_job" && grep -q "name: signage-release-assets" <<<"$gate_job" && grep -q "name: signage-release-assets" <<<"$publish_job" && grep -q "release-assets.py publish" <<<"$publish_job"' \
|
||||
"integration proves the package handed unchanged to the publisher"
|
||||
check 'grep -q "EXPECTED_PACKAGE_SHA256:.*steps.canonical.outputs.sha256" <<<"$gate_job" && grep -q "EXPECTED_PACKAGE_SHA256:.*needs.release-gate.outputs.sha256" <<<"$publish_job"' \
|
||||
"the publish gate binds consumers to the canonical package digest"
|
||||
check 'grep -q "ekklesia-corresponding-source verify" <<<"$gate_job" && grep -q "needs: release-gate" <<<"$publish_job"' \
|
||||
"corresponding-source verification finishes before the publishing job"
|
||||
check 'grep -q "GITHUB_TOKEN:.*github.token" <<<"$publish_job" && grep -q "contents: write" <<<"$publish_job" && ! grep -q "contents: write" <<<"$gate_job"' \
|
||||
"only the final manual publishing job receives release write authority"
|
||||
|
||||
# The manual workflow also owns a Docker integration stack, so it follows the same cleanup rule.
|
||||
publish_stale="$(grep -n -F -- 'ci-sweep.sh --stale' <<<"$gate_job" | cut -d: -f1 || true)"
|
||||
publish_run="$(grep -n -F -- 'bash ci/integration.sh' <<<"$gate_job" | cut -d: -f1 || true)"
|
||||
publish_own="$(grep -n -F -- 'ci-sweep.sh --job' <<<"$gate_job" | cut -d: -f1 || true)"
|
||||
check '[[ -n "$publish_stale" && -n "$publish_run" ]] && ((publish_stale < publish_run))' \
|
||||
"manual publishing sweeps stale stacks before integration"
|
||||
check '[[ -n "$publish_own" && -n "$publish_run" ]] && ((publish_own > publish_run))' \
|
||||
"manual publishing always has a post-integration cleanup step"
|
||||
|
||||
((failures == 0)) || {
|
||||
echo "check-cleanup: ${failures} check(s) failed." >&2
|
||||
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fetches the exact Ekklesia source revision Signage's external-package integration runs against.
|
||||
# The ref is fetched only to make the pinned object reachable; the checkout is always by full SHA.
|
||||
set -Eeuo pipefail
|
||||
|
||||
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
out="$root/build/platform"
|
||||
|
||||
fail() {
|
||||
echo "fetch-platform: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
(($# == 0)) || fail "this command takes no arguments."
|
||||
|
||||
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)"
|
||||
ref="$(pin EKKLESIA_PLATFORM_REF)"
|
||||
commit="$(pin EKKLESIA_PLATFORM_COMMIT)"
|
||||
[[ "$commit" =~ ^[0-9a-f]{40}$ ]] || fail "EKKLESIA_PLATFORM_COMMIT must be a full commit SHA."
|
||||
[[ "$ref" == refs/* ]] || fail "EKKLESIA_PLATFORM_REF must be a full Git ref."
|
||||
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
git -C "$out" init --quiet
|
||||
git -C "$out" remote add origin "$repository"
|
||||
git -C "$out" fetch --quiet --no-tags --depth=1 origin "+${ref}:refs/remotes/pinned" ||
|
||||
fail "cannot fetch the pinned platform ref."
|
||||
[[ "$(git -C "$out" rev-parse refs/remotes/pinned)" == "$commit" ]] ||
|
||||
fail "the pinned ref does not resolve to EKKLESIA_PLATFORM_COMMIT."
|
||||
git -C "$out" -c advice.detachedHead=false checkout --quiet --detach "$commit"
|
||||
rm -rf "$out/.git"
|
||||
echo "fetch-platform: Ekklesia ${commit}"
|
||||
+33
-26
@@ -1,46 +1,53 @@
|
||||
#!/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.
|
||||
# Gives the canonical linux-x64-glibc Module Package pair from the package job to the pinned
|
||||
# platform's generic deployment smoke. The platform image contains no Signage source:
|
||||
# its verifier extracts the package into installation-owned module storage before migrations,
|
||||
# discovery, browser UI/display checks, restart, container recreation, disable/re-enable, removal,
|
||||
# and reinstall are exercised against disposable PostgreSQL and object-storage services.
|
||||
#
|
||||
# bash ci/package.sh
|
||||
# bash ci/fetch-platform.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: Ekklesia's deployment tree is not at build/workspace." >&2
|
||||
platform="$root/build/platform"
|
||||
release="${1:-$root/build/module-package}"
|
||||
[[ -f "$platform/scripts/smoke-deploy.sh" ]] || {
|
||||
echo "integration: run bash ci/fetch-platform.sh first." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Named and labeled as a CI stack of this run and job (the platform's scripts/lib/ci-project.sh), so
|
||||
# the workflow's final `ci-sweep.sh --job` removes it even if this script is killed before its trap.
|
||||
# shellcheck source=/dev/null
|
||||
source "$workspace/scripts/lib/ci-project.sh"
|
||||
project="$(ci_project_name signage-image)"
|
||||
image="${project}:latest"
|
||||
trap 'docker image rm --force "$image" >/dev/null 2>&1 || true' EXIT
|
||||
mapfile -t archives < <(find "$release" -maxdepth 1 -type f -name 'signage-*-linux-x64-glibc.tar.gz' -print)
|
||||
[[ "${#archives[@]}" -eq 1 ]] || {
|
||||
echo "integration: expected exactly one prebuilt Module Package archive." >&2
|
||||
exit 1
|
||||
}
|
||||
archive="${archives[0]}"
|
||||
checksum="$archive.sha256"
|
||||
[[ -f "$checksum" ]] || {
|
||||
echo "integration: missing $(basename "$checksum")." >&2
|
||||
exit 1
|
||||
}
|
||||
(cd "$release" && sha256sum --check --strict "$(basename "$checksum")")
|
||||
digest="$(sha256sum "$archive" | cut -d' ' -f1)"
|
||||
if [[ -n "${EXPECTED_PACKAGE_SHA256:-}" && "$digest" != "$EXPECTED_PACKAGE_SHA256" ]]; then
|
||||
echo "integration: prebuilt package sha256 $digest differs from the package job's $EXPECTED_PACKAGE_SHA256." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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
|
||||
}
|
||||
echo "integration: consuming prebuilt $(basename "$archive"); scenarios: ${scenarios}"
|
||||
|
||||
DOCKER_BUILDKIT=1 docker build --quiet --file "$workspace/deploy/Dockerfile" --tag "$image" \
|
||||
--label "${EKKLESIA_CI_LABEL}=${project}" --label "${EKKLESIA_CI_CREATED_LABEL}=$(date +%s)" \
|
||||
"$workspace" >/dev/null
|
||||
echo "integration: built the deployment image with Signage; scenarios: ${scenarios}"
|
||||
|
||||
cd "$workspace"
|
||||
cd "$platform"
|
||||
EKKLESIA_SMOKE_TOPOLOGY=split \
|
||||
EKKLESIA_SMOKE_IMAGE="$image" \
|
||||
EKKLESIA_SMOKE_MODULE_RELEASE_DIR="$release" \
|
||||
EKKLESIA_SMOKE_MODULE_ID=signage \
|
||||
EKKLESIA_SMOKE_MODULE_PROBE_PATH=/api/v1/modules/signage/channel-groups \
|
||||
EKKLESIA_SMOKE_DISPLAY_DEVICE=1 \
|
||||
EKKLESIA_SMOKE_DISPLAY_SCENARIOS="$scenarios" \
|
||||
EKKLESIA_SMOKE_WATCH_MS=120000 \
|
||||
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the one canonical linux-x64-glibc Module Package pair that later jobs must consume.
|
||||
set -Eeuo pipefail
|
||||
|
||||
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
release="${1:-$root/build/module-package}"
|
||||
source_revision="${GITHUB_SHA:-$(git -C "$root" rev-parse HEAD 2>/dev/null || true)}"
|
||||
[[ "$source_revision" =~ ^[0-9a-f]{40}$ ]] || {
|
||||
echo "package: no full source revision is available." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
rm -rf -- "$release"
|
||||
mkdir -p -- "$release"
|
||||
pnpm --dir "$root" build
|
||||
pnpm --dir "$root" exec ekklesia-pack-module --module "$root" --target linux-x64-glibc \
|
||||
--out "$release" --license-file "$root/LICENSE" --source-revision "$source_revision"
|
||||
|
||||
mapfile -t archives < <(find "$release" -maxdepth 1 -type f -name 'signage-*-linux-x64-glibc.tar.gz' -print)
|
||||
[[ "${#archives[@]}" -eq 1 ]] || {
|
||||
echo "package: expected exactly one Module Package archive." >&2
|
||||
exit 1
|
||||
}
|
||||
archive="${archives[0]}"
|
||||
checksum="$archive.sha256"
|
||||
[[ -f "$checksum" ]] || {
|
||||
echo "package: missing $(basename "$checksum")." >&2
|
||||
exit 1
|
||||
}
|
||||
(cd "$release" && sha256sum --check --strict "$(basename "$checksum")")
|
||||
digest="$(sha256sum "$archive" | cut -d' ' -f1)"
|
||||
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
|
||||
echo "sha256=$digest" >>"$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "package: canonical pair is $(basename "$archive") and $(basename "$checksum")."
|
||||
@@ -0,0 +1,5 @@
|
||||
# The companion Core harness Signage uses to install and run its real external package. The pull ref
|
||||
# remains reachable after its branch is deleted; the full commit pin makes the checkout immutable.
|
||||
EKKLESIA_PLATFORM_REPOSITORY=https://git.riverofglorymn.org/River_of_Glory_Church/Ekklesia.git
|
||||
EKKLESIA_PLATFORM_REF=refs/pull/90/head
|
||||
EKKLESIA_PLATFORM_COMMIT=40ef0ff802c124ae5a3a0b9aa51a1502a7b72644
|
||||
Executable
+263
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate and publish one tested Signage release bundle through the Gitea API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tarfile
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ARCHIVE = re.compile(r"^signage-(\d+\.\d+\.\d+)-linux-x64-glibc\.tar\.gz$")
|
||||
|
||||
|
||||
class Refused(Exception):
|
||||
"""The release does not meet a condition that must fail closed."""
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def package_metadata(archive: Path) -> dict:
|
||||
try:
|
||||
with tarfile.open(archive, "r:gz") as bundle:
|
||||
member = next(
|
||||
entry for entry in bundle.getmembers()
|
||||
if entry.name == "signage/ekklesia.package.json" and entry.isfile()
|
||||
)
|
||||
extracted = bundle.extractfile(member)
|
||||
if extracted is None:
|
||||
raise KeyError(member.name)
|
||||
return json.loads(extracted.read())
|
||||
except (KeyError, StopIteration, tarfile.TarError, json.JSONDecodeError) as error:
|
||||
raise Refused(f"cannot read signage/ekklesia.package.json from {archive.name}") from error
|
||||
|
||||
|
||||
def package_pair(directory: Path, tag: str, revision: str) -> list[Path]:
|
||||
archives = [path for path in directory.iterdir() if path.is_file() and ARCHIVE.fullmatch(path.name)]
|
||||
if len(archives) != 1:
|
||||
raise Refused(f"expected one Signage Module Package, found {len(archives)}")
|
||||
|
||||
package = archives[0]
|
||||
match = ARCHIVE.fullmatch(package.name)
|
||||
assert match is not None
|
||||
version = match.group(1)
|
||||
if tag != f"v{version}":
|
||||
raise Refused(f"tag {tag} does not match package version {version}")
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", revision):
|
||||
raise Refused("release revision is not a full Git object ID")
|
||||
|
||||
checksum = directory / f"{package.name}.sha256"
|
||||
expected = [package, checksum]
|
||||
missing = [path.name for path in expected if not path.is_file()]
|
||||
if missing:
|
||||
raise Refused(f"Module Package pair is incomplete; missing={missing}")
|
||||
package_digest = sha256(package)
|
||||
checksum_line = checksum.read_text(encoding="utf-8")
|
||||
wanted_line = f"{package_digest} {package.name}\n"
|
||||
if checksum_line != wanted_line:
|
||||
raise Refused(f"{checksum.name} does not match {package.name}")
|
||||
built_digest = os.environ.get("EXPECTED_PACKAGE_SHA256")
|
||||
if built_digest is not None and built_digest != package_digest:
|
||||
raise Refused("selected Module Package differs from the package job's bytes")
|
||||
metadata = package_metadata(package)
|
||||
if metadata.get("module") != {"id": "signage", "version": version}:
|
||||
raise Refused("package metadata does not identify the expected Signage version")
|
||||
if metadata.get("provenance", {}).get("revision") != revision:
|
||||
raise Refused("package provenance revision does not match the tag commit")
|
||||
|
||||
return expected
|
||||
|
||||
|
||||
def expected_paths(directory: Path, tag: str, revision: str) -> list[Path]:
|
||||
package, checksum = package_pair(directory, tag, revision)
|
||||
package_digest = sha256(package)
|
||||
match = ARCHIVE.fullmatch(package.name)
|
||||
assert match is not None
|
||||
version = match.group(1)
|
||||
source = directory / f"signage-{version}-linux-x64-glibc-corresponding-source.tar.gz"
|
||||
source_checksum = directory / f"{source.name}.sha256"
|
||||
redistribution = directory / f"signage-{version}-linux-x64-glibc.redistribution.json"
|
||||
notice = directory / f"signage-{version}-linux-x64-glibc.source-notice.txt"
|
||||
expected = [package, checksum, source, source_checksum, redistribution, notice]
|
||||
|
||||
missing = [path.name for path in expected if not path.is_file()]
|
||||
extras = sorted(path.name for path in directory.iterdir() if path.is_file() and path not in expected)
|
||||
if missing or extras:
|
||||
raise Refused(f"release bundle differs from the six required files; missing={missing}, extra={extras}")
|
||||
|
||||
source_line = source_checksum.read_text(encoding="utf-8")
|
||||
wanted_source_line = f"{sha256(source)} {source.name}\n"
|
||||
if source_line != wanted_source_line:
|
||||
raise Refused(f"{source_checksum.name} does not match {source.name}")
|
||||
|
||||
binding = json.loads(redistribution.read_text(encoding="utf-8"))
|
||||
package_binding = binding.get("package", {})
|
||||
if package_binding.get("file") != package.name or package_binding.get("sha256") != package_digest:
|
||||
raise Refused("redistribution metadata does not bind the selected Module Package")
|
||||
corresponding = binding.get("correspondingSource", {})
|
||||
if corresponding.get("file") != source.name or corresponding.get("sha256") != sha256(source):
|
||||
raise Refused("redistribution metadata does not bind the selected corresponding source")
|
||||
|
||||
return expected
|
||||
|
||||
|
||||
class Gitea:
|
||||
def __init__(self) -> None:
|
||||
self.server = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/")
|
||||
self.repository = os.environ.get("GITHUB_REPOSITORY", "")
|
||||
self.token = os.environ.get("GITHUB_TOKEN", "")
|
||||
if not self.server or not self.repository or not self.token:
|
||||
raise Refused("GITHUB_SERVER_URL, GITHUB_REPOSITORY, and GITHUB_TOKEN are required")
|
||||
|
||||
def request(self, method: str, path: str, body: bytes | None = None,
|
||||
headers: dict[str, str] | None = None, *, authenticated: bool = True,
|
||||
missing_ok: bool = False) -> bytes | None:
|
||||
target = path if path.startswith("http") else f"{self.server}/api/v1/repos/{self.repository}{path}"
|
||||
request_headers = dict(headers or {})
|
||||
if authenticated:
|
||||
request_headers["Authorization"] = f"token {self.token}"
|
||||
request = urllib.request.Request(target, data=body, method=method, headers=request_headers)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=120) as response:
|
||||
return response.read()
|
||||
except urllib.error.HTTPError as error:
|
||||
if missing_ok and error.code == 404:
|
||||
return None
|
||||
detail = error.read()[:300].decode("utf-8", "replace")
|
||||
raise Refused(f"{method} {path} answered {error.code}: {detail}") from error
|
||||
|
||||
def json(self, method: str, path: str, body: object | None = None,
|
||||
*, missing_ok: bool = False) -> object:
|
||||
encoded = json.dumps(body).encode() if body is not None else None
|
||||
headers = {"Content-Type": "application/json"} if encoded is not None else None
|
||||
data = self.request(method, path, encoded, headers, missing_ok=missing_ok)
|
||||
return json.loads(data) if data is not None else None
|
||||
|
||||
def public_download(self, url: str) -> bytes:
|
||||
return self.request("GET", url, authenticated=False)
|
||||
|
||||
|
||||
def publish(directory: Path, tag: str, revision: str) -> None:
|
||||
# This validates every local byte before the first API request, and therefore before creating or
|
||||
# mutating a release. Corresponding-source policy verification happens in the workflow first.
|
||||
files = expected_paths(directory, tag, revision)
|
||||
gitea = Gitea()
|
||||
quoted_tag = urllib.parse.quote(tag, safe="")
|
||||
release = gitea.json("GET", f"/releases/tags/{quoted_tag}", missing_ok=True)
|
||||
tagged = gitea.json("GET", f"/tags/{quoted_tag}", missing_ok=True)
|
||||
if tagged is not None and (
|
||||
not isinstance(tagged, dict) or tagged.get("commit", {}).get("sha") != revision
|
||||
):
|
||||
raise Refused(f"existing tag {tag} does not name {revision}")
|
||||
if release is not None and tagged is None:
|
||||
raise Refused(f"release {tag} exists without a readable tag")
|
||||
if release is None:
|
||||
release = gitea.json("POST", "/releases", {
|
||||
"tag_name": tag,
|
||||
# A full immutable commit, never a moving branch, is the release's tag target.
|
||||
"target_commitish": revision,
|
||||
"name": f"Ekklesia Signage {tag}",
|
||||
"body": (
|
||||
"Published by the manual Publish Signage Release workflow after package, "
|
||||
"external integration, and corresponding-source verification."
|
||||
),
|
||||
# Existing Signage 0.x releases use Gitea's prerelease convention.
|
||||
"draft": False,
|
||||
"prerelease": True,
|
||||
})
|
||||
if not isinstance(release, dict):
|
||||
raise Refused(f"Gitea did not return the created release {tag}")
|
||||
tagged = gitea.json("GET", f"/tags/{quoted_tag}")
|
||||
if not isinstance(tagged, dict) or tagged.get("commit", {}).get("sha") != revision:
|
||||
raise Refused(f"created tag {tag} does not name {revision}")
|
||||
elif not isinstance(release, dict):
|
||||
raise Refused(f"cannot read release {tag}")
|
||||
|
||||
release_id = release.get("id")
|
||||
if not isinstance(release_id, int):
|
||||
raise Refused(f"release {tag} has no numeric id")
|
||||
listed = gitea.json("GET", f"/releases/{release_id}/assets")
|
||||
present = {asset["name"]: asset for asset in listed or []}
|
||||
unexpected = sorted(set(present) - {path.name for path in files})
|
||||
if unexpected:
|
||||
raise Refused(f"release already has unexpected assets: {unexpected}")
|
||||
|
||||
# Preflight every published name before changing the release. This prevents a later conflict
|
||||
# from leaving newly uploaded earlier files beside immutable, different bytes.
|
||||
verified: dict[str, str] = {}
|
||||
for path in files:
|
||||
asset = present.get(path.name)
|
||||
if asset is None:
|
||||
continue
|
||||
public_digest = hashlib.sha256(gitea.public_download(asset["browser_download_url"])).hexdigest()
|
||||
expected_digest = sha256(path)
|
||||
if public_digest != expected_digest:
|
||||
raise Refused(f"public {path.name} has sha256 {public_digest}, expected {expected_digest}")
|
||||
verified[path.name] = public_digest
|
||||
|
||||
for path in files:
|
||||
digest = sha256(path)
|
||||
asset = present.get(path.name)
|
||||
if asset is None:
|
||||
boundary = uuid.uuid4().hex
|
||||
body = (
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="attachment"; filename="{path.name}"\r\n'
|
||||
"Content-Type: application/octet-stream\r\n\r\n"
|
||||
).encode() + path.read_bytes() + f"\r\n--{boundary}--\r\n".encode()
|
||||
gitea.request(
|
||||
"POST",
|
||||
f"/releases/{release_id}/assets?name={urllib.parse.quote(path.name)}",
|
||||
body,
|
||||
{"Content-Type": f"multipart/form-data; boundary={boundary}"},
|
||||
)
|
||||
listed = gitea.json("GET", f"/releases/{release_id}/assets")
|
||||
asset = next((item for item in listed or [] if item.get("name") == path.name), None)
|
||||
if asset is None:
|
||||
raise Refused(f"Gitea did not list {path.name} after upload")
|
||||
|
||||
public_digest = verified.get(path.name)
|
||||
if public_digest is None:
|
||||
public_digest = hashlib.sha256(gitea.public_download(asset["browser_download_url"])).hexdigest()
|
||||
if public_digest != digest:
|
||||
raise Refused(f"public {path.name} has sha256 {public_digest}, expected {digest}")
|
||||
print(f"release-assets: public {path.name}, sha256 {digest}", flush=True)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
try:
|
||||
if len(argv) != 4 or argv[0] not in {"verify-package", "verify", "publish"}:
|
||||
raise Refused(
|
||||
"usage: release-assets.py (verify-package|verify|publish) DIRECTORY TAG REVISION"
|
||||
)
|
||||
command, directory, tag, revision = argv
|
||||
selected = (package_pair(Path(directory), tag, revision)
|
||||
if command == "verify-package"
|
||||
else expected_paths(Path(directory), tag, revision))
|
||||
if command in {"verify-package", "verify"}:
|
||||
for path in selected:
|
||||
print(path)
|
||||
else:
|
||||
publish(Path(directory), tag, revision)
|
||||
return 0
|
||||
except (OSError, json.JSONDecodeError, Refused) as error:
|
||||
print(f"release-assets: REFUSED: {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
Executable
+253
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Release selection and Gitea upload tests; no real server or credential is used."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import hashlib
|
||||
import http.server
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import socketserver
|
||||
import tarfile
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
SPEC = importlib.util.spec_from_file_location("release_assets", HERE / "release-assets.py")
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
release_assets = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(release_assets)
|
||||
|
||||
VERSION = "1.2.3"
|
||||
REVISION = "a" * 40
|
||||
|
||||
|
||||
def digest(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def release_bundle(directory: Path) -> list[Path]:
|
||||
package = directory / f"signage-{VERSION}-linux-x64-glibc.tar.gz"
|
||||
metadata = json.dumps({
|
||||
"module": {"id": "signage", "version": VERSION},
|
||||
"provenance": {"revision": REVISION},
|
||||
}).encode()
|
||||
with tarfile.open(package, "w:gz") as archive:
|
||||
member = tarfile.TarInfo("signage/ekklesia.package.json")
|
||||
member.size = len(metadata)
|
||||
archive.addfile(member, io.BytesIO(metadata))
|
||||
|
||||
checksum = directory / f"{package.name}.sha256"
|
||||
checksum.write_text(f"{release_assets.sha256(package)} {package.name}\n", encoding="utf-8")
|
||||
source = directory / f"signage-{VERSION}-linux-x64-glibc-corresponding-source.tar.gz"
|
||||
source.write_bytes(b"corresponding source")
|
||||
source_checksum = directory / f"{source.name}.sha256"
|
||||
source_checksum.write_text(f"{release_assets.sha256(source)} {source.name}\n", encoding="utf-8")
|
||||
redistribution = directory / f"signage-{VERSION}-linux-x64-glibc.redistribution.json"
|
||||
redistribution.write_text(json.dumps({
|
||||
"package": {"file": package.name, "sha256": release_assets.sha256(package)},
|
||||
"correspondingSource": {"file": source.name, "sha256": release_assets.sha256(source)},
|
||||
}), encoding="utf-8")
|
||||
notice = directory / f"signage-{VERSION}-linux-x64-glibc.source-notice.txt"
|
||||
notice.write_text("source notice\n", encoding="utf-8")
|
||||
return [package, checksum, source, source_checksum, redistribution, notice]
|
||||
|
||||
|
||||
class FakeGitea(http.server.BaseHTTPRequestHandler):
|
||||
assets: dict[str, bytes] = {}
|
||||
release_exists = True
|
||||
tag_revision: str | None = REVISION
|
||||
api_authorizations: list[str | None] = []
|
||||
public_authorizations: list[str | None] = []
|
||||
asset_posts = 0
|
||||
release_posts = 0
|
||||
release_request: dict[str, object] | None = None
|
||||
|
||||
def log_message(self, _format: str, *_args: object) -> None:
|
||||
pass
|
||||
|
||||
def answer(self, status: int, body: object = None) -> None:
|
||||
data = b"" if body is None else json.dumps(body).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def asset_list(self) -> list[dict[str, object]]:
|
||||
server = self.server
|
||||
assert isinstance(server, socketserver.TCPServer)
|
||||
host, port = server.server_address
|
||||
return [
|
||||
{"id": index, "name": name, "browser_download_url": f"http://{host}:{port}/public/{name}"}
|
||||
for index, name in enumerate(self.assets, 1)
|
||||
]
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler's API
|
||||
if self.path.startswith("/public/"):
|
||||
self.public_authorizations.append(self.headers.get("Authorization"))
|
||||
name = urllib.parse.unquote(self.path.removeprefix("/public/"))
|
||||
data = self.assets[name]
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
return
|
||||
|
||||
self.api_authorizations.append(self.headers.get("Authorization"))
|
||||
if "/releases/tags/" in self.path:
|
||||
if self.release_exists:
|
||||
self.answer(200, {"id": 7, "tag_name": f"v{VERSION}"})
|
||||
else:
|
||||
self.answer(404, {"message": "not found"})
|
||||
elif "/tags/" in self.path:
|
||||
if self.tag_revision is not None:
|
||||
self.answer(200, {"commit": {"sha": self.tag_revision}})
|
||||
else:
|
||||
self.answer(404, {"message": "not found"})
|
||||
elif self.path.endswith("/releases/7/assets"):
|
||||
self.answer(200, self.asset_list())
|
||||
else:
|
||||
self.answer(404, {"message": "not found"})
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802 - BaseHTTPRequestHandler's API
|
||||
self.api_authorizations.append(self.headers.get("Authorization"))
|
||||
length = int(self.headers["Content-Length"])
|
||||
body = self.rfile.read(length)
|
||||
if self.path.endswith("/releases"):
|
||||
request = json.loads(body)
|
||||
self.__class__.release_posts += 1
|
||||
self.__class__.release_request = request
|
||||
self.__class__.release_exists = True
|
||||
self.__class__.tag_revision = request["target_commitish"]
|
||||
self.answer(201, {"id": 7, "tag_name": request["tag_name"]})
|
||||
return
|
||||
|
||||
self.__class__.asset_posts += 1
|
||||
name = urllib.parse.parse_qs(urllib.parse.urlsplit(self.path).query)["name"][0]
|
||||
boundary = self.headers["Content-Type"].split("boundary=", 1)[1].encode()
|
||||
payload = body.split(b"\r\n\r\n", 1)[1].rsplit(b"\r\n--" + boundary + b"--", 1)[0]
|
||||
self.assets[name] = payload
|
||||
self.answer(201, {"name": name})
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def fake_gitea(initial: dict[str, bytes] | None = None, *, release_exists: bool = True,
|
||||
tag_revision: str | None = REVISION):
|
||||
FakeGitea.assets = dict(initial or {})
|
||||
FakeGitea.release_exists = release_exists
|
||||
FakeGitea.tag_revision = tag_revision
|
||||
FakeGitea.api_authorizations = []
|
||||
FakeGitea.public_authorizations = []
|
||||
FakeGitea.asset_posts = 0
|
||||
FakeGitea.release_posts = 0
|
||||
FakeGitea.release_request = None
|
||||
with socketserver.TCPServer(("127.0.0.1", 0), FakeGitea) as server:
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
host, port = server.server_address
|
||||
old = {name: os.environ.get(name) for name in ("GITHUB_SERVER_URL", "GITHUB_REPOSITORY", "GITHUB_TOKEN")}
|
||||
os.environ.update({
|
||||
"GITHUB_SERVER_URL": f"http://{host}:{port}",
|
||||
"GITHUB_REPOSITORY": "owner/repository",
|
||||
"GITHUB_TOKEN": "test-token",
|
||||
})
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join()
|
||||
for name, value in old.items():
|
||||
if value is None:
|
||||
os.environ.pop(name, None)
|
||||
else:
|
||||
os.environ[name] = value
|
||||
|
||||
|
||||
class ReleaseAssetsTest(unittest.TestCase):
|
||||
def test_selects_only_the_complete_bound_bundle(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
expected = release_bundle(directory)
|
||||
self.assertEqual(release_assets.expected_paths(directory, f"v{VERSION}", REVISION), expected)
|
||||
|
||||
(directory / "unexpected.txt").write_text("no", encoding="utf-8")
|
||||
with self.assertRaisesRegex(release_assets.Refused, "six required files"):
|
||||
release_assets.expected_paths(directory, f"v{VERSION}", REVISION)
|
||||
|
||||
def test_refuses_wrong_tag_revision_and_checksum(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
paths = release_bundle(directory)
|
||||
with self.assertRaisesRegex(release_assets.Refused, "tag"):
|
||||
release_assets.expected_paths(directory, "v9.9.9", REVISION)
|
||||
with self.assertRaisesRegex(release_assets.Refused, "provenance"):
|
||||
release_assets.expected_paths(directory, f"v{VERSION}", "b" * 40)
|
||||
with mock.patch.dict(os.environ, {"EXPECTED_PACKAGE_SHA256": "0" * 64}):
|
||||
with self.assertRaisesRegex(release_assets.Refused, "package job's bytes"):
|
||||
release_assets.expected_paths(directory, f"v{VERSION}", REVISION)
|
||||
paths[1].write_text(f"{'0' * 64} {paths[0].name}\n", encoding="utf-8")
|
||||
with self.assertRaisesRegex(release_assets.Refused, "does not match"):
|
||||
release_assets.expected_paths(directory, f"v{VERSION}", REVISION)
|
||||
|
||||
def test_creates_release_uploads_each_byte_and_reads_it_back_anonymously(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
expected = release_bundle(directory)
|
||||
with fake_gitea(release_exists=False, tag_revision=None):
|
||||
release_assets.publish(directory, f"v{VERSION}", REVISION)
|
||||
self.assertEqual(FakeGitea.release_posts, 1)
|
||||
self.assertEqual(FakeGitea.release_request, {
|
||||
"tag_name": f"v{VERSION}",
|
||||
"target_commitish": REVISION,
|
||||
"name": f"Ekklesia Signage v{VERSION}",
|
||||
"body": (
|
||||
"Published by the manual Publish Signage Release workflow after package, "
|
||||
"external integration, and corresponding-source verification."
|
||||
),
|
||||
"draft": False,
|
||||
"prerelease": True,
|
||||
})
|
||||
self.assertEqual(FakeGitea.asset_posts, 6)
|
||||
self.assertEqual(FakeGitea.assets, {path.name: path.read_bytes() for path in expected})
|
||||
self.assertTrue(all(value == "token test-token" for value in FakeGitea.api_authorizations))
|
||||
self.assertEqual(FakeGitea.public_authorizations, [None] * 6)
|
||||
|
||||
def test_existing_identical_assets_are_idempotent_but_conflicts_fail(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
expected = release_bundle(directory)
|
||||
initial = {path.name: path.read_bytes() for path in expected}
|
||||
with fake_gitea(initial):
|
||||
release_assets.publish(directory, f"v{VERSION}", REVISION)
|
||||
self.assertEqual(FakeGitea.release_posts, 0)
|
||||
self.assertEqual(FakeGitea.asset_posts, 0)
|
||||
|
||||
conflicting = {expected[1].name: b"different published bytes"}
|
||||
with fake_gitea(conflicting):
|
||||
with self.assertRaisesRegex(release_assets.Refused, "public .* expected"):
|
||||
release_assets.publish(directory, f"v{VERSION}", REVISION)
|
||||
self.assertEqual(FakeGitea.release_posts, 0)
|
||||
self.assertEqual(FakeGitea.asset_posts, 0)
|
||||
|
||||
def test_refuses_existing_tag_at_another_revision_before_mutation(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
release_bundle(directory)
|
||||
with fake_gitea(release_exists=False, tag_revision="b" * 40):
|
||||
with self.assertRaisesRegex(release_assets.Refused, "existing tag"):
|
||||
release_assets.publish(directory, f"v{VERSION}", REVISION)
|
||||
self.assertEqual(FakeGitea.release_posts, 0)
|
||||
self.assertEqual(FakeGitea.asset_posts, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -3,7 +3,7 @@
|
||||
"id": "signage",
|
||||
"name": "Signage",
|
||||
"description": "Visual programming, timelines, synchronized channels, and display delivery for church screens.",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"compatibility": {
|
||||
"platform": "^0.2.0",
|
||||
"moduleSdk": "^0.2.0"
|
||||
|
||||
+11
-10
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@ekklesia/signage",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"type": "module",
|
||||
"packageManager": "[email protected]",
|
||||
"engines": {
|
||||
@@ -33,21 +34,21 @@
|
||||
"validate": "node scripts/validate-module.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@ekklesia/design-tokens": "^0.2.3",
|
||||
"@ekklesia/module-database": "^0.2.3",
|
||||
"@ekklesia/module-sdk": "^0.2.3",
|
||||
"@ekklesia/module-ui-sdk": "^0.2.3",
|
||||
"@ekklesia/ui": "^0.2.3",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8",
|
||||
"sharp": "0.35.4"
|
||||
"sharp": "0.35.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.3",
|
||||
"@ekklesia/module-pack": "0.2.0",
|
||||
"@ekklesia/module-ui-build": "^0.2.0",
|
||||
"@ekklesia/typescript-config": "^0.2.0",
|
||||
"@ekklesia/module-pack": "0.2.3",
|
||||
"@ekklesia/module-ui-build": "^0.2.3",
|
||||
"@ekklesia/typescript-config": "^0.2.3",
|
||||
"@types/node": "24.13.3",
|
||||
"@types/react": "19.2.18",
|
||||
"@types/react-dom": "19.2.4",
|
||||
|
||||
Generated
+169
-169
@@ -12,20 +12,20 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@ekklesia/design-tokens':
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3
|
||||
'@ekklesia/module-database':
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0([email protected])
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3([email protected])
|
||||
'@ekklesia/module-sdk':
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3
|
||||
'@ekklesia/module-ui-sdk':
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3
|
||||
'@ekklesia/ui':
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0([email protected])
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3([email protected])
|
||||
drizzle-orm:
|
||||
specifier: 0.45.2
|
||||
version: 0.45.2
|
||||
@@ -36,21 +36,21 @@ importers:
|
||||
specifier: 19.2.8
|
||||
version: 19.2.8([email protected])
|
||||
sharp:
|
||||
specifier: 0.35.4
|
||||
version: 0.35.4(@types/[email protected])
|
||||
specifier: 0.35.5
|
||||
version: 0.35.5(@types/[email protected])
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: 2.5.3
|
||||
version: 2.5.3
|
||||
'@ekklesia/module-pack':
|
||||
specifier: 0.2.0
|
||||
version: 0.2.0
|
||||
specifier: 0.2.3
|
||||
version: 0.2.3
|
||||
'@ekklesia/module-ui-build':
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3
|
||||
'@ekklesia/typescript-config':
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3
|
||||
'@types/node':
|
||||
specifier: 24.13.3
|
||||
version: 24.13.3
|
||||
@@ -135,39 +135,39 @@ packages:
|
||||
'@drizzle-team/[email protected]':
|
||||
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
|
||||
|
||||
'@ekklesia/[email protected].0':
|
||||
resolution: {integrity: sha512-SRSRIRPkrdGEJGnMgoIHlA/LpKXRya14wYg5zRp28Ev9ipinq8JuJJThHvZzma+NzdUAgOFwDKbP+Gr96Hu2Kg==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fcontracts/-/0.2.0/contracts-0.2.0.tgz}
|
||||
'@ekklesia/[email protected].3':
|
||||
resolution: {integrity: sha512-h1kwuurRp2sOdrVxVtU7TbDx3NoOKEj1G7jhLRawFsWrlqkuhtcBgRXXfHgC5cbEwh5VRfXT3viLwwHnFIRjdw==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fcontracts/-/0.2.3/contracts-0.2.3.tgz}
|
||||
|
||||
'@ekklesia/[email protected].0':
|
||||
resolution: {integrity: sha512-Y62oHBiKhzKifAZM6ARJbMqOE0K5iu5Fp+l9hOIoxzdHZVZmqe4ZrcOoeFY6mm9Xyih1fK1qH5fcGJmgD/wS7w==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fdesign-tokens/-/0.2.0/design-tokens-0.2.0.tgz}
|
||||
'@ekklesia/[email protected].3':
|
||||
resolution: {integrity: sha512-ZN4x/qehXxnWccWrICyMMDgHXvXoKyicFc/qm8wmf3513PTBzM9lDtfrgnfBLEoc+mjeEfTTnfOyigX97U56XA==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fdesign-tokens/-/0.2.3/design-tokens-0.2.3.tgz}
|
||||
|
||||
'@ekklesia/[email protected].0':
|
||||
resolution: {integrity: sha512-0hfB2ac1IfW1BrFEFTpyb0OhGo4umYqWycbEx6IlMjG73e2Q0czsMO32+GniidKnU6yiPv61iZ7g6SquxyKbDg==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fmodule-database/-/0.2.0/module-database-0.2.0.tgz}
|
||||
'@ekklesia/[email protected].3':
|
||||
resolution: {integrity: sha512-xGwFPPD/wjbfWqgB45TpGVdLNC7eUb6j5l2wgRdpGCpTScnVsS30EJmgwsplPi6uHErUiNIZBwuN6YV5jVcZDg==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fmodule-database/-/0.2.3/module-database-0.2.3.tgz}
|
||||
peerDependencies:
|
||||
drizzle-orm: '>=0.45.2 <0.46.0'
|
||||
peerDependenciesMeta:
|
||||
drizzle-orm:
|
||||
optional: true
|
||||
|
||||
'@ekklesia/[email protected].0':
|
||||
resolution: {integrity: sha512-p6nfveGeMimLs5DyaFw2gExLOPZDp1MEeiMcqN8zIunsGavIMgJYbfVmuxvbshk+3KSCnYzq2q1yurQ3G82/3w==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fmodule-pack/-/0.2.0/module-pack-0.2.0.tgz}
|
||||
'@ekklesia/[email protected].3':
|
||||
resolution: {integrity: sha512-U755u1sctmLm1bS9uaWQjcHPZw8WCYSTY5Ddi4QTbLVB3QzqYaWCaOyeeKJyEtoI+iZQNFkTAjAQPMSCVbCj8g==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fmodule-pack/-/0.2.3/module-pack-0.2.3.tgz}
|
||||
hasBin: true
|
||||
|
||||
'@ekklesia/[email protected].0':
|
||||
resolution: {integrity: sha512-7kaE9No+ppYDuOiKD0T2W4aUo5mmWzt53djKQPQvzStWOYtI3TmkGhRq2aUIRXmQ9l86W3tyk9c9A2f1Zm3zJQ==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fmodule-sdk/-/0.2.0/module-sdk-0.2.0.tgz}
|
||||
'@ekklesia/[email protected].3':
|
||||
resolution: {integrity: sha512-H6g2F2xfA/akmzWBfSQTtUtgIrGkHxzPpvbJk4EqUJ6eXllHtn82hAw9r0S7vjLWE9IMD9m9KLAJ3Xil3mZsig==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fmodule-sdk/-/0.2.3/module-sdk-0.2.3.tgz}
|
||||
|
||||
'@ekklesia/[email protected].0':
|
||||
resolution: {integrity: sha512-zX8Gi9b/ZV2n7/hZeOTW2+ar18hDvEotfNKivDdH6+PzeNCH1iF6i5KpoNR3rXfPdTVu7o+pdN64FqiOeldhIQ==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fmodule-ui-build/-/0.2.0/module-ui-build-0.2.0.tgz}
|
||||
'@ekklesia/[email protected].3':
|
||||
resolution: {integrity: sha512-rSymIjPeoDzxpXpTC7D74/g8MV5PgUP56dHGXxe/jrvlvfdFeNa7pSMUYa4lKc3DvlcJucvq4BjwGXRuDUCdEA==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fmodule-ui-build/-/0.2.3/module-ui-build-0.2.3.tgz}
|
||||
hasBin: true
|
||||
|
||||
'@ekklesia/[email protected].0':
|
||||
resolution: {integrity: sha512-W9oR0OoIjRhDqMKC7oJfDUUSVZDQ/i2u5ESY4bbKt3HSdHHL3XQ3LMWaVLwvlW6JKeZe/xIs8F0hupfYoIwMKQ==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fmodule-ui-sdk/-/0.2.0/module-ui-sdk-0.2.0.tgz}
|
||||
'@ekklesia/[email protected].3':
|
||||
resolution: {integrity: sha512-zSvI3gfnqBiBt7h0Xbs6xmsLYP9E8M0xpLGVno+OaC1cyrKVUvlwq78as2zoiTGFHl2aoI5Mz9V4/KJOVxyByg==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fmodule-ui-sdk/-/0.2.3/module-ui-sdk-0.2.3.tgz}
|
||||
|
||||
'@ekklesia/[email protected].0':
|
||||
resolution: {integrity: sha512-ymcds9FV3VVGyCo3AEy7WCzpZx0dOJUTsu4hIfyLOJM/5sHxJxz76YpNght3MfDi0WdGy8yUb0854lA6nPjpyg==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Ftypescript-config/-/0.2.0/typescript-config-0.2.0.tgz}
|
||||
'@ekklesia/[email protected].3':
|
||||
resolution: {integrity: sha512-dm8d8hJtdwwaMpsIwXcPXdCu616RQu3Y3/5OacPS1L2eb7Bhnxfl5SQJMuee8CtUSs45kBr0KXN8VWHl+AxSXg==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Ftypescript-config/-/0.2.3/typescript-config-0.2.3.tgz}
|
||||
|
||||
'@ekklesia/[email protected].0':
|
||||
resolution: {integrity: sha512-17VPs94+Lcb9yshooSbRaWiR8XMXptosOTp89cE+HnpL3HjIOhMSNb5vOFNVEPwMYJ0phZhQupwb6Qsd6Gatlg==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fui/-/0.2.0/ui-0.2.0.tgz}
|
||||
'@ekklesia/[email protected].3':
|
||||
resolution: {integrity: sha512-9xNOnp47VvARP91+XFG8mo/F5ln1Zi+w2RVY0mBeyf6qO5P39nyOAzwxunsD44Mb3E1o+fCTTgZoSJbLMaRtMA==, tarball: https://git.riverofglorymn.org/api/packages/River_of_Glory_Church/npm/%40ekklesia%2Fui/-/0.2.3/ui-0.2.3.tgz}
|
||||
peerDependencies:
|
||||
react: ^19.2.0
|
||||
|
||||
@@ -654,160 +654,160 @@ packages:
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
os: [freebsd]
|
||||
|
||||
'@img/[email protected].3':
|
||||
resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==}
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/[email protected].3':
|
||||
resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==}
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/[email protected].3':
|
||||
resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==}
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected].3':
|
||||
resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==}
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected].3':
|
||||
resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==}
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected].3':
|
||||
resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==}
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected].3':
|
||||
resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==}
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected].3':
|
||||
resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==}
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected].3':
|
||||
resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==}
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected].3':
|
||||
resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==}
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==}
|
||||
engines: {node: ^20.9.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/[email protected].4':
|
||||
resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==}
|
||||
'@img/[email protected].5':
|
||||
resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1383,8 +1383,8 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
[email protected].4:
|
||||
resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
|
||||
[email protected].5:
|
||||
resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '*'
|
||||
@@ -1590,43 +1590,43 @@ snapshots:
|
||||
|
||||
'@drizzle-team/[email protected]': {}
|
||||
|
||||
'@ekklesia/[email protected].0':
|
||||
'@ekklesia/[email protected].3':
|
||||
dependencies:
|
||||
semver: 7.8.5
|
||||
spdx-expression-parse: 5.0.0
|
||||
zod: 4.4.3
|
||||
|
||||
'@ekklesia/[email protected].0': {}
|
||||
'@ekklesia/[email protected].3': {}
|
||||
|
||||
'@ekklesia/[email protected].0([email protected])':
|
||||
'@ekklesia/[email protected].3([email protected])':
|
||||
dependencies:
|
||||
'@ekklesia/contracts': 0.2.0
|
||||
'@ekklesia/contracts': 0.2.3
|
||||
optionalDependencies:
|
||||
drizzle-orm: 0.45.2
|
||||
|
||||
'@ekklesia/[email protected].0':
|
||||
'@ekklesia/[email protected].3':
|
||||
dependencies:
|
||||
'@ekklesia/contracts': 0.2.0
|
||||
'@ekklesia/module-ui-build': 0.2.0
|
||||
'@ekklesia/contracts': 0.2.3
|
||||
'@ekklesia/module-ui-build': 0.2.3
|
||||
esbuild: 0.28.1
|
||||
semver: 7.8.5
|
||||
|
||||
'@ekklesia/[email protected].0':
|
||||
'@ekklesia/[email protected].3':
|
||||
dependencies:
|
||||
'@ekklesia/contracts': 0.2.0
|
||||
'@ekklesia/contracts': 0.2.3
|
||||
|
||||
'@ekklesia/[email protected].0':
|
||||
'@ekklesia/[email protected].3':
|
||||
dependencies:
|
||||
'@ekklesia/contracts': 0.2.0
|
||||
'@ekklesia/contracts': 0.2.3
|
||||
esbuild: 0.28.1
|
||||
|
||||
'@ekklesia/[email protected].0': {}
|
||||
'@ekklesia/[email protected].3': {}
|
||||
|
||||
'@ekklesia/[email protected].0': {}
|
||||
'@ekklesia/[email protected].3': {}
|
||||
|
||||
'@ekklesia/[email protected].0([email protected])':
|
||||
'@ekklesia/[email protected].3([email protected])':
|
||||
dependencies:
|
||||
'@ekklesia/design-tokens': 0.2.0
|
||||
'@ekklesia/design-tokens': 0.2.3
|
||||
react: 19.2.8
|
||||
|
||||
'@emnapi/[email protected]':
|
||||
@@ -1880,108 +1880,108 @@ snapshots:
|
||||
|
||||
'@img/[email protected]': {}
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.3
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.3
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
dependencies:
|
||||
'@img/sharp-wasm32': 0.35.4
|
||||
'@img/sharp-wasm32': 0.35.5
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].3':
|
||||
'@img/[email protected].4':
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].3':
|
||||
'@img/[email protected].4':
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].3':
|
||||
'@img/[email protected].4':
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].3':
|
||||
'@img/[email protected].4':
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].3':
|
||||
'@img/[email protected].4':
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].3':
|
||||
'@img/[email protected].4':
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].3':
|
||||
'@img/[email protected].4':
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].3':
|
||||
'@img/[email protected].4':
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].3':
|
||||
'@img/[email protected].4':
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].3':
|
||||
'@img/[email protected].4':
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.3
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.3
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.3
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.3.3
|
||||
'@img/sharp-libvips-linux-x64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.11.3
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
dependencies:
|
||||
'@img/sharp-wasm32': 0.35.4
|
||||
'@img/sharp-wasm32': 0.35.5
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
optional: true
|
||||
|
||||
'@img/[email protected].4':
|
||||
'@img/[email protected].5':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/[email protected]': {}
|
||||
@@ -2401,37 +2401,37 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected].4(@types/[email protected]):
|
||||
[email protected].5(@types/[email protected]):
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.8.5
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.35.4
|
||||
'@img/sharp-darwin-x64': 0.35.4
|
||||
'@img/sharp-freebsd-wasm32': 0.35.4
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.3
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.3
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.3
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.3
|
||||
'@img/sharp-libvips-linux-x64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
|
||||
'@img/sharp-linux-arm': 0.35.4
|
||||
'@img/sharp-linux-arm64': 0.35.4
|
||||
'@img/sharp-linux-ppc64': 0.35.4
|
||||
'@img/sharp-linux-riscv64': 0.35.4
|
||||
'@img/sharp-linux-s390x': 0.35.4
|
||||
'@img/sharp-linux-x64': 0.35.4
|
||||
'@img/sharp-linuxmusl-arm64': 0.35.4
|
||||
'@img/sharp-linuxmusl-x64': 0.35.4
|
||||
'@img/sharp-webcontainers-wasm32': 0.35.4
|
||||
'@img/sharp-win32-arm64': 0.35.4
|
||||
'@img/sharp-win32-ia32': 0.35.4
|
||||
'@img/sharp-win32-x64': 0.35.4
|
||||
'@img/sharp-darwin-arm64': 0.35.5
|
||||
'@img/sharp-darwin-x64': 0.35.5
|
||||
'@img/sharp-freebsd-wasm32': 0.35.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.4
|
||||
'@img/sharp-libvips-linux-arm': 1.3.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.4
|
||||
'@img/sharp-libvips-linux-x64': 1.3.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.4
|
||||
'@img/sharp-linux-arm': 0.35.5
|
||||
'@img/sharp-linux-arm64': 0.35.5
|
||||
'@img/sharp-linux-ppc64': 0.35.5
|
||||
'@img/sharp-linux-riscv64': 0.35.5
|
||||
'@img/sharp-linux-s390x': 0.35.5
|
||||
'@img/sharp-linux-x64': 0.35.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.35.5
|
||||
'@img/sharp-linuxmusl-x64': 0.35.5
|
||||
'@img/sharp-webcontainers-wasm32': 0.35.5
|
||||
'@img/sharp-win32-arm64': 0.35.5
|
||||
'@img/sharp-win32-ia32': 0.35.5
|
||||
'@img/sharp-win32-x64': 0.35.5
|
||||
'@types/node': 24.13.3
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
+9
-9
@@ -11,15 +11,15 @@ allowBuilds:
|
||||
# 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].0'
|
||||
- '@ekklesia/[email protected].0'
|
||||
- '@ekklesia/[email protected].0'
|
||||
- '@ekklesia/[email protected].0'
|
||||
- '@ekklesia/[email protected].0'
|
||||
- '@ekklesia/[email protected].0'
|
||||
- '@ekklesia/[email protected].0'
|
||||
- '@ekklesia/[email protected].0'
|
||||
- '@ekklesia/[email protected].0'
|
||||
- '@ekklesia/[email protected].3'
|
||||
- '@ekklesia/[email protected].3'
|
||||
- '@ekklesia/[email protected].3'
|
||||
- '@ekklesia/[email protected].3'
|
||||
- '@ekklesia/[email protected].3'
|
||||
- '@ekklesia/[email protected].3'
|
||||
- '@ekklesia/[email protected].3'
|
||||
- '@ekklesia/[email protected].3'
|
||||
- '@ekklesia/[email protected].3'
|
||||
|
||||
# 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
|
||||
|
||||
@@ -59,7 +59,7 @@ function SignageDisplay({ context }: { readonly context: ModuleUiHostContext }):
|
||||
*/
|
||||
export default defineModuleUi({
|
||||
id: "signage",
|
||||
version: "0.1.2",
|
||||
version: "0.1.3",
|
||||
styles: displayStyles,
|
||||
mount(container, context) {
|
||||
const root = createRoot(container);
|
||||
|
||||
@@ -177,10 +177,11 @@ export async function processSignageImage(bytes: Uint8Array): Promise<SignagePro
|
||||
height: metadata.height as number,
|
||||
width: metadata.width as number,
|
||||
};
|
||||
const [display, thumbnail] = await Promise.all([
|
||||
renderRendition(bytes, SIGNAGE_IMAGE_DISPLAY_RENDITION),
|
||||
renderRendition(bytes, SIGNAGE_IMAGE_THUMBNAIL_RENDITION),
|
||||
]);
|
||||
// Keep peak native CPU and memory bounded inside the shared API process. Upload concurrency is
|
||||
// already bounded at the module UI; running both renditions for every upload in parallel would
|
||||
// multiply that concurrency again and can starve the database and storage clients on a small host.
|
||||
const display = await renderRendition(bytes, SIGNAGE_IMAGE_DISPLAY_RENDITION);
|
||||
const thumbnail = await renderRendition(bytes, SIGNAGE_IMAGE_THUMBNAIL_RENDITION);
|
||||
|
||||
return {
|
||||
byteSize: bytes.byteLength,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
MODULE_STORAGE_ERROR_CODES,
|
||||
type ModuleStorage,
|
||||
type ModuleStorageBucket,
|
||||
ModuleStorageError,
|
||||
type ModuleStorageErrorCode,
|
||||
} from "@ekklesia/module-sdk";
|
||||
import { SignageError } from "../errors.js";
|
||||
import { type SignageStore, SignageStoreConflict, type StoredImage } from "../store/types.js";
|
||||
@@ -17,6 +18,20 @@ export function nativeImageBucket(storage: ModuleStorage): ModuleStorageBucket {
|
||||
return storage.bucket(SIGNAGE_IMAGE_BUCKET);
|
||||
}
|
||||
|
||||
/** Host errors cross the installed-package boundary from a different copy of the SDK, so class
|
||||
* identity is not part of the contract (ADR 0049). Recognize only the SDK's closed, sanitized
|
||||
* storage-error shape; provider messages and causes remain inaccessible. */
|
||||
function storageErrorCode(error: unknown): ModuleStorageErrorCode | undefined {
|
||||
if (!(error instanceof Error) || error.name !== "ModuleStorageError") {
|
||||
return undefined;
|
||||
}
|
||||
const code = (error as { readonly code?: unknown }).code;
|
||||
return typeof code === "string" &&
|
||||
(MODULE_STORAGE_ERROR_CODES as readonly string[]).includes(code)
|
||||
? (code as ModuleStorageErrorCode)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Object keys are scoped to the asset they belong to.
|
||||
*
|
||||
@@ -41,7 +56,7 @@ export function renditionObjectKey(assetId: string, kind: string): string {
|
||||
* not a caller's business.
|
||||
*/
|
||||
function storageFailure(error: unknown): SignageError {
|
||||
if (error instanceof ModuleStorageError && error.code === "STORAGE_OBJECT_NOT_FOUND") {
|
||||
if (storageErrorCode(error) === "STORAGE_OBJECT_NOT_FOUND") {
|
||||
return new SignageError("SIGNAGE_NOT_FOUND");
|
||||
}
|
||||
return new SignageError("SIGNAGE_STORAGE_UNAVAILABLE");
|
||||
@@ -150,7 +165,7 @@ export async function ingestNativeImage(
|
||||
if (error instanceof SignageError) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof ModuleStorageError) {
|
||||
if (storageErrorCode(error) !== undefined) {
|
||||
throw storageFailure(error);
|
||||
}
|
||||
throw new SignageError("SIGNAGE_OPERATION_FAILED");
|
||||
|
||||
@@ -130,7 +130,7 @@ export function createSignageModuleRuntime(
|
||||
// Database ABI v1 executor, which `createSignageStore` binds Signage's own Drizzle adapter to.
|
||||
database: { abi: 1 },
|
||||
id: "signage",
|
||||
version: "0.1.2",
|
||||
version: "0.1.3",
|
||||
|
||||
register({ capabilities, config, http, realtime }) {
|
||||
const settings = readSignageSettings(config);
|
||||
@@ -192,7 +192,7 @@ export function createSignageModuleRuntime(
|
||||
// server enforces — a UI believing a stale limit would offer an upload the server refuses.
|
||||
return Response.json({
|
||||
crossfadeMs: current.crossfadeMs,
|
||||
moduleVersion: "0.1.2",
|
||||
moduleVersion: "0.1.3",
|
||||
nativeImageDurationMs: current.nativeImageDurationMs,
|
||||
nativePresentation: current.nativePresentation,
|
||||
rendererInputVersion: SIGNAGE_SLIDE_RENDERER_INPUT_VERSION,
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import ownStyles from "./styles.css";
|
||||
*/
|
||||
export default defineModuleUi({
|
||||
id: "signage",
|
||||
version: "0.1.2",
|
||||
version: "0.1.3",
|
||||
styles: `${EKKLESIA_UI_STYLESHEET}\n${ownStyles}`,
|
||||
mount(container, context) {
|
||||
const root = createRoot(container);
|
||||
|
||||
@@ -127,7 +127,7 @@ export function createFakeHost(options: FakeHostOptions): FakeHost {
|
||||
/** The settings payload every screen loads first. */
|
||||
export const FAKE_SETTINGS = Object.freeze({
|
||||
crossfadeMs: 1000,
|
||||
moduleVersion: "0.1.2",
|
||||
moduleVersion: "0.1.3",
|
||||
nativeImageDurationMs: 7000,
|
||||
nativePresentation: {
|
||||
backgroundColor: "#000000",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ModuleStorage } from "@ekklesia/module-sdk";
|
||||
import sharp from "sharp";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { SignageError } from "../src/server/errors.js";
|
||||
@@ -372,6 +373,30 @@ describe("native image ingest", () => {
|
||||
expect(store.images.size).toBe(0);
|
||||
});
|
||||
|
||||
it("recognizes a sanitized storage failure from the host's separate SDK copy", async () => {
|
||||
const bucket = storage.bucket("native-images");
|
||||
const externalError = Object.assign(new Error("The storage service could not be reached."), {
|
||||
code: "STORAGE_PROVIDER_UNAVAILABLE",
|
||||
name: "ModuleStorageError",
|
||||
});
|
||||
const externalStorage: ModuleStorage = {
|
||||
bucket: () => ({ ...bucket, put: () => Promise.reject(externalError) }),
|
||||
buckets: () => ["native-images"],
|
||||
};
|
||||
|
||||
await expectSignageCode(
|
||||
ingestNativeImage({
|
||||
bytes: await makeImage({ format: "png", height: 360, width: 640 }),
|
||||
displayName: "welcome.png",
|
||||
expiresAt: null,
|
||||
filename: "welcome.png",
|
||||
storage: externalStorage,
|
||||
store,
|
||||
}),
|
||||
"SIGNAGE_STORAGE_UNAVAILABLE",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats a race with an identical upload as a success rather than a failure", async () => {
|
||||
const bytes = await makeImage({ format: "png", height: 1080, width: 1920 });
|
||||
const first = await ingest(bytes);
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("Signage module manifest", () => {
|
||||
expect(manifest.manifestVersion).toBe(8);
|
||||
expect(manifest.id).toBe("signage");
|
||||
expect(manifest.name).toBe("Signage");
|
||||
expect(manifest.version).toBe("0.1.2");
|
||||
expect(manifest.version).toBe("0.1.3");
|
||||
expect(manifest.compatibility).toEqual({ moduleSdk: "^0.2.0", platform: "^0.2.0" });
|
||||
});
|
||||
|
||||
@@ -151,7 +151,7 @@ describe("Signage module manifest", () => {
|
||||
describe("Signage server runtime", () => {
|
||||
it("exports the typed runtime definition the compiled server entrypoint publishes", async () => {
|
||||
// The runtime's database declaration must match the manifest's, or Core refuses to start it.
|
||||
expect(moduleRuntime).toMatchObject({ database: { abi: 1 }, id: "signage", version: "0.1.2" });
|
||||
expect(moduleRuntime).toMatchObject({ database: { abi: 1 }, id: "signage", version: "0.1.3" });
|
||||
});
|
||||
|
||||
it("registers its routes declaratively, with no database, storage, or realtime work", async () => {
|
||||
@@ -250,7 +250,7 @@ describe("Signage server runtime", () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toEqual({
|
||||
crossfadeMs: 400,
|
||||
moduleVersion: "0.1.2",
|
||||
moduleVersion: "0.1.3",
|
||||
nativeImageDurationMs: 12_000,
|
||||
nativePresentation: SIGNAGE_DEFAULT_SETTINGS.nativePresentation,
|
||||
rendererInputVersion: "1.0.0",
|
||||
|
||||
Reference in New Issue
Block a user