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
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:
48
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
48
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal 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.
|
||||
-->
|
||||
11
.gitea/ISSUE_TEMPLATE/config.yaml
Normal file
11
.gitea/ISSUE_TEMPLATE/config.yaml
Normal 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.
|
||||
38
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
38
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal 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. -->
|
||||
42
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
42
.gitea/PULL_REQUEST_TEMPLATE.md
Normal 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.
|
||||
150
.gitea/workflows/build-image.yml
Normal file
150
.gitea/workflows/build-image.yml
Normal 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
|
||||
Reference in New Issue
Block a user