feat: add community publishing and news syndication
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 50s
Validate and deploy production / deploy (push) Failing after 40s
Validate and deploy staging / deploy (push) Failing after 7s

This commit is contained in:
jb
2026-08-01 21:42:09 -04:00
parent b47e2588e9
commit dd84dff055
288 changed files with 83254 additions and 38351 deletions
+102 -51
View File
@@ -2,8 +2,8 @@
SHD.build runs in a Node 24 container through the official SvelteKit Node
adapter. The image includes application code, migration and notification
scripts, and native runtime dependencies. Environment files, databases,
backups, and user uploads remain outside the image.
scripts, and native runtime dependencies. Environment files, databases, backups,
and user uploads remain outside the image.
## Persistent data
@@ -16,8 +16,8 @@ mounted at `/app/data`. It contains:
- uploaded game-data icons under `game-icons/`.
Keep this volume during routine deployments. `docker compose down` without
`--volumes` preserves it. Before a manual backup or restore, stop the `app`
and `scheduler` services because both access the embedded database.
`--volumes` preserves it. Before a manual backup or restore, stop the `app` and
`scheduler` services because both access the embedded database.
## Staging environment
@@ -29,6 +29,21 @@ Staging defaults to:
- Passkey relying-party ID: `shd.build`
- Adapter canonical origin: `https://stage.shd.build`
Guide ratings and reports use Cloudflare Turnstile after repeated anonymous
activity. Configure both values in each encrypted environment:
```text
TURNSTILE_SITE_KEY=<public site key>
TURNSTILE_SECRET_KEY=<server verification secret>
```
Register the exact staging and production hostnames in the Turnstile widget. The
browser receives only `TURNSTILE_SITE_KEY`. The server sends a challenge token
and the request address to Cloudflare for verification; it never exposes
`TURNSTILE_SECRET_KEY` or stores the raw address as visitor identity. Without a
valid secret, a visitor who reaches the challenge threshold cannot submit more
anonymous ratings or Guide reports.
Edit its encrypted configuration locally:
```sh
@@ -38,12 +53,11 @@ npm run env:encrypt:staging
```
Add `https://stage.shd.build/auth/discord/callback` to the Discord application's
allowed OAuth redirect URIs. Point the staging DNS record at the deployment
host and configure the host's TLS reverse proxy to forward
`stage.shd.build` to port `5569` on the staging host. Docker publishes this
port on the host interfaces so a containerized reverse proxy or tunnel can
reach it; firewall the port to trusted LAN/tunnel sources when the host is
reachable from other networks.
allowed OAuth redirect URIs. Point the staging DNS record at the deployment host
and configure the host's TLS reverse proxy to forward `stage.shd.build` to port
`5569` on the staging host. Docker publishes this port on the host interfaces so
a containerized reverse proxy or tunnel can reach it; firewall the port to
trusted LAN/tunnel sources when the host is reachable from other networks.
The trusted staging runner decrypts `.env.staging` into a mode-`0600` runtime
file. Gitea Actions holds the decryption key; Git and the container image do
@@ -55,10 +69,10 @@ that signs in instead of writing generated credentials into deployment logs.
## Gitea Actions runner
Gitea Actions requires an `act_runner`. Register a trusted runner on the
staging host from the repository's **Settings → Actions → Runners** page. The
host needs Node/npm, Docker Engine, and Docker Compose v2. The runner account
must be able to use Docker and write to the deployment directory.
Gitea Actions requires an `act_runner`. Register a trusted runner on the staging
host from the repository's **Settings → Actions → Runners** page. The host needs
Node/npm, Docker Engine, and Docker Compose v2. The runner account must be able
to use Docker and write to the deployment directory.
Use runner labels that provide an isolated validation job and a host deployment
job:
@@ -94,9 +108,9 @@ In `https://git.jb.codes/jb/shd.build/settings/actions`:
`.gitea/workflows/deploy-staging.yml` runs for pushes to `main` and manual
dispatches. The trusted staging runner builds each commit-tagged image locally,
so the workflow needs no registry credentials. Gitea assigns
`gitea.run_number` to `SHD_BUILD_NUMBER`; each new workflow run receives the
next integer, while retries of the same run retain their number.
so the workflow needs no registry credentials. Gitea assigns `gitea.run_number`
to `SHD_BUILD_NUMBER`; each new workflow run receives the next integer, while
retries of the same run retain their number.
## Deployment lifecycle
@@ -114,12 +128,28 @@ The workflow:
The outage covers backup, migration, and container startup. Migrations may also
remove records that use retired data formats when a release explicitly ends
compatibility. The deployment backup remains the recovery source for those
records; the application does not repair or reinterpret them at runtime.
The scheduler calls a bearer-authenticated endpoint on the application service
once per minute. Scheduled message delivery therefore uses the web process's
existing embedded-database connection instead of opening the locked database
from a second process. The endpoint runs automated system messages, Oversight
consolidation, and email digest checks and is not published through a host port.
records; the application does not repair or reinterpret them at runtime. The
scheduler calls a bearer-authenticated endpoint on the application service once
per minute. Scheduled message delivery therefore uses the web process's existing
embedded-database connection instead of opening the locked database from a
second process. The endpoint runs automated system messages, Oversight
consolidation, email digest checks, and the Ubisoft News importer. Docker does
not publish the endpoint through a host port.
When enabled in production, the Ubisoft importer keeps its own persisted due
time, so the one-minute tick starts a feed synchronization no more than once per
hour after a successful run. When a synchronization fails, the importer stores
the last error and consecutive failure count, then retries after 15 minutes with
exponential backoff capped at six hours. Source items that disappear from the
feed remain published; the importer performs no automatic removal. The scheduler
runs notification delivery before it starts the bounded Ubisoft request, so a
slow provider does not postpone notifications for the current tick.
`deploy/compose.production.yml` declares `UBISOFT_NEWS_IMPORT_ENABLED=true`,
while the staging Compose file declares `false`. Development does not set the
flag and remains disabled because the importer requires the exact value `true`.
The application process evaluates the flag when it handles
`/api/internal/scheduler`.
## Prelaunch operation
@@ -131,8 +161,8 @@ for a database that does not contain the row:
- News, Database, Builds, Guides, and Builder: enabled
The migration uses an insert-if-missing seed. Later deployments preserve the
operator's settings, and configuration changes take effect without a restart.
A full environment bootstrap copies the source row. Canonical ISAC game-data
operator's settings, and configuration changes take effect without a restart. A
full environment bootstrap copies the source row. Canonical ISAC game-data
imports leave the target environment's launch settings unchanged.
Use this operator flow after the first deployment:
@@ -149,13 +179,13 @@ Use this operator flow after the first deployment:
session before public release. Sign in and verify the same disabled routes
remain unavailable.
Keep Coming Soon enabled while preparing production. Section controls can be
set in advance because the launch gate takes precedence for signed-out traffic.
Set Account Creation to Disabled if prelaunch access must remain limited to
existing identities. Switch the Launch Gate to Public Launch after verification.
Disabled sections remain absent from navigation and unavailable by direct URL
for every user, including authenticated agents. Authentication bypasses only
the Coming Soon gate.
Keep Coming Soon enabled while preparing production. Section controls can be set
in advance because the launch gate takes precedence for signed-out traffic. Set
Account Creation to Disabled if prelaunch access must remain limited to existing
identities. Switch the Launch Gate to Public Launch after verification. Disabled
sections remain absent from navigation and unavailable by direct URL for every
user, including authenticated agents. Authentication bypasses only the Coming
Soon gate.
Turn Coming Soon on to restore the promotional gate. Site-control changes append
`system_configuration / site_access.updated` audit events, including the
@@ -177,9 +207,9 @@ open while its complete transfer bundle is created:
npm run stage:bootstrap:export
```
The command writes a timestamped, ignored JSON bundle under `data/backups/`.
The bundle includes user records and password hashes. Transfer it over an
encrypted channel, keep it out of Git, then copy it to the staging host and run:
The command writes a timestamped, ignored JSON bundle under `data/backups/`. The
bundle includes user records and password hashes. Transfer it over an encrypted
channel, keep it out of Git, then copy it to the staging host and run:
```sh
/opt/shd-build-stage/bootstrap-staging.sh \
@@ -187,11 +217,10 @@ encrypted channel, keep it out of Git, then copy it to the staging host and run:
```
The bootstrap flow stops the app and scheduler, creates a safety backup of the
target, verifies that staging is unused, imports the entire development
database and uploads, and restarts both services. Delete the transfer copy from
the staging host after verifying the imported record counts and critical
entities. The copy under the local ignored backup directory remains the
recovery source.
target, verifies that staging is unused, imports the entire development database
and uploads, and restarts both services. Delete the transfer copy from the
staging host after verifying the imported record counts and critical entities.
The copy under the local ignored backup directory remains the recovery source.
Because this is a full initial clone, development session tokens and account
data also enter staging. Staging and development remain independent from that
@@ -293,9 +322,31 @@ Copy the resulting ignored JSON bundle to the deployment host over SSH, then:
/secure/path/shd-production-bootstrap-YYYY-MM-DDTHH-MM-SSZ.json
```
The importer verifies the bundle target, creates a production safety backup,
and refuses to replace a production database containing users, builds, audit
events, or proposals. Remove the remote transfer copy after verification.
The importer verifies the bundle target, creates a production safety backup, and
refuses to replace a production database containing users, builds, audit events,
or proposals. Remove the remote transfer copy after verification.
### Promote one News article
Export a locally authored News article by ID or slug while the development
server is stopped:
```sh
npm run article:export -- <article-id-or-slug>
```
The ignored bundle contains the article draft, immutable revisions, search row,
managed-image metadata, and image bytes. It excludes comments, reports, user
records, Builds, and ISAC data.
After deploying the matching application schema, stop the production app and
scheduler and mount the transferred bundle into a one-off application container.
Run `scripts/article-transfer.mjs import` with `ARTICLE_IMPORT_ENABLED=true` and
`--confirm-article-import`. The importer creates a production database backup
before opening the database, resolves the article owner by stable ID or
username, rejects ID and slug conflicts, verifies image checksums, and appends
`editorial_content / news.promoted`. Restart and health-check both services,
then remove the transferred bundle from the host.
### Pull production ISAC data into development
@@ -312,10 +363,10 @@ notifications, settings, and audit history untouched.
### Pull production ISAC data into staging
Manually run **Replace staging ISAC data from production** in Gitea Actions.
The workflow exports a consistent production snapshot, restarts production,
then stops staging and imports it. The staging importer creates full and
ISAC-only safety backups before replacement. Deployments do not run this flow.
Manually run **Replace staging ISAC data from production** in Gitea Actions. The
workflow exports a consistent production snapshot, restarts production, then
stops staging and imports it. The staging importer creates full and ISAC-only
safety backups before replacement. Deployments do not run this flow.
### Promote managed images without replacing other data
@@ -333,8 +384,8 @@ internal scheduler. `REMOVE_BG_CREDIT_CHECK_HOURS` controls the account lookup
interval and defaults to six hours. Low-capacity alerts require both remaining
pools to cross their thresholds: `REMOVE_BG_LOW_CREDIT_THRESHOLD` defaults to
five paid credits, and `REMOVE_BG_LOW_FREE_CALL_THRESHOLD` defaults to ten free
calls. Alerts are mandatory ISAC notifications for active Administrators and
are deduplicated for each warning or critical episode.
calls. Alerts are mandatory ISAC notifications for active Administrators and are
deduplicated for each warning or critical episode.
```sh
npm run images:export
@@ -368,8 +419,8 @@ adds new stable IDs, skips identical assets, and blocks conflicting records.
After reviewing an intentional metadata or file replacement, add
`--update-existing`. Imported changes append `image.imported` or
`image.import_updated` audit events. Remove the transferred bundle after
verifying both environments because it may contain unpublished visual assets
and creator identifiers.
verifying both environments because it may contain unpublished visual assets and
creator identifiers.
The `.env.staging` and `.env.production` npm import commands are intended for
maintenance performed directly in an environment where those database and