Add the cnxvinyl.lol test site with a Gitea deploy workflow
Deploy test site / deploy (push) Canceled after 0s
Deploy test site / deploy (push) Canceled after 0s
site/ builds the internal test site: public/ served as is (landing page, robots.txt, static sites) plus demo pages copied out of docs/ (the plant operation page at /demos/plant-operation/). build.sh fails on dead internal links and stamps version.txt. .gitea/workflows/deploy-site.yml rsyncs site/dist to the buildfor.life web server with the blog's two-directory rename swap, checks version.txt on the server, purges Cloudflare when a token is set, and checks the public URL. Secret names match the blog's workflow. site/Caddyfile is the virtual host to append on s1 (noindex everywhere, Cloudflare-only like the blog apex, www redirect). site/README.md has the one-time setup: DNS, Caddy, web root, secrets, runner.
This commit is contained in:
1 parent
0e033784a2
commit
560d073019
10 files changed
+396
-2
No files matched your search
@@ -0,0 +1,40 @@
|
||||
# cnxvinyl.lol: internal test site on the buildfor.life web server (s1).
|
||||
# Static web root /var/www/cnxvinyl.lol, filled by
|
||||
# .gitea/workflows/deploy-site.yml in the cnx_vinyl repo.
|
||||
#
|
||||
# Append these blocks to /etc/caddy/Caddyfile on s1. They reuse the
|
||||
# cloudflare_only and security_headers snippets defined there for
|
||||
# buildfor.life (buildfor_life_web: infra/caddy/Caddyfile.example); if
|
||||
# they are missing, validate fails with "File to import not found:
|
||||
# cloudflare_only" and they have to be copied in from the example first.
|
||||
# caddy validate --config /etc/caddy/Caddyfile
|
||||
# systemctl reload caddy
|
||||
#
|
||||
# Cloudflare: both names proxied (orange cloud), SSL/TLS mode "Full (strict)".
|
||||
# "Flexible" sends plain HTTP to Caddy, which redirects to HTTPS: a loop.
|
||||
# Same rule as the buildfor.life apex: if a record is ever switched to DNS
|
||||
# only, remove "import cloudflare_only" first or that name goes dark.
|
||||
|
||||
cnxvinyl.lol {
|
||||
import cloudflare_only
|
||||
import security_headers
|
||||
header X-Frame-Options "SAMEORIGIN"
|
||||
# Internal testing: keep every page and file out of search engines.
|
||||
header X-Robots-Tag "noindex, nofollow"
|
||||
|
||||
# Optional login for the whole site. Make the hash on s1 with
|
||||
# caddy hash-password
|
||||
# then uncomment and fill in:
|
||||
# basic_auth {
|
||||
# <user> <hash>
|
||||
# }
|
||||
|
||||
root * /var/www/cnxvinyl.lol
|
||||
encode zstd gzip
|
||||
file_server
|
||||
}
|
||||
|
||||
www.cnxvinyl.lol {
|
||||
import cloudflare_only
|
||||
redir https://cnxvinyl.lol{uri} permanent
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
# Test site: cnxvinyl.lol
|
||||
|
||||
`https://cnxvinyl.lol/` is the internal test site for this repository: demo
|
||||
pages from `docs/` (the [plant operation page](../docs/plant-operation.html)
|
||||
first) and any static site we want to show by link. It runs on the same web
|
||||
server as buildfor.life (Debian VPS `s1`, Caddy behind Cloudflare) and is
|
||||
not a plant service; plant systems follow the
|
||||
[digital factory plan](../docs/digital-factory.md). Every page and file is
|
||||
sent with `noindex` and `robots.txt` disallows everything; nothing is behind
|
||||
a login unless `basic_auth` in the Caddyfile is switched on.
|
||||
|
||||
## How it is built
|
||||
|
||||
- `public/` is copied verbatim to the web root: the landing page
|
||||
(`index.html`), `robots.txt`, and any static site as its own directory
|
||||
(`public/<name>/index.html` is served at `/<name>/`).
|
||||
- `build.sh` assembles `site/dist`: `public/` plus every entry in its
|
||||
`DEMOS` list, which copies a page out of `docs/` (for example
|
||||
`docs/plant-operation.html` to `/demos/plant-operation/`), so the docs stay
|
||||
the one source. It writes `version.txt` (the commit) and fails on any
|
||||
internal link that does not resolve.
|
||||
- To add a demo: one line in `DEMOS`, one entry on the landing page. To add
|
||||
a static site: drop its built output in `public/<name>/` and link it.
|
||||
- Local preview: `bash site/build.sh`, then open `site/dist/index.html`.
|
||||
|
||||
## How it is deployed
|
||||
|
||||
`.gitea/workflows/deploy-site.yml` runs on every push to `master` that
|
||||
touches `site/`, an HTML page under `docs/`, or the workflow itself, and on
|
||||
demand (Actions tab, "Run workflow"). It builds, rsyncs `site/dist` into
|
||||
the idle one of `/var/www/cnxvinyl.lol` and `/var/www/cnxvinyl.lol-prev`,
|
||||
swaps them by rename (the blog's pattern), checks `version.txt` on the
|
||||
server, purges the Cloudflare cache when the token is set, and finally
|
||||
checks the public URL (warning only).
|
||||
|
||||
The deploy owns the whole web root: anything put into
|
||||
`/var/www/cnxvinyl.lol` by hand disappears on the next deploy. Static sites
|
||||
go into `public/` instead.
|
||||
|
||||
## One-time setup
|
||||
|
||||
1. DNS in Cloudflare, both proxied (orange cloud):
|
||||
`cnxvinyl.lol CNAME s1.buildfor.life` and `www CNAME cnxvinyl.lol`
|
||||
(Cloudflare flattens the apex CNAME, so the site follows s1 if its
|
||||
address changes). SSL/TLS mode **Full (strict)**: "Flexible" makes a
|
||||
redirect loop with Caddy.
|
||||
2. Caddy on s1: append [`Caddyfile`](Caddyfile) to `/etc/caddy/Caddyfile`
|
||||
(the comments in it list the checks), `caddy validate`, then
|
||||
`systemctl reload caddy`. Caddy gets the certificate the same way it
|
||||
does for the buildfor.life apex.
|
||||
3. Web root: the deploy user must be able to create and rename
|
||||
directories in `/var/www`, as it already does for the blog. If not:
|
||||
`sudo install -d -o <deploy user> /var/www/cnxvinyl.lol /var/www/cnxvinyl.lol-prev`
|
||||
and make `/var/www` writable for that user (the swap renames entries
|
||||
in it).
|
||||
4. Secrets in Gitea (repo: Settings > Actions > Secrets, or user level so
|
||||
every repo of the account gets them): `VPS_HOST`, `VPS_USER`,
|
||||
`VPS_SSH_KEY`, optionally `VPS_PORT` (default 22), `VPS_KNOWN_HOSTS`
|
||||
(output of `ssh-keyscan -p <port> s1.buildfor.life`, pins the host key),
|
||||
`CLOUDFLARE_ZONE_ID` and `CLOUDFLARE_API_TOKEN` (Zone > Cache Purge on
|
||||
cnxvinyl.lol). The names match the blog's workflow, so the blog's
|
||||
deploy key and values work unchanged: s1 only checks the key against
|
||||
the deploy user's `authorized_keys`, whichever repository sends it.
|
||||
Gitea secrets cannot be read back, so they are entered again here from
|
||||
the private key file; repo secrets set on `B4L/buildfor_life_web` do not
|
||||
reach this repository, B4L organisation secrets only reach it if it moves
|
||||
into B4L. If the private key file is gone, make a new key instead:
|
||||
`ssh-keygen -t ed25519 -N "" -C cnx_vinyl-deploy -f cnx_vinyl-deploy`,
|
||||
public half into the deploy user's `~/.ssh/authorized_keys` on s1,
|
||||
private half into `VPS_SSH_KEY`, then delete the local copy.
|
||||
5. Runner: the job asks for `ubuntu-latest`, the label the blog's runner
|
||||
serves. This repository is under a user account, not the B4L
|
||||
organisation, so a runner registered only for B4L does not pick it up;
|
||||
if the run stays "Waiting", register the runner for this repository or
|
||||
instance-wide, or move the repository into B4L.
|
||||
6. Run the workflow once from the Actions tab and open
|
||||
`https://cnxvinyl.lol/`.
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Assemble the cnxvinyl.lol web root in site/dist.
|
||||
#
|
||||
# site/public/ copied verbatim: landing page, robots.txt, and any static
|
||||
# site dropped in as its own directory (site/public/<name>/)
|
||||
# DEMOS below pages copied out of docs/, so docs/ stays their one source
|
||||
#
|
||||
# Run locally with `bash site/build.sh`, then open site/dist/index.html.
|
||||
# CI (.gitea/workflows/deploy-site.yml) runs the same script and deploys
|
||||
# site/dist as it is.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
out=site/dist
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
cp -R site/public/. "$out/"
|
||||
|
||||
# "<source in the repo> <path under the web root>"
|
||||
DEMOS=(
|
||||
"docs/plant-operation.html demos/plant-operation/index.html"
|
||||
)
|
||||
for entry in "${DEMOS[@]}"; do
|
||||
read -r src dst <<<"$entry"
|
||||
mkdir -p "$out/$(dirname "$dst")"
|
||||
cp "$src" "$out/$dst"
|
||||
done
|
||||
|
||||
# The deploy job reads this back from the server to confirm what is live.
|
||||
git rev-parse HEAD >"$out/version.txt" 2>/dev/null || echo unknown >"$out/version.txt"
|
||||
|
||||
# Fail on a dead internal link: every href/src in every page that points
|
||||
# inside the site must resolve to a file in dist.
|
||||
dead=0
|
||||
while IFS= read -r page; do
|
||||
dir=$(dirname "$page")
|
||||
while IFS= read -r ref; do
|
||||
# shellcheck disable=SC2016 # literal ${ and {{ mark JS/template strings
|
||||
case "$ref" in
|
||||
'' | '#'* | http:* | https:* | mailto:* | tel:* | data:* | javascript:* | *'${'* | *'{{'*) continue ;;
|
||||
esac
|
||||
ref=${ref%%#*}
|
||||
ref=${ref%%\?*}
|
||||
case "$ref" in
|
||||
/*) target="$out$ref" ;;
|
||||
*) target="$dir/$ref" ;;
|
||||
esac
|
||||
[ -d "$target" ] && target="${target%/}/index.html"
|
||||
if [ ! -f "$target" ]; then
|
||||
echo "dead link in ${page#"$out"/}: $ref" >&2
|
||||
dead=1
|
||||
fi
|
||||
done < <(grep -o -E '(href|src)="[^"]*"' "$page" | sed -E 's/^(href|src)="//; s/"$//')
|
||||
done < <(find "$out" -name '*.html')
|
||||
[ "$dead" -eq 0 ] || exit 1
|
||||
|
||||
echo "built $out: $(find "$out" -type f | wc -l | tr -d ' ') files"
|
||||
@@ -0,0 +1,68 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>CNX Vinyl · internal test site</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0a0f1a; --panel: #0f1626; --stroke: #2a3a56;
|
||||
--fg: #d6deeb; --mute: #8a97b0; --accent: #3ddc84;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; background: var(--bg); color: var(--fg);
|
||||
font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
main { max-width: 760px; margin: 0 auto; padding: 48px 20px; }
|
||||
h1 { margin: 0; font-size: 28px; letter-spacing: 0.02em; }
|
||||
.tag {
|
||||
display: inline-block; margin-top: 6px; padding: 2px 8px;
|
||||
border: 1px solid var(--stroke); border-radius: 4px;
|
||||
color: var(--mute); font-size: 13px;
|
||||
}
|
||||
p.lead { color: var(--mute); margin: 18px 0 32px; }
|
||||
h2 {
|
||||
font-size: 13px; text-transform: uppercase; letter-spacing: 0.08em;
|
||||
color: var(--mute); margin: 32px 0 12px;
|
||||
}
|
||||
ul { list-style: none; margin: 0; padding: 0; }
|
||||
li a {
|
||||
display: block; padding: 14px 16px; margin-bottom: 10px;
|
||||
background: var(--panel); border: 1px solid var(--stroke); border-radius: 6px;
|
||||
color: var(--fg); text-decoration: none;
|
||||
}
|
||||
li a:hover, li a:focus-visible { border-color: var(--accent); }
|
||||
li strong { display: block; color: var(--accent); font-weight: 600; }
|
||||
li span { color: var(--mute); font-size: 14px; }
|
||||
footer { margin-top: 40px; color: var(--mute); font-size: 13px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>CNX Vinyl</h1>
|
||||
<span class="tag">internal test site</span>
|
||||
<p class="lead">
|
||||
Demos and static pages from the CNX Vinyl planning repository, a vinyl
|
||||
pressing plant planned near Chiang Mai. Shared by link only and not
|
||||
indexed. Figures in the demos are planning estimates.
|
||||
</p>
|
||||
|
||||
<h2>Demos</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="demos/plant-operation/">
|
||||
<strong>Plant overview (simulated SCADA)</strong>
|
||||
<span>The whole plant animated, from PVC intake and the material lab
|
||||
through extruder, press, trim, optical inspection, finishing,
|
||||
packaging and fulfillment, with the metal path, utilities and energy
|
||||
alongside. Every station is clickable.</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<footer>Built from <code>site/</code> in the cnx_vinyl repository.</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
Reference in new issue
Block a user