22 Commits

Author SHA1 Message Date
lyrathorpe 8bb782287e Merge pull request 'fix(deps): pin dependencies' (#7) from renovate/pin-dependencies into main
Build and publish container / build (push) Successful in 9m52s
Reviewed-on: #7
2026-06-12 12:08:23 +01:00
Renovate Bot 56ec85c17e fix(deps): pin dependencies
Build and publish container / build (pull_request) Successful in 7m56s
2026-06-12 10:58:16 +00:00
lyrathorpe 9412c20248 Merge pull request 'Fix/renovate semantic fix' (#14) from fix/renovate-semantic-fix into main
Build and publish container / build (push) Has been cancelled
Reviewed-on: #14
2026-06-12 11:49:53 +01:00
lyrathorpe 65f97d2707 Merge pull request 'feat: present theme switch as a segmented control' (#15) from feat/segmented-theme-switch into main
Build and publish container / build (push) Successful in 6m7s
Reviewed-on: #15
2026-06-12 11:45:08 +01:00
Emma Thorpe 564070b270 ci: auto-merge patch and minor renovate updates
Build and publish container / build (pull_request) Successful in 4m55s
Add a packageRule that automerges patch and minor updates, with
platformAutomerge enabled so Gitea merges them once required checks pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:39:02 +01:00
Emma Thorpe ad90451846 feat: present theme switch as a segmented control
Build and publish container / build (pull_request) Successful in 4m49s
Join the two theme options into a single rounded container with the active
segment filled and a divider between them, replacing the two separate pills.
Markup and aria-pressed behaviour are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:34:32 +01:00
Emma Thorpe 22c2ef1b26 fix: force fix(deps) commit type so renovate PRs trigger releases
Build and publish container / build (pull_request) Successful in 6m16s
config:recommended pulls in :semanticPrefixFixDepsChoreOthers, whose catch-all
packageRule forces non-npm updates (Docker base image, Gitea Actions) to the
chore type, overriding the top-level semanticCommitType. chore produces no
release, so Renovate updates were never auto-tagged.

Append a packageRule matching all packages that sets semanticCommitType to fix,
so every merged Renovate PR registers as a patch and is released and tagged.
Also migrate the deprecated fileMatch keys to managerFilePatterns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:31:07 +01:00
Emma Thorpe 301120c174 fix: recolour original theme to a red field with white text
Switch the original theme to a red background with white message text, with
navy buttons, evoking the red B07/B99 stock. Red/white/blue are all retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:24:30 +01:00
renovate-bot 1a0ffead5a Merge pull request 'chore(deps): update docker base image to v1.31' (#8) from renovate/docker-base-image into main
Build and publish container / build (push) Successful in 6m21s
2026-06-12 01:14:42 +01:00
Renovate Bot 719956341c chore(deps): update docker base image to v1.31
Build and publish container / build (pull_request) Successful in 4m38s
2026-06-12 00:02:27 +00:00
lyrathorpe 2a1e6dc8a4 Merge pull request 'feat(messages): add initial messages' (#6) from feat/reasons-why into main
Build and publish container / build (push) Successful in 5m59s
Reviewed-on: #6
2026-06-11 21:08:15 +01:00
lyrathorpe 6b19a55655 feat(messages): add initial messages
Build and publish container / build (pull_request) Successful in 4m22s
more added later on
2026-06-11 21:03:31 +01:00
lyrathorpe a2b9d445e4 Merge pull request 'Fix/theme colours' (#5) from fix/theme-colours into main
Build and publish container / build (push) Successful in 6m18s
Reviewed-on: #5
2026-06-11 17:49:40 +01:00
Emma Thorpe a14306cce4 fix: recolour original theme to a cream field with blue text
Build and publish container / build (pull_request) Successful in 4m51s
Use a cream/white background with navy message text and red buttons, so the
original red/white/blue livery presents blue text rather than white text on a
blue field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:41:55 +01:00
Emma Thorpe 43f42a8274 fix: use solid theme backgrounds instead of a gradient
Replace the body background gradient with the solid theme colour and remove
the now-unused --bg-accent variables from both colour schemes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:36:47 +01:00
lyrathorpe 8376860fb4 Merge pull request 'fix: build multi-arch images for amd64 and arm64' (#4) from fix/multi-arch-build into main
Build and publish container / build (push) Successful in 6m51s
Reviewed-on: #4
2026-06-11 17:26:57 +01:00
Emma Thorpe d1803f06dd fix: build multi-arch images for amd64 and arm64
Build and publish container / build (pull_request) Successful in 5m2s
Add QEMU setup and build for linux/amd64 and linux/arm64 (armv8), publishing
a single multi-arch manifest. The nginx-unprivileged base image provides both
architectures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:20:20 +01:00
lyrathorpe ea16d1b21b Merge pull request 'ci: tag images by semver and point latest at newest release' (#3) from ci/image-tagging into main
Build and publish container / build (push) Successful in 3m32s
Reviewed-on: #3
2026-06-11 17:10:26 +01:00
Emma Thorpe 99084cc597 feat: auto-release images from conventional commits on main
Build and publish container / build (pull_request) Successful in 2m58s
On each push to main, derive the next semantic version from the
conventional-commit messages since the last v* tag (feat -> minor,
fix/perf -> patch, \! or BREAKING CHANGE -> major) and, when a release is
warranted, build and publish the image tagged X.Y.Z, X.Y, X and latest,
then record an annotated vX.Y.Z tag for the next computation. Non-release
pushes publish a sha-<short> image only.

Configure Renovate to commit updates as fix(deps): so each merged Renovate
PR registers as a patch change and is released and tagged automatically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:04:40 +01:00
Emma Thorpe d296d88c4c ci: tag images by semver and point latest at newest release
Build and publish container / build (pull_request) Successful in 1m59s
Replace the raw latest-on-default-branch tag, which moved latest on every
main push, with metadata-action's latest=auto flavor so latest follows the
newest non-prerelease v* release. Add a {{major}} tag alongside the
existing version and major.minor semver tags; branch and SHA tags remain
for traceability of non-release builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:43:33 +01:00
lyrathorpe 17d10ce1a0 Merge pull request 'fix: authenticate to container registry with a package-scoped PAT' (#2) from fix/ci-registry-auth into main
Build and publish container / build (push) Successful in 2m39s
Reviewed-on: #2
2026-06-11 16:36:30 +01:00
Emma Thorpe 7549aa6c90 fix: authenticate to container registry with a package-scoped PAT
Build and publish container / build (pull_request) Successful in 2m7s
The auto-provided GITEA_TOKEN does not carry container registry write
permission on most Gitea instances, causing docker login to fail with
"unauthorized". Use a Personal Access Token supplied via the
PACKAGES_TOKEN secret, with the package namespace owner as the username.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:31:32 +01:00
6 changed files with 182 additions and 51 deletions
+102 -23
View File
@@ -3,51 +3,130 @@ name: Build and publish container
on: on:
push: push:
branches: [main] branches: [main]
tags: ["v*"]
pull_request: pull_request:
branches: [main] branches: [main]
defaults:
run:
shell: bash
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: write
packages: write packages: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
# Full history and tags are required to derive the next version
# from the conventional-commit messages since the last release.
fetch-depth: 0
- name: Determine registry host - name: Determine registry host
run: echo "REGISTRY=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_ENV" 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@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Set up Buildx - name: Set up Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Log in to the Gitea container registry - name: Log in to the Gitea container registry
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v3 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITEA_TOKEN }} password: ${{ secrets.PACKAGES_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 - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.version.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} 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"
+1 -1
View File
@@ -1,7 +1,7 @@
# Lightweight, non-root nginx serving the static site. # Lightweight, non-root nginx serving the static site.
# Runs as user "nginx" and listens on 8080, ready to sit behind an # Runs as user "nginx" and listens on 8080, ready to sit behind an
# external reverse proxy that terminates TLS and forwards requests. # external reverse proxy that terminates TLS and forwards requests.
FROM nginxinc/nginx-unprivileged:1.27-alpine-slim FROM nginxinc/nginx-unprivileged:1.31-alpine-slim@sha256:6616de6eaa82bc2ee3541fa287a8fca7dc7271e6374e9402014dbd13f4a980ae
COPY default.conf /etc/nginx/conf.d/default.conf 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/ COPY --chown=nginx:nginx index.html styles.css script.js messages.js /usr/share/nginx/html/
+35 -6
View File
@@ -45,13 +45,38 @@ docker run --rm -p 8080:8080 dlr
## CI ## CI
`.gitea/workflows/build-and-publish.yml` builds the container with Gitea Actions `.gitea/workflows/build-and-publish.yml` builds the container with Gitea Actions
and publishes it to this Gitea instance's container registry on pushes to `main` on every push to `main` and on pull requests. Pull requests build the image but
and on `v*` tags. Pull requests build the image but do not push. Authentication do not push. The registry host is derived from the Gitea server URL. Images are
uses the automatically provided `GITEA_TOKEN`; the registry host is derived from built for `linux/amd64` and `linux/arm64` (armv8) and published as a single
the Gitea server URL. multi-arch manifest; the arm64 build runs under QEMU emulation.
The published image is `<gitea-host>/<owner>/<repo>`, tagged by branch, semver Authentication requires a Personal Access Token with package read/write scope,
(for `v*` tags), commit SHA, and `latest` on the default branch. 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.
## Dependency updates ## Dependency updates
@@ -72,6 +97,10 @@ 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 or referenced through a versioned jsDelivr / unpkg npm URL, which is detected
automatically. 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 ## Files
| File | Purpose | | File | Purpose |
+9 -4
View File
@@ -11,8 +11,13 @@
*/ */
const MESSAGES = [ const MESSAGES = [
"PLACEHOLDER: write your first reason here", "Maggie came back, she was unimpressed",
"PLACEHOLDER: write another reason here", "They mixed up the B23s and the 2024 tube stock",
// Add as many entries as you like, one per line: "The computer went on strike",
// "Your reason here", "Leaves on the track",
"Escalators broke at Cutty Sark",
"EHRC decided it was woke",
"JK Rowling",
"Kaiju",
"28 Days Later happened",
]; ];
+17 -3
View File
@@ -6,21 +6,35 @@
":semanticCommits" ":semanticCommits"
], ],
"labels": ["renovate"], "labels": ["renovate"],
"semanticCommits": "enabled",
"semanticCommitType": "fix",
"semanticCommitScope": "deps",
"github-actions": { "github-actions": {
"fileMatch": ["^\\.gitea/workflows/[^/]+\\.ya?ml$"] "managerFilePatterns": ["/^\\.gitea/workflows/[^/]+\\.ya?ml$/"]
}, },
"packageRules": [ "packageRules": [
{ {
"description": "Group nginx base image updates", "description": "Group nginx base image updates",
"matchManagers": ["dockerfile"], "matchManagers": ["dockerfile"],
"groupName": "docker base image" "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": [ "customManagers": [
{ {
"customType": "regex", "customType": "regex",
"description": "Update HTML dependencies annotated with a renovate comment, e.g. <!-- renovate: datasource=npm depName=bootstrap --> before the versioned URL", "description": "Update HTML dependencies annotated with a renovate comment, e.g. <!-- renovate: datasource=npm depName=bootstrap --> before the versioned URL",
"fileMatch": ["\\.html$"], "managerFilePatterns": ["/\\.html$/"],
"matchStrings": [ "matchStrings": [
"datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)( versioning=(?<versioning>\\S+))?[\\s\\S]*?(?<currentValue>v?\\d+\\.\\d+\\.\\d+[\\w.-]*)" "datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)( versioning=(?<versioning>\\S+))?[\\s\\S]*?(?<currentValue>v?\\d+\\.\\d+\\.\\d+[\\w.-]*)"
] ]
@@ -28,7 +42,7 @@
{ {
"customType": "regex", "customType": "regex",
"description": "Auto-detect versioned jsDelivr / unpkg npm assets in HTML", "description": "Auto-detect versioned jsDelivr / unpkg npm assets in HTML",
"fileMatch": ["\\.html$"], "managerFilePatterns": ["/\\.html$/"],
"matchStrings": [ "matchStrings": [
"https://cdn\\.jsdelivr\\.net/npm/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)", "https://cdn\\.jsdelivr\\.net/npm/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)",
"https://unpkg\\.com/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)" "https://unpkg\\.com/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)"
+18 -14
View File
@@ -10,7 +10,6 @@
/* Modern DLR — turquoise/teal */ /* Modern DLR — turquoise/teal */
[data-theme="modern"] { [data-theme="modern"] {
--bg: #00afaa; --bg: #00afaa;
--bg-accent: #007e7a;
--surface: #ffffff; --surface: #ffffff;
--text: #ffffff; --text: #ffffff;
--message: #ffffff; --message: #ffffff;
@@ -20,14 +19,13 @@
--button-active-text: #ffffff; --button-active-text: #ffffff;
} }
/* Original DLR — 1987 red and blue */ /* Original DLR — 1987 red, white and blue */
[data-theme="original"] { [data-theme="original"] {
--bg: #002b5c; --bg: #c8102e;
--bg-accent: #c8102e; --surface: #002b5c;
--surface: #f5f0e1;
--text: #f5f0e1; --text: #f5f0e1;
--message: #f5f0e1; --message: #f5f0e1;
--button-bg: #c8102e; --button-bg: #002b5c;
--button-text: #f5f0e1; --button-text: #f5f0e1;
--button-active-bg: #f5f0e1; --button-active-bg: #f5f0e1;
--button-active-text: #002b5c; --button-active-text: #002b5c;
@@ -44,7 +42,7 @@ body {
flex-direction: column; flex-direction: column;
font-family: var(--font-stack); font-family: var(--font-stack);
color: var(--text); color: var(--text);
background: linear-gradient(135deg, var(--bg) 0%, var(--bg-accent) 100%); background: var(--bg);
transition: background 0.4s ease, color 0.4s ease; transition: background 0.4s ease, color 0.4s ease;
} }
@@ -65,10 +63,13 @@ body {
padding: 1.5rem; padding: 1.5rem;
} }
/* Segmented control: both options joined in one rounded container,
with the active segment filled. */
.theme-toggle { .theme-toggle {
display: flex; display: inline-flex;
gap: 0.5rem; border: 2px solid var(--button-bg);
border: none; border-radius: 999px;
overflow: hidden;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
@@ -77,19 +78,22 @@ body {
font-family: inherit; font-family: inherit;
font-size: 0.9rem; font-size: 0.9rem;
font-weight: 600; font-weight: 600;
padding: 0.5rem 1rem; padding: 0.5rem 1.1rem;
border: 2px solid var(--button-bg); border: none;
border-radius: 999px;
background: transparent; background: transparent;
color: var(--text); color: var(--text);
cursor: pointer; cursor: pointer;
transition: background 0.2s ease, color 0.2s ease; 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"] { .theme-button[aria-pressed="true"] {
background: var(--button-active-bg); background: var(--button-active-bg);
color: var(--button-active-text); color: var(--button-active-text);
border-color: var(--button-active-bg);
} }
.stage { .stage {