7 Commits

Author SHA1 Message Date
Emma Thorpe 5fc2292e8f docs: document container, CI and dependency updates
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:07:10 +01:00
Emma Thorpe d249656be8 build: add Renovate configuration
Track the Dockerfile base image, the actions used in the Gitea workflow,
and versioned front-end dependencies referenced in HTML (via renovate
comment annotations or jsDelivr/unpkg npm URLs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:07:10 +01:00
Emma Thorpe 3941429bb1 ci: build and publish the container via Gitea Actions
Build the image on pushes to main, version tags and pull requests, and
push to the Gitea container registry (except on PRs) using the
auto-provided GITEA_TOKEN. Tags are derived with docker/metadata-action.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:07:10 +01:00
Emma Thorpe 40c6af5326 feat: containerise the site with nginx-unprivileged
Serve the static site from a non-root nginx image listening on 8080,
with cache headers, gzip and a /healthz endpoint. Designed to run behind
an external reverse proxy that terminates TLS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:07:10 +01:00
Emma Thorpe 499488e355 docs: add README
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:59:39 +01:00
Emma Thorpe 9f98c371b0 feat: add random message selection and theme toggle
script.js picks a random entry from MESSAGES on load and on each "Check
again" click, and persists the chosen colour scheme in localStorage.
messages.js holds the MESSAGES array as a placeholder template to be
filled in with content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:59:39 +01:00
Emma Thorpe 467eee25c2 feat: add page structure and DLR colour schemes
Single-page layout with a centred message, a colour-scheme toggle, and a
"Check again" button. styles.css defines two palettes selected via the
data-theme attribute: modern DLR turquoise/teal and the original 1987
red-and-blue livery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:59:39 +01:00
7 changed files with 55 additions and 213 deletions
+23 -122
View File
@@ -1,152 +1,53 @@
name: Build and publish container
on:
# On merge to main, only build/release when image-affecting files change;
# CI-config, Renovate-config and docs changes do not produce a new image.
push:
branches: [main]
paths:
- 'Dockerfile'
- 'default.conf'
- 'index.html'
- 'styles.css'
- 'script.js'
- 'messages.js'
- '.dockerignore'
# Pull requests always run (the build is a required check); no path filter.
tags: ["v*"]
pull_request:
branches: [main]
workflow_dispatch:
# A newer run cancels an older in-flight run in the same group (keyed by ref),
# so a fresh merge to main supersedes the previous build and only the latest
# release is produced, avoiding tags that would be immediately replaced. Each
# pull request likewise supersedes only its own earlier runs.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
# Full history and tags are required to derive the next version
# from the conventional-commit messages since the last release.
fetch-depth: 0
uses: actions/checkout@v4
- name: Determine registry host
run: echo "REGISTRY=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_ENV"
# Derive the release version from conventional commits since the last
# v* tag: feat -> minor, fix/perf -> patch, ! or BREAKING CHANGE -> major.
# Anything else (chore, ci, docs, build) produces no release; those builds
# are published under a sha-<short> tag only.
- name: Compute version and image tags
id: version
run: |
set -euo pipefail
image="${REGISTRY}/${GITHUB_REPOSITORY,,}"
last_tag="$(git tag --list 'v*' --sort=-v:refname | head -n1 || true)"
if [ -n "$last_tag" ]; then
range="${last_tag}..HEAD"
base="${last_tag#v}"
else
range=""
base="0.0.0"
fi
subjects="$(git log ${range} --format='%s')"
bodies="$(git log ${range} --format='%B')"
bump="none"
if printf '%s\n' "$bodies" | grep -qiE 'BREAKING[ -]CHANGE' \
|| printf '%s\n' "$subjects" | grep -qE '^[a-z]+([(][^)]*[)])?!:'; then
bump="major"
elif printf '%s\n' "$subjects" | grep -qE '^feat([(][^)]*[)])?:'; then
bump="minor"
elif printf '%s\n' "$subjects" | grep -qE '^(fix|perf)([(][^)]*[)])?:'; then
bump="patch"
fi
major="${base%%.*}"
rest="${base#*.}"
minor="${rest%%.*}"
patch="${rest##*.}"
release="false"
if [ "${GITHUB_EVENT_NAME}" = "push" ] && [ "$bump" != "none" ]; then
release="true"
case "$bump" in
major) major=$((major + 1)); minor=0; patch=0 ;;
minor) minor=$((minor + 1)); patch=0 ;;
patch) patch=$((patch + 1)) ;;
esac
version="${major}.${minor}.${patch}"
{
echo "tags<<__EOT__"
echo "${image}:${version}"
echo "${image}:${major}.${minor}"
echo "${image}:${major}"
echo "${image}:latest"
echo "__EOT__"
} >> "$GITHUB_OUTPUT"
echo "version=${version}" >> "$GITHUB_OUTPUT"
else
short="$(git rev-parse --short HEAD)"
{
echo "tags<<__EOT__"
echo "${image}:sha-${short}"
echo "__EOT__"
} >> "$GITHUB_OUTPUT"
fi
echo "release=${release}" >> "$GITHUB_OUTPUT"
echo "Computed bump=${bump}, release=${release}, base=${base}"
- name: Set up QEMU
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4
- name: Set up Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
uses: docker/setup-buildx-action@v3
- name: Log in to the Gitea container registry
if: github.event_name != 'pull_request'
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.PACKAGES_TOKEN }}
username: ${{ github.actor }}
password: ${{ secrets.GITEA_TOKEN }}
- name: Extract image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.version.outputs.tags }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
# Record the release as an annotated git tag so the next run computes the
# following version from it. This push does not re-trigger the workflow,
# which only listens on the main branch and pull requests.
- name: Tag the release
if: steps.version.outputs.release == 'true'
run: |
set -euo pipefail
v="v${{ steps.version.outputs.version }}"
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.${REGISTRY}"
git tag -a "$v" -m "$v"
git push origin "$v"
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+1 -1
View File
@@ -1,7 +1,7 @@
# Lightweight, non-root nginx serving the static site.
# Runs as user "nginx" and listens on 8080, ready to sit behind an
# external reverse proxy that terminates TLS and forwards requests.
FROM nginxinc/nginx-unprivileged:1.31-alpine-slim@sha256:6616de6eaa82bc2ee3541fa287a8fca7dc7271e6374e9402014dbd13f4a980ae
FROM nginxinc/nginx-unprivileged:1.27-alpine-slim
COPY default.conf /etc/nginx/conf.d/default.conf
COPY --chown=nginx:nginx index.html styles.css script.js messages.js /usr/share/nginx/html/
+6 -35
View File
@@ -45,38 +45,13 @@ docker run --rm -p 8080:8080 dlr
## CI
`.gitea/workflows/build-and-publish.yml` builds the container with Gitea Actions
on every push to `main` and on pull requests. Pull requests build the image but
do not push. The registry host is derived from the Gitea server URL. Images are
built for `linux/amd64` and `linux/arm64` (armv8) and published as a single
multi-arch manifest; the arm64 build runs under QEMU emulation.
and publishes it to this Gitea instance's container registry on pushes to `main`
and on `v*` tags. Pull requests build the image but do not push. Authentication
uses the automatically provided `GITEA_TOKEN`; the registry host is derived from
the Gitea server URL.
Authentication requires a Personal Access Token with package read/write scope,
because the automatically provided `GITEA_TOKEN` does not carry container
registry write permission on most Gitea instances. Create the token under an
account with write access to the target package namespace, then store it as a
repository Actions secret named `PACKAGES_TOKEN`.
### Automatic releases
The published image is `<gitea-host>/<owner>/<repo>`. Releases are derived from
[Conventional Commits](https://www.conventionalcommits.org/). On each push to
`main`, the workflow inspects the commits since the last `v*` tag and computes
the next version:
- `feat:` → minor bump,
- `fix:` / `perf:` → patch bump,
- `!` or `BREAKING CHANGE` → major bump,
- anything else (`chore`, `ci`, `docs`, `build`) → no release.
When a release is warranted, the image is published with `X.Y.Z`, `X.Y`, `X` and
`latest` tags, and the workflow records an annotated `vX.Y.Z` git tag so the next
release is computed from it. Pushes to `main` that warrant no release are
published under a `sha-<short>` tag only, so `latest` always points at the most
recent release rather than the newest commit.
Recording the release tag requires the workflow's `contents: write` permission;
if the instance forbids the automatic token from pushing, supply a PAT with
repository write scope and push the tag with it instead.
The published image is `<gitea-host>/<owner>/<repo>`, tagged by branch, semver
(for `v*` tags), commit SHA, and `latest` on the default branch.
## Dependency updates
@@ -97,10 +72,6 @@ CDN, Renovate will track it if it is either annotated with a comment, e.g.
or referenced through a versioned jsDelivr / unpkg npm URL, which is detected
automatically.
Renovate is configured to commit updates as `fix(deps): …`. Each merged Renovate
PR therefore registers as a patch-level change, so the release workflow above
cuts a new patch release and tags the image automatically.
## Files
| File | Purpose |
-3
View File
@@ -4,9 +4,6 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Why is the DLR shut today?</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Rokkitt:wght@400;700;800&display=swap">
<link rel="stylesheet" href="styles.css">
</head>
<body data-theme="modern">
+4 -9
View File
@@ -11,13 +11,8 @@
*/
const MESSAGES = [
"Maggie came back, she was unimpressed",
"They mixed up the B23s and the 2024 tube stock",
"The computer went on strike",
"Leaves on the track",
"Escalators broke at Cutty Sark",
"EHRC decided it was woke",
"JK Rowling",
"Kaiju",
"28 Days Later happened",
"PLACEHOLDER: write your first reason here",
"PLACEHOLDER: write another reason here",
// Add as many entries as you like, one per line:
// "Your reason here",
];
+3 -17
View File
@@ -6,35 +6,21 @@
":semanticCommits"
],
"labels": ["renovate"],
"semanticCommits": "enabled",
"semanticCommitType": "fix",
"semanticCommitScope": "deps",
"github-actions": {
"managerFilePatterns": ["/^\\.gitea/workflows/[^/]+\\.ya?ml$/"]
"fileMatch": ["^\\.gitea/workflows/[^/]+\\.ya?ml$"]
},
"packageRules": [
{
"description": "Group nginx base image updates",
"matchManagers": ["dockerfile"],
"groupName": "docker base image"
},
{
"description": "Commit every update as fix(deps) so each merged Renovate PR triggers a patch release. config:recommended pulls in :semanticPrefixFixDepsChoreOthers, which forces non-npm updates (Docker, Actions) to chore and would otherwise produce no release.",
"matchPackageNames": ["*"],
"semanticCommitType": "fix"
},
{
"description": "Auto-merge patch and minor updates once checks pass.",
"matchUpdateTypes": ["patch", "minor"],
"automerge": true
}
],
"platformAutomerge": true,
"customManagers": [
{
"customType": "regex",
"description": "Update HTML dependencies annotated with a renovate comment, e.g. <!-- renovate: datasource=npm depName=bootstrap --> before the versioned URL",
"managerFilePatterns": ["/\\.html$/"],
"fileMatch": ["\\.html$"],
"matchStrings": [
"datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)( versioning=(?<versioning>\\S+))?[\\s\\S]*?(?<currentValue>v?\\d+\\.\\d+\\.\\d+[\\w.-]*)"
]
@@ -42,7 +28,7 @@
{
"customType": "regex",
"description": "Auto-detect versioned jsDelivr / unpkg npm assets in HTML",
"managerFilePatterns": ["/\\.html$/"],
"fileMatch": ["\\.html$"],
"matchStrings": [
"https://cdn\\.jsdelivr\\.net/npm/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)",
"https://unpkg\\.com/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)"
+18 -26
View File
@@ -10,6 +10,7 @@
/* Modern DLR — turquoise/teal */
[data-theme="modern"] {
--bg: #00afaa;
--bg-accent: #007e7a;
--surface: #ffffff;
--text: #ffffff;
--message: #ffffff;
@@ -19,20 +20,17 @@
--button-active-text: #ffffff;
}
/* Original DLR — 1987 P86/P89 livery: blue body and skirt, red band and
doors, white stripes. Blue is the dominant field, red the accent.
1987 branding used the Rockwell slab serif; Rokkitt is a free fallback for
systems without Rockwell installed. */
/* Original DLR — 1987 red and blue */
[data-theme="original"] {
--font-stack: Rockwell, "Rockwell Nova", Rokkitt, Georgia, serif;
--bg: #1f3a93;
--surface: #c8102e;
--text: #ffffff;
--message: #ffffff;
--bg: #002b5c;
--bg-accent: #c8102e;
--surface: #f5f0e1;
--text: #f5f0e1;
--message: #f5f0e1;
--button-bg: #c8102e;
--button-text: #ffffff;
--button-active-bg: #ffffff;
--button-active-text: #1f3a93;
--button-text: #f5f0e1;
--button-active-bg: #f5f0e1;
--button-active-text: #002b5c;
}
* {
@@ -46,7 +44,7 @@ body {
flex-direction: column;
font-family: var(--font-stack);
color: var(--text);
background: var(--bg);
background: linear-gradient(135deg, var(--bg) 0%, var(--bg-accent) 100%);
transition: background 0.4s ease, color 0.4s ease;
}
@@ -67,13 +65,10 @@ body {
padding: 1.5rem;
}
/* Segmented control: both options joined in one rounded container,
with the active segment filled. */
.theme-toggle {
display: inline-flex;
border: 2px solid var(--button-bg);
border-radius: 999px;
overflow: hidden;
display: flex;
gap: 0.5rem;
border: none;
margin: 0;
padding: 0;
}
@@ -82,22 +77,19 @@ body {
font-family: inherit;
font-size: 0.9rem;
font-weight: 600;
padding: 0.5rem 1.1rem;
border: none;
padding: 0.5rem 1rem;
border: 2px solid var(--button-bg);
border-radius: 999px;
background: transparent;
color: var(--text);
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
}
/* Divider between the two segments. */
.theme-button + .theme-button {
border-left: 2px solid var(--button-bg);
}
.theme-button[aria-pressed="true"] {
background: var(--button-active-bg);
color: var(--button-active-text);
border-color: var(--button-active-bg);
}
.stage {