feat(delivery): phase 12 — the container, and the defect only a proxy could find
All checks were successful
PR checks / checks (pull_request) Successful in 9m46s

PLAN.md §13 phase 12, the last one. Four decisions of record, D54–D57, taking the
count to fifty-seven; recorded in §6, "How phase 12 delivered it".

A two-stage Dockerfile, a pull-only docker-compose.yml carrying both bind mounts,
.env.example, the workflow that publishes and deploys, CONTRIBUTING.md, the
community-health files this was the only repository of the ten to lack, and
DEPLOY.md.

D54 — a merge deploys, amending D6. build-image.yml pushes
runicgateway-site:latest and :sha-<7>, then rolls the container over on the
`rgcom` runner out of /opt/runicgateway.com, and waits for the container's own
healthcheck rather than for `up -d` to return.

D55 — the site runs on its own host behind a generic reverse proxy, so DEPLOY.md
states the four requirements rather than one worked example, and the container
binds 127.0.0.1 so the safe configuration is the default.

D56 — @astrojs/node derives the request protocol from req.socket.encrypted and
never reads x-forwarded-proto, so behind a TLS-terminating proxy the browser sends
Origin: https://… while the container computes http://… and Astro's CSRF check
compares them for equality. Every beta signup, from every visitor, was answered
403. serve.mjs now normalises both forwarded headers, unconditionally — the image
should deploy and work. Two assertions in test/headers.test.mjs hold both halves.

D57 — DEPLOY.md rather than a README section; SECURITY.md and CODE_OF_CONDUCT.md
are pointers to the org's copies rather than copies, because a copy would hard-code
the contact address D13 confines to brand.json.

