G2.3: Windy Git branding — and get it out of one host's disk into the repo
The front end was 100% stock Gitea: green teacup, "Gitea: Git with a cup of tea", "A painless, self-hosted Git service". G2.3 was specified in the plan with an acceptance test and never executed, and nothing enforced it. Now: Windy Git name, wind-mark logo, brand-blue accent, and a landing page that says what this actually is. Uses Gitea's SUPPORTED surface (custom templates + public assets) so upstream upgrades keep arriving — no source modified (D-2/I-1). Two traps this cost, both now documented and tested: 1. GITEA__DEFAULT__APP_NAME does not work. Gitea reads APP_NAME from the TOP LEVEL of app.ini; the env var created a literal [default] section that Gitea ignores, so the installer's stock APP_NAME kept winning while the config looked correct. The env-to-ini pass also APPENDED a second APP_NAME rather than replacing the first — a new variant of the documented G4A.3 trap. 2. Cloudflare caches /assets/* for 6h and no token in this stack can purge, so the new logo and CSS were invisible while being correct at origin. Brand assets now carry a VERSION IN THE FILENAME; bump it on every change. Committed with an idempotent apply.sh, because applying it straight to Veron's disk first was itself the config-drift trap this project documents: a rebuild would have silently reverted to stock Gitea. 85 tests green. Co-Authored-By: Claude (Fable 5) <noreply@anthropic.com>
This commit is contained in:
@@ -703,3 +703,28 @@ def test_i12_build_fails_when_commit_sha_is_empty():
|
||||
and it happened on 2026-08-14."""
|
||||
df = (ROOT / "Dockerfile").read_text()
|
||||
assert 'test -n "${COMMIT_SHA}"' in df
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# G2.3 — branding lives in the repo, not only on one host's disk
|
||||
# --------------------------------------------------------------------------
|
||||
def test_g23_branding_is_version_controlled():
|
||||
"""It was applied directly to Veron's disk first, which is the config-drift
|
||||
trap this project documents: the running system and the repo disagree, and
|
||||
a rebuild silently reverts to stock Gitea."""
|
||||
b = ROOT / "deploy" / "branding"
|
||||
for f in ("apply.sh", "README.md", "templates/home.tmpl",
|
||||
"templates/custom/header.tmpl"):
|
||||
assert (b / f).exists(), f"missing {f}"
|
||||
|
||||
|
||||
def test_g23_brand_css_filename_is_versioned():
|
||||
"""Cloudflare caches /assets/* for 6h and no token here can purge, so a
|
||||
fixed filename leaves stale bytes live for hours."""
|
||||
import re as _re
|
||||
|
||||
hdr = (ROOT / "deploy" / "branding" / "templates" / "custom" / "header.tmpl").read_text()
|
||||
m = _re.search(r"theme-windy\.v(\d+)\.css", hdr)
|
||||
assert m, "brand CSS must carry a version in its FILENAME"
|
||||
assert (ROOT / "deploy" / "branding" / "public" / "assets" / "css"
|
||||
/ f"theme-windy.v{m.group(1)}.css").exists()
|
||||
|
||||
28
deploy/branding/README.md
Normal file
28
deploy/branding/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Windy Git branding (G2.3)
|
||||
|
||||
Gitea's **supported** customisation surface: custom templates and public assets.
|
||||
No Gitea source is modified, so upstream upgrades keep arriving (D-2, I-1).
|
||||
|
||||
deploy/branding/ → $GITEA_CUSTOM (/data/gitea) in the container
|
||||
→ /srv/windygit/git/gitea/ on Veron 1
|
||||
|
||||
Apply with `./deploy/branding/apply.sh`.
|
||||
|
||||
## Two traps this cost, both worth knowing
|
||||
|
||||
**1. `GITEA__DEFAULT__APP_NAME` does not work.** Gitea reads `APP_NAME` from the
|
||||
*top level* of `app.ini` (before any `[section]`). The env var instead created a
|
||||
literal `[default]` section, which Gitea ignores — and the installer's stock
|
||||
`APP_NAME` kept winning, so the site said "Gitea: Git with a cup of tea" while
|
||||
the config looked correct. Worse, the env-to-ini pass **appended** a second
|
||||
`APP_NAME` rather than replacing the first. `apply.sh` sets it at the top level
|
||||
directly.
|
||||
|
||||
**2. Cloudflare caches `/assets/*` for 6 hours and no token in this stack can
|
||||
purge.** Editing a fixed filename leaves the old bytes live for hours — the new
|
||||
logo and CSS were both invisible while being correct at origin. **Version the
|
||||
filename** (`theme-windy.v2.css`) on every brand change; a `?query` is not
|
||||
enough because some caches ignore it.
|
||||
|
||||
The nav logo is swapped in CSS rather than by overriding Gitea's navbar
|
||||
template — a one-line rule instead of a forked template that would drift.
|
||||
23
deploy/branding/apply.sh
Executable file
23
deploy/branding/apply.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# Apply Windy Git branding to the Gitea custom tree. Idempotent.
|
||||
set -euo pipefail
|
||||
CUSTOM="${GITEA_CUSTOM_HOST:-/srv/windygit/git/gitea}"
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
sudo mkdir -p "$CUSTOM"/templates/custom "$CUSTOM"/public/assets/img "$CUSTOM"/public/assets/css
|
||||
sudo cp -r "$HERE"/templates/. "$CUSTOM"/templates/
|
||||
sudo cp -r "$HERE"/public/. "$CUSTOM"/public/
|
||||
sudo chown -R 1000:1000 "$CUSTOM"/templates "$CUSTOM"/public
|
||||
|
||||
# APP_NAME must sit at the TOP LEVEL. See README trap #1.
|
||||
INI="$CUSTOM/conf/app.ini"
|
||||
sudo cp "$INI" "$INI.bak-brand-$(date +%s)"
|
||||
sudo python3 - "$INI" <<'PY'
|
||||
import sys
|
||||
p = sys.argv[1]
|
||||
lines = [l for l in open(p).read().splitlines()
|
||||
if not l.strip().startswith(("APP_NAME", "APP_SLOGAN"))]
|
||||
lines.insert(0, "APP_NAME = Windy Git")
|
||||
open(p, "w").write("\n".join(lines) + "\n")
|
||||
PY
|
||||
echo "applied. restart gitea to pick it up."
|
||||
27
deploy/branding/public/assets/css/theme-windy.v2.css
Normal file
27
deploy/branding/public/assets/css/theme-windy.v2.css
Normal file
@@ -0,0 +1,27 @@
|
||||
/* Windy Git brand accent. Layered on top of Gitea's theme rather than
|
||||
replacing it, so upstream theme fixes keep arriving. */
|
||||
:root {
|
||||
--color-primary: #0ea5e9;
|
||||
--color-primary-dark-1: #0284c7;
|
||||
--color-primary-dark-2: #0369a1;
|
||||
--color-primary-light-1: #38bdf8;
|
||||
--color-primary-light-2: #7dd3fc;
|
||||
}
|
||||
.wg-hero { max-width: 780px; margin: 4rem auto 2rem; padding: 0 1.5rem; text-align: center; }
|
||||
.wg-hero h1 { font-size: 2.6rem; margin: 1.2rem 0 .4rem; letter-spacing: -.02em; }
|
||||
.wg-hero .wg-sub { font-size: 1.15rem; opacity: .78; margin-bottom: 2.2rem; }
|
||||
.wg-grid { display: grid; gap: 1.1rem; grid-template-columns: repeat(auto-fit,minmax(230px,1fr));
|
||||
max-width: 900px; margin: 0 auto 2.5rem; padding: 0 1.5rem; text-align: left; }
|
||||
.wg-card { border: 1px solid var(--color-secondary); border-radius: 8px; padding: 1.1rem 1.2rem; }
|
||||
.wg-card h3 { margin: 0 0 .35rem; font-size: 1.02rem; }
|
||||
.wg-card p { margin: 0; opacity: .74; font-size: .9rem; line-height: 1.5; }
|
||||
.wg-cta { margin-bottom: 3rem; }
|
||||
|
||||
/* Logo swap via CSS.
|
||||
Cloudflare cached the stock /assets/img/logo.svg for 6h and no available API
|
||||
token can purge. Pointing at a NEW filename sidesteps the stale object
|
||||
without touching Gitea's own templates — the supported customisation surface,
|
||||
per D-2 (membrane, not merge). */
|
||||
img[src$="/assets/img/logo.svg"] {
|
||||
content: url("/assets/img/wg-mark.svg");
|
||||
}
|
||||
9
deploy/branding/public/assets/img/logo.svg
Normal file
9
deploy/branding/public/assets/img/logo.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<defs><linearGradient id="w" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#38bdf8"/><stop offset="100%" stop-color="#0ea5e9"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="16" cy="16" r="15" fill="#0b1220"/>
|
||||
<path d="M5 11h13a3.2 3.2 0 1 0-3.1-4" fill="none" stroke="url(#w)" stroke-width="2.4" stroke-linecap="round"/>
|
||||
<path d="M5 16h17a3.6 3.6 0 1 1-3.5 4.5" fill="none" stroke="url(#w)" stroke-width="2.4" stroke-linecap="round"/>
|
||||
<path d="M5 21h9a2.8 2.8 0 1 1-2.7 3.5" fill="none" stroke="url(#w)" stroke-width="2.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 660 B |
9
deploy/branding/public/assets/img/wg-mark.svg
Normal file
9
deploy/branding/public/assets/img/wg-mark.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<defs><linearGradient id="w" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#38bdf8"/><stop offset="100%" stop-color="#0ea5e9"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="16" cy="16" r="15" fill="#0b1220"/>
|
||||
<path d="M5 11h13a3.2 3.2 0 1 0-3.1-4" fill="none" stroke="url(#w)" stroke-width="2.4" stroke-linecap="round"/>
|
||||
<path d="M5 16h17a3.6 3.6 0 1 1-3.5 4.5" fill="none" stroke="url(#w)" stroke-width="2.4" stroke-linecap="round"/>
|
||||
<path d="M5 21h9a2.8 2.8 0 1 1-2.7 3.5" fill="none" stroke="url(#w)" stroke-width="2.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 660 B |
5
deploy/branding/templates/custom/header.tmpl
Normal file
5
deploy/branding/templates/custom/header.tmpl
Normal file
@@ -0,0 +1,5 @@
|
||||
{{/* Windy Git brand layer.
|
||||
The filename carries a version: Cloudflare caches /assets/* for 6h with no
|
||||
purge token available to this stack, so editing a fixed filename leaves the
|
||||
old bytes live for hours. Bump the suffix on every brand change. */}}
|
||||
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/theme-windy.v2.css">
|
||||
27
deploy/branding/templates/home.tmpl
Normal file
27
deploy/branding/templates/home.tmpl
Normal file
@@ -0,0 +1,27 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{ctx.Locale.Tr "home"}}" class="page-content home">
|
||||
<div class="wg-hero">
|
||||
<img src="{{AssetUrlPrefix}}/img/logo.svg" alt="Windy Git" width="72" height="72">
|
||||
<h1>Windy Git</h1>
|
||||
<p class="wg-sub">Your work, every version — and agents as citizens.</p>
|
||||
<div class="wg-cta">
|
||||
{{if not .IsSigned}}
|
||||
<a class="ui primary button" href="{{AppSubUrl}}/user/login">Sign in with Windy</a>
|
||||
{{else}}
|
||||
<a class="ui primary button" href="{{AppSubUrl}}/{{.SignedUser.Name}}?tab=repositories">Your repositories</a>
|
||||
{{end}}
|
||||
<a class="ui button" href="{{AppSubUrl}}/explore/repos">Explore</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wg-grid">
|
||||
<div class="wg-card"><h3>Code and models, one host</h3>
|
||||
<p>Git and LFS over Windy Cloud storage. Source, weights and adapters live side by side.</p></div>
|
||||
<div class="wg-card"><h3>Agents are first-class</h3>
|
||||
<p>An agent signs in with its own Eternitas passport — not a human's borrowed token — and its work is attributable to it.</p></div>
|
||||
<div class="wg-card"><h3>Your account, no second password</h3>
|
||||
<p>Sign in with the Windy account you already have. Nothing new to remember.</p></div>
|
||||
<div class="wg-card"><h3>Kept, and kept elsewhere</h3>
|
||||
<p>Every repository is bundled nightly to off-site storage, and the restore is rehearsed rather than assumed.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
Reference in New Issue
Block a user