Merge pull request 'Feat/why is the dlr shut' (#1) from feat/why-is-the-dlr-shut into main
Build and publish container / build (push) Failing after 40s
Build and publish container / build (push) Failing after 40s
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
# Keep the build context minimal; only the site assets and nginx config
|
||||||
|
# referenced by the Dockerfile are needed.
|
||||||
|
.git
|
||||||
|
.gitea
|
||||||
|
.github
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.md
|
||||||
|
renovate.json
|
||||||
|
.renovaterc*
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: Build and publish container
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ["v*"]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Determine registry host
|
||||||
|
run: echo "REGISTRY=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Set up Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to the Gitea container registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
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@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 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.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/
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Why is the DLR shut today?
|
||||||
|
|
||||||
|
A single-page site that displays one randomly chosen message in the centre of
|
||||||
|
the screen. The message changes on every page load and whenever the
|
||||||
|
**Check again** button is pressed.
|
||||||
|
|
||||||
|
The site is themed around the Docklands Light Railway colour scheme, with a
|
||||||
|
toggle between:
|
||||||
|
|
||||||
|
- **Modern colours** — the current DLR turquoise/teal branding.
|
||||||
|
- **Original colours** — the 1987 DLR red-and-blue livery.
|
||||||
|
|
||||||
|
The chosen theme is remembered between visits via `localStorage`.
|
||||||
|
|
||||||
|
## Adding messages
|
||||||
|
|
||||||
|
Edit `messages.js` and fill the `MESSAGES` array with your own reasons — one
|
||||||
|
string per entry. Entries are inserted as plain text. Until you add some, the
|
||||||
|
page shows a fallback prompt.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
It is a static site with no build step. Open `index.html` in a browser, or
|
||||||
|
serve the directory with any static file server, for example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 -m http.server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Container
|
||||||
|
|
||||||
|
The site is packaged as a container based on `nginxinc/nginx-unprivileged`. It
|
||||||
|
runs as a non-root user and listens on port **8080**, serving the static files
|
||||||
|
and exposing a `/healthz` endpoint. It is designed to sit behind an external
|
||||||
|
reverse proxy that terminates TLS and routes by host.
|
||||||
|
|
||||||
|
Build and run locally:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build -t dlr .
|
||||||
|
docker run --rm -p 8080:8080 dlr
|
||||||
|
# then browse http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
`.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`
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
`renovate.json` configures Renovate to keep dependencies current:
|
||||||
|
|
||||||
|
- the Dockerfile base image,
|
||||||
|
- the actions used in the Gitea workflow,
|
||||||
|
- versioned front-end dependencies referenced in HTML.
|
||||||
|
|
||||||
|
There are currently no external front-end dependencies. When one is added via a
|
||||||
|
CDN, Renovate will track it if it is either annotated with a comment, e.g.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- renovate: datasource=npm depName=bootstrap -->
|
||||||
|
<link href="https://cdn.example.com/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
or referenced through a versioned jsDelivr / unpkg npm URL, which is detected
|
||||||
|
automatically.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
| ------------------------------------- | ------------------------------------------------ |
|
||||||
|
| `index.html` | Page structure. |
|
||||||
|
| `styles.css` | Both colour schemes, selected via `data-theme`. |
|
||||||
|
| `messages.js` | The list of messages (fill this in). |
|
||||||
|
| `script.js` | Random message selection and the theme toggle. |
|
||||||
|
| `Dockerfile` / `default.conf` | Container image and nginx static-serving config. |
|
||||||
|
| `.gitea/workflows/` | Gitea Actions build-and-publish pipeline. |
|
||||||
|
| `renovate.json` | Renovate dependency-update configuration. |
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Static file serving for "Why is the DLR shut today?".
|
||||||
|
# Intended to run behind an external reverse proxy (e.g. NGINX) which
|
||||||
|
# handles TLS, host routing and any X-Forwarded-* headers.
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
listen [::]:8080;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_types text/css application/javascript;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTML should always be revalidated so deploys are picked up promptly.
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
# The message list is edited frequently; do not cache it.
|
||||||
|
location = /messages.js {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Other static assets may be cached for a short period.
|
||||||
|
location ~* \.(?:css|js)$ {
|
||||||
|
add_header Cache-Control "public, max-age=3600";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health endpoint for the proxy / orchestrator.
|
||||||
|
location = /healthz {
|
||||||
|
access_log off;
|
||||||
|
default_type text/plain;
|
||||||
|
return 200 "ok\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<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="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body data-theme="modern">
|
||||||
|
<header class="controls">
|
||||||
|
<fieldset class="theme-toggle">
|
||||||
|
<legend class="sr-only">Colour scheme</legend>
|
||||||
|
<button type="button" class="theme-button" data-set-theme="modern" aria-pressed="true">
|
||||||
|
Modern colours
|
||||||
|
</button>
|
||||||
|
<button type="button" class="theme-button" data-set-theme="original" aria-pressed="false">
|
||||||
|
Original colours
|
||||||
|
</button>
|
||||||
|
</fieldset>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="stage">
|
||||||
|
<h1 class="title">Why is the DLR shut today?</h1>
|
||||||
|
<p class="message" id="message" role="status" aria-live="polite"></p>
|
||||||
|
<button type="button" class="refresh-button" id="refresh">Check again</button>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="messages.js"></script>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
/* Messages for "Why is the DLR shut today?"
|
||||||
|
*
|
||||||
|
* Fill this array with your own reasons. One string per entry.
|
||||||
|
* On every page load (and every "Check again" click) one entry is
|
||||||
|
* picked at random and shown in the centre of the screen.
|
||||||
|
*
|
||||||
|
* Keep them short — roughly a sentence — so they fit the large display
|
||||||
|
* text. HTML is NOT rendered; entries are inserted as plain text.
|
||||||
|
*
|
||||||
|
* Replace the placeholders below with your own content.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const MESSAGES = [
|
||||||
|
"PLACEHOLDER: write your first reason here",
|
||||||
|
"PLACEHOLDER: write another reason here",
|
||||||
|
// Add as many entries as you like, one per line:
|
||||||
|
// "Your reason here",
|
||||||
|
];
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended",
|
||||||
|
":dependencyDashboard",
|
||||||
|
":semanticCommits"
|
||||||
|
],
|
||||||
|
"labels": ["renovate"],
|
||||||
|
"github-actions": {
|
||||||
|
"fileMatch": ["^\\.gitea/workflows/[^/]+\\.ya?ml$"]
|
||||||
|
},
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"description": "Group nginx base image updates",
|
||||||
|
"matchManagers": ["dockerfile"],
|
||||||
|
"groupName": "docker base image"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"customManagers": [
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Update HTML dependencies annotated with a renovate comment, e.g. <!-- renovate: datasource=npm depName=bootstrap --> before the versioned URL",
|
||||||
|
"fileMatch": ["\\.html$"],
|
||||||
|
"matchStrings": [
|
||||||
|
"datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)( versioning=(?<versioning>\\S+))?[\\s\\S]*?(?<currentValue>v?\\d+\\.\\d+\\.\\d+[\\w.-]*)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Auto-detect versioned jsDelivr / unpkg npm assets in HTML",
|
||||||
|
"fileMatch": ["\\.html$"],
|
||||||
|
"matchStrings": [
|
||||||
|
"https://cdn\\.jsdelivr\\.net/npm/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)",
|
||||||
|
"https://unpkg\\.com/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)"
|
||||||
|
],
|
||||||
|
"datasourceTemplate": "npm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/* Why is the DLR shut today?
|
||||||
|
* Picks a random message on load and on demand, and handles the
|
||||||
|
* colour-scheme toggle. Messages live in messages.js (MESSAGES). */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const FALLBACK = "Add some reasons in messages.js";
|
||||||
|
const messageEl = document.getElementById("message");
|
||||||
|
const refreshButton = document.getElementById("refresh");
|
||||||
|
const themeButtons = document.querySelectorAll("[data-set-theme]");
|
||||||
|
const THEME_KEY = "dlr-theme";
|
||||||
|
|
||||||
|
function pickMessage() {
|
||||||
|
if (!Array.isArray(MESSAGES) || MESSAGES.length === 0) {
|
||||||
|
return FALLBACK;
|
||||||
|
}
|
||||||
|
const index = Math.floor(Math.random() * MESSAGES.length);
|
||||||
|
return MESSAGES[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessage() {
|
||||||
|
messageEl.textContent = pickMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(theme) {
|
||||||
|
document.body.setAttribute("data-theme", theme);
|
||||||
|
themeButtons.forEach(function (button) {
|
||||||
|
const isActive = button.dataset.setTheme === theme;
|
||||||
|
button.setAttribute("aria-pressed", String(isActive));
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
localStorage.setItem(THEME_KEY, theme);
|
||||||
|
} catch (err) {
|
||||||
|
/* localStorage unavailable; theme simply won't persist. */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTheme() {
|
||||||
|
let saved = null;
|
||||||
|
try {
|
||||||
|
saved = localStorage.getItem(THEME_KEY);
|
||||||
|
} catch (err) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
applyTheme(saved === "original" ? "original" : "modern");
|
||||||
|
}
|
||||||
|
|
||||||
|
themeButtons.forEach(function (button) {
|
||||||
|
button.addEventListener("click", function () {
|
||||||
|
applyTheme(button.dataset.setTheme);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshButton.addEventListener("click", showMessage);
|
||||||
|
|
||||||
|
initTheme();
|
||||||
|
showMessage();
|
||||||
|
})();
|
||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
/* Why is the DLR shut today?
|
||||||
|
Two colour schemes selected via the [data-theme] attribute on <body>:
|
||||||
|
- modern: current DLR turquoise/teal branding
|
||||||
|
- original: 1987 DLR red-and-blue livery */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-stack: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modern DLR — turquoise/teal */
|
||||||
|
[data-theme="modern"] {
|
||||||
|
--bg: #00afaa;
|
||||||
|
--bg-accent: #007e7a;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--text: #ffffff;
|
||||||
|
--message: #ffffff;
|
||||||
|
--button-bg: #ffffff;
|
||||||
|
--button-text: #007e7a;
|
||||||
|
--button-active-bg: #00302e;
|
||||||
|
--button-active-text: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Original DLR — 1987 red and blue */
|
||||||
|
[data-theme="original"] {
|
||||||
|
--bg: #002b5c;
|
||||||
|
--bg-accent: #c8102e;
|
||||||
|
--surface: #f5f0e1;
|
||||||
|
--text: #f5f0e1;
|
||||||
|
--message: #f5f0e1;
|
||||||
|
--button-bg: #c8102e;
|
||||||
|
--button-text: #f5f0e1;
|
||||||
|
--button-active-bg: #f5f0e1;
|
||||||
|
--button-active-text: #002b5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: var(--font-stack);
|
||||||
|
color: var(--text);
|
||||||
|
background: linear-gradient(135deg, var(--bg) 0%, var(--bg-accent) 100%);
|
||||||
|
transition: background 0.4s ease, color 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-button {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-button[aria-pressed="true"] {
|
||||||
|
background: var(--button-active-bg);
|
||||||
|
color: var(--button-active-text);
|
||||||
|
border-color: var(--button-active-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: clamp(1.5rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
font-size: clamp(1.75rem, 6vw, 4rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 22ch;
|
||||||
|
color: var(--message);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.75rem 1.75rem;
|
||||||
|
border: 2px solid var(--button-bg);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--button-bg);
|
||||||
|
color: var(--button-text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.1s ease, opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user