Verified: npm run verify green (eleven checks, 36 unit tests, 7 served tests,
astro check 0 errors). The image was built and run with both mounts — a mounted
brand reached 51 files and all 50 search pages, /brand/* fell back per file, a
proxy-shaped signup reached the store, and the export CLI wrote both Play files to
the host mount. docker compose config caught a YAML trap in the healthcheck: a
block sequence reads the `: ` in `r.ok ? 0 : 1` as a mapping.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-25 16:54:38 -05:00
parent 18064062a9
commit f2e59a2426
17 changed files with 1451 additions and 14 deletions

View File

@@ -0,0 +1,48 @@
---
name: Bug report
about: Something on the site is broken, wrong, or behaving unexpectedly
title: "[bug] "
labels:
- bug
---
## Summary
<!-- A clear, concise description of the problem. -->
## Where
<!-- The URL, or the page and the section. If it is a documentation page, the heading. -->
- Page:
- Viewport width, if it is a layout problem:
- Browser and version:
## What happened, and what you expected
<!--
Include exact wording for a factual error, and the console message if there is
one. A screenshot helps for anything visual.
-->
## Is it a factual error?
<!--
The most valuable reports this repository gets are claims that are WRONG about
the platform — a version, a command, a flag, a capability that no longer works
that way. If so, say where the correct answer lives (which repository, which
file), because the fix is usually to a data file or a check rather than to the
sentence.
-->
## Additional context
<!-- Anything else that helps. -->
<!--
Security issue? Do NOT file it here — see SECURITY.md for the private route.
A problem with the PLATFORM rather than with this site (the website, the
sidecar, the shard plugin, the installer, the Android app) belongs in that
repository's tracker. This one only describes them.
-->

View File

@@ -0,0 +1,11 @@
blank_issues_enabled: true
contact_links:
- name: Security vulnerability
url: https://gitea.whitlocktech.com/RunicGateway/runicgateway.com/src/branch/main/SECURITY.md
about: Please do not open a public issue for security problems — report them privately instead (see SECURITY.md).
- name: Questions, help and the Android beta
url: https://discord.gg/t2Jav8yT4g
about: Discord is the front door — instant, and it needs no account here. Bug reports are welcome there too.
- name: A problem with the platform, not the site
url: https://gitea.whitlocktech.com/RunicGateway
about: The website, the sidecar, the shard plugin, the installer and the Android app each have their own tracker. This repository only describes them.

View File

@@ -0,0 +1,38 @@
---
name: Feature request
about: Suggest a page, a section, or a change to how the site explains something
title: "[feature] "
labels:
- enhancement
---
## Problem / motivation
<!--
What were you trying to find out, or do, when the site let you down? A missing
page is easier to judge from the question that went unanswered than from the
page title.
-->
## Proposed solution
<!-- What you would like to see. -->
## Does it belong here?
<!--
Two boundaries this repository holds deliberately (PLAN.md §1):
- The site TEACHES; `docs/` SPECIFIES. Protocol, module API, backend design and
installer behaviour are normative in the docs repository — a page here links
out rather than restating them, so that it cannot drift.
- Absences are stated as data, not implied. If the request is for something the
platform does not do yet, it may belong in src/data/notBuilt.mjs rather than
as a page.
Say which side you think it falls on; being wrong about it is fine.
-->
## Additional context
<!-- Mockups, links, related issues, the repository the change would describe. -->

View File

@@ -0,0 +1,42 @@
<!--
Thanks for contributing to Runic Gateway!
Please fill out the sections below and check every box before requesting review.
Merging to main publishes: it builds the image, pushes it to the registry and
deploys the site. There is no separate release step. See DEPLOY.md.
-->
## What & why
<!-- What does this PR change, and why? Link any related issue: "Closes #123". -->
## How it was tested
<!--
`npm run verify` output is the baseline. If the change touches a page, say what
you looked at and at what width; if it touches the container, say whether you
built and ran the image.
-->
## Checklist
- [ ] I have read [CONTRIBUTING.md](CONTRIBUTING.md).
- [ ] `npm run verify` passes locally (all eleven checks and both test suites).
- [ ] No fact is stated in prose — versions and platform facts come from `src/data/platform.json`.
- [ ] `PLAN.md` still describes what this repository does; a decision it records is either
unchanged or amended here, with the reasoning.
- [ ] My commits are reasonably scoped, with Conventional Commit messages.
## AI-assisted contributions (required)
This project **requires disclosure of AI tool usage**. Please pick one:
- [ ] No AI tools were used to produce this contribution.
- [ ] AI tools were used. Tool(s): `___________`. I have reviewed and understand
every change, and take responsibility for it. AI-authored commits are
marked with a `Co-Authored-By` / `Assisted-By` trailer.
## License
- [ ] I agree that my contribution is licensed under this project's license
(**GNU GPL v3.0 or later**), and I have the right to contribute it.

View File

@@ -0,0 +1,150 @@
# Build the container image, publish it to Gitea's container registry, then roll
# the site onto it — on every merge to main.
#
# Gitea Actions caution, learned elsewhere in this org and repeated from
# pr-checks.yml because it costs one comment and has already cost months: never
# leave an empty template expression anywhere in a `run:` script, not even inside
# a comment. The runner silently SKIPS the whole step without failing the job,
# and the problem is invisible in the workflow list.
#
# Two jobs, in sequence:
#
# build — builds and pushes the image (on `ubuntu-latest`)
# deploy — `needs: build`, so it starts only after a clean build and push, and
# pulls + recreates the stack on the host (on `rgcom`)
#
# Prerequisites, one-time:
#
# • A runner labelled `ubuntu-latest` whose jobs have the host Docker socket
# mounted (/var/run/docker.sock), so `docker build` talks to the host daemon.
# This also gives free layer caching between runs. The org already runs one.
#
# • A runner labelled `rgcom` ON the host that serves the site, able to reach
# the Docker daemon and /opt/runicgateway.com — the directory holding the
# production docker-compose.yml and .env. DEPLOY.md has the registration
# command and the directory layout.
#
# • Two repository secrets (Settings → Actions → Secrets), both of which
# already exist for pr-checks.yml's cross-repository checks:
# REGISTRY_USER — the Gitea username owning the token below
# REGISTRY_TOKEN — a token with write:package (and read:package)
#
# Produces, in gitea.whitlocktech.com/runicgateway/ :
# runicgateway-site:latest + runicgateway-site:sha-<7>
#
# and deploys `:latest`, which is what docker-compose.yml defaults IMAGE_TAG to.
#
# WHY THIS REPOSITORY DEPLOYS AND D6 SAID IT WOULD NOT: D6 was written before
# there was a host to deploy to, and read "ship the image, the org lead deploys".
# The org lead amended it on 2026-08-25 (D54): a marketing site whose content is
# its whole purpose is a bad fit for a manual step between merging a fix and the
# fix being visible. What D6 was protecting — that a bad build cannot reach
# production — is held by `needs: build` instead, plus every check in
# pr-checks.yml having already run on the pull request.
name: Build and publish the image
on:
push:
branches: [main]
workflow_dispatch: {}
concurrency:
group: image-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: gitea.whitlocktech.com
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out the merged commit
uses: actions/checkout@v4
- name: Derive the image ref
# The registry path must be lowercase for Docker; the org is `RunicGateway`.
run: |
set -euo pipefail
OWNER="$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')"
SHORT_SHA="${GITHUB_SHA:0:7}"
echo "IMAGE=${REGISTRY}/${OWNER}/runicgateway-site" >> "$GITHUB_ENV"
echo "TAG=sha-${SHORT_SHA}" >> "$GITHUB_ENV"
- name: Verify the Docker daemon is reachable
# Fails fast with a clear message if the host socket is not mounted into
# the job container — the one hard runner prerequisite.
run: |
set -euo pipefail
if ! docker info >/dev/null 2>&1; then
echo "::error::Docker daemon not reachable. Mount /var/run/docker.sock into the runner's job containers."
exit 1
fi
echo "Docker daemon OK"
- name: Log in to the Gitea container registry
run: |
set -euo pipefail
echo "${{ secrets.REGISTRY_TOKEN }}" \
| docker login "${REGISTRY}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Build and push
# Two tags from one build: `latest` for the compose default, `sha-<7>` so
# a deploy can be pinned or rolled back to an exact commit.
run: |
set -euo pipefail
docker build -f Dockerfile \
-t "${IMAGE}:latest" \
-t "${IMAGE}:${TAG}" \
.
docker push "${IMAGE}:latest"
docker push "${IMAGE}:${TAG}"
- name: Log out
if: always()
run: docker logout "${REGISTRY}" || true
deploy:
# Roll the site onto the image `build` just pushed. `needs: build` makes this
# wait for a clean build and push — if the build fails, deploy never fires and
# the running container is left alone rather than torn down for nothing.
needs: build
runs-on: rgcom
# Guard against a workflow_dispatch fired from a branch: only main is deployed.
if: github.ref == 'refs/heads/main'
steps:
- name: Pull the fresh image and recreate the container
# No `down` first, deliberately. There is one service and no database to
# keep still, so `up -d` recreates it in place when the pulled digest
# differs — a couple of seconds of connection refused behind the proxy
# rather than the whole stack stopped while an image is fetched.
run: |
set -euo pipefail
cd /opt/runicgateway.com
docker compose pull
docker compose up -d --remove-orphans
docker compose ps
- name: Wait for the container to report healthy
# The image's healthcheck watches an actual response, and `npm start` runs
# the brand rewrite before the server starts — so "running" arrives well
# before "serving". Without this the job would go green on a container
# that is about to crash-loop on, say, an unwritable ./data.
run: |
set -euo pipefail
cd /opt/runicgateway.com
for attempt in $(seq 1 30); do
STATUS="$(docker compose ps --format '{{.Health}}' site | head -n 1)"
echo "attempt ${attempt}: ${STATUS:-unknown}"
if [ "$STATUS" = "healthy" ]; then
echo "Site is healthy."
exit 0
fi
sleep 5
done
echo "::error::The site did not become healthy within 150s."
docker compose logs --tail 100 site
exit 1