content split + copy audit + css polish: content (news/pages/uploads) moves to the ctao/content repo (build overlays it — two-repo build.sh, link-content.sh for local dev); portal copy trimmed to source-documents-only (dashboard empty states, minimal privacy/disclaimer, no mock badges); css: inverted selection on navy bands, featured-card text-track floor, 44px toc target, mobile search dates on own line, footer link row gap; DESIGN.md rewritten as a short handoff
This commit is contained in:
+15
-8
@@ -4,15 +4,20 @@ Everything runs rootless as user `strapi`. Footprint on the machine:
|
||||
ONE directory (`~/ctao-portal-demo/`) + 4 unit files in `~/.config/`.
|
||||
Uninstall restores the machine exactly (see bottom). No secrets in any file.
|
||||
|
||||
Two Gitea repos by design — `ctao/portal` (code, developed by the team) and
|
||||
`ctao/content` (Markdown + uploads, committed by the CMS). The build overlays
|
||||
content onto code; a push to EITHER republishes the site.
|
||||
|
||||
```
|
||||
~/ctao-portal-demo/
|
||||
├── gitea-data/ # Gitea state (repos, SQLite, accounts) ← the ONLY thing worth backing up
|
||||
├── gitea-config/ # Gitea app.ini (generated from env)
|
||||
├── repo/ # clone of the portal repo (created by first build)
|
||||
├── releases/ # <sha>/ dirs + `current` symlink (what nginx serves)
|
||||
├── state/ # last-built SHA
|
||||
├── repo/ # clone of the code repo (created by first build)
|
||||
├── content/ # clone of the content repo (created by first build)
|
||||
├── releases/ # <code>-<content>/ dirs + `current` symlink (what nginx serves)
|
||||
├── state/ # last-built release id
|
||||
├── npm-cache/ # npm cache for the build container (created by build.sh)
|
||||
├── bin/build.sh # copied from deploy/ (source of truth stays in the repo)
|
||||
├── bin/build.sh # copied from deploy/ (source of truth stays in the code repo)
|
||||
└── config/nginx.conf
|
||||
```
|
||||
|
||||
@@ -47,10 +52,11 @@ missing). Everything else runs inside containers.
|
||||
6. **[W]** Create the Gitea admin — run interactively in a terminal so the
|
||||
password never lands in a file or shell history:
|
||||
`podman exec -it ctao-demo-gitea gitea admin user create --admin --username <you> --email <you@…> --random-password`
|
||||
7. **[W]** In the Gitea UI: create org `ctao`, repo `portal` (public read).
|
||||
Then, from your workstation, push the portal repo over the vhost with a
|
||||
repo-scoped token:
|
||||
7. **[W]** In the Gitea UI: create org `ctao` with repos `portal` and
|
||||
`content` (both public read). Then, from your workstation, push both over
|
||||
the vhost with a repo-scoped token:
|
||||
`git push https://<user>:<token>@astro-git.isl-dev.grid.cyfronet.pl/ctao/portal.git main`
|
||||
`git push https://<user>:<token>@astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git main`
|
||||
8. **[W]** `systemctl --user enable --now ctao-portal-build.timer` — first run
|
||||
clones + `npm ci` + builds (minutes); later runs are seconds. Wait until
|
||||
`journalctl --user -u ctao-portal-build -n 5` shows `published <sha>`
|
||||
@@ -88,7 +94,8 @@ loginctl disable-linger $USER # only if nothing else of yours should survive l
|
||||
|
||||
## Notes
|
||||
|
||||
- **Only site content auto-deploys.** Changes to `deploy/*` need a manual
|
||||
- **Only pushes to the machine's Gitea auto-deploy** (code and content repos
|
||||
alike). Changes to `deploy/*` need a manual
|
||||
re-copy: `build.sh` → `bin/`, `nginx.conf` → `config/` +
|
||||
`systemctl --user restart ctao-demo-web`, unit files →
|
||||
`~/.config/…` + `systemctl --user daemon-reload` (+ restart). This is
|
||||
|
||||
+60
-32
@@ -1,15 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# CTAO portal demo — poll the content repo, rebuild, publish atomically.
|
||||
# Triggered every 10 s by ctao-portal-build.timer; systemd oneshot semantics
|
||||
# guarantee runs never overlap. The 99.9% case is one local curl and exit 0.
|
||||
# No secrets anywhere: the portal repo is public-read on the local Gitea.
|
||||
# CTAO portal — poll the code AND content repos, rebuild on any change,
|
||||
# publish atomically. Triggered every 10 s by ctao-portal-build.timer; systemd
|
||||
# oneshot semantics guarantee runs never overlap. The 99.9% case is two local
|
||||
# curls and exit 0. No secrets anywhere: both repos are public-read on the
|
||||
# local Gitea.
|
||||
#
|
||||
# Two repositories by design: code (templates/CSS, developed by the team) and
|
||||
# content (Markdown + uploads, committed by the CMS). The build overlays
|
||||
# content onto code, so an editor publishing an article and a developer
|
||||
# shipping CSS never mix histories — either change republishes the site.
|
||||
set -euo pipefail
|
||||
|
||||
# Defaults match the machine; every var is env-overridable so the whole
|
||||
# pipeline can be tested locally against a sandbox dir + local Gitea.
|
||||
BASE="${BASE:-$HOME/ctao-portal-demo}"
|
||||
GITEA_URL="${GITEA_URL:-http://localhost:3000}" # published by ctao-demo-gitea
|
||||
REPO="${REPO:-ctao/portal}" # owner/repo in Gitea
|
||||
CODE_REPO="${CODE_REPO:-ctao/portal}" # owner/repo in Gitea
|
||||
CONTENT_REPO="${CONTENT_REPO:-ctao/content}"
|
||||
BRANCH="${BRANCH:-main}"
|
||||
BUILD_IMAGE="${BUILD_IMAGE:-localhost/ctao-portal-build:1}"
|
||||
# The build joins the Gitea container's network namespace: localhost inside
|
||||
@@ -17,9 +24,10 @@ BUILD_IMAGE="${BUILD_IMAGE:-localhost/ctao-portal-build:1}"
|
||||
# (A netavark bridge would be equivalent, but rootless bridges need the
|
||||
# ip_tables kernel module, absent on the machine — pasta needs nothing.)
|
||||
BUILD_NETNS="${BUILD_NETNS:-container:ctao-demo-gitea}"
|
||||
REPO_INTERNAL="${REPO_INTERNAL:-http://localhost:3000/$REPO.git}"
|
||||
CODE_URL="${CODE_URL:-http://localhost:3000/$CODE_REPO.git}"
|
||||
CONTENT_URL="${CONTENT_URL:-http://localhost:3000/$CONTENT_REPO.git}"
|
||||
KEEP="${KEEP:-3}" # released builds to retain
|
||||
mkdir -p "$BASE/repo" "$BASE/releases" "$BASE/state" "$BASE/npm-cache"
|
||||
mkdir -p "$BASE/repo" "$BASE/content" "$BASE/releases" "$BASE/state" "$BASE/npm-cache"
|
||||
|
||||
# Host prerequisites (everything else runs inside containers). Fail loud —
|
||||
# a missing tool is permanent, unlike a Gitea hiccup below.
|
||||
@@ -27,43 +35,63 @@ for tool in curl jq podman; do
|
||||
command -v "$tool" >/dev/null || { echo "missing host tool: $tool"; exit 1; }
|
||||
done
|
||||
|
||||
# --- 1. Cheap poll: branch head via the local Gitea API (host curl + jq) ---
|
||||
sha=$(curl -fsS --max-time 5 "$GITEA_URL/api/v1/repos/$REPO/branches/$BRANCH" \
|
||||
| jq -r '.commit.id' || true)
|
||||
# --- 1. Cheap poll: both branch heads via the local Gitea API ---
|
||||
head_of() {
|
||||
curl -fsS --max-time 5 "$GITEA_URL/api/v1/repos/$1/branches/$BRANCH" \
|
||||
| jq -r '.commit.id' || true
|
||||
}
|
||||
code_sha=$(head_of "$CODE_REPO")
|
||||
content_sha=$(head_of "$CONTENT_REPO")
|
||||
# Gitea down/unreachable is a transient, not a unit failure — exit 0 quietly
|
||||
# instead of painting the journal red every 10 s.
|
||||
if [[ ! "$sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "poll failed (gitea unreachable?) — skipping this tick"
|
||||
exit 0
|
||||
fi
|
||||
# Skip only if this sha is both recorded AND still present in releases/
|
||||
for sha in "$code_sha" "$content_sha"; do
|
||||
if [[ ! "$sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "poll failed (gitea unreachable?) — skipping this tick"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
release="${code_sha:0:12}-${content_sha:0:12}" # code+content pin the release
|
||||
# Skip only if this pair is both recorded AND still present in releases/
|
||||
# (a deleted release dir must trigger a rebuild, not an eternal skip).
|
||||
[[ "$sha" == "$(cat "$BASE/state/last-built" 2>/dev/null)" \
|
||||
&& -d "$BASE/releases/$sha" ]] && exit 0
|
||||
[[ "$release" == "$(cat "$BASE/state/last-built" 2>/dev/null)" \
|
||||
&& -d "$BASE/releases/$release" ]] && exit 0
|
||||
|
||||
echo "building $sha"
|
||||
echo "building code=$code_sha content=$content_sha"
|
||||
t0=$(date +%s)
|
||||
|
||||
# --- 2. Build in the ephemeral container (git + pinned node live there).
|
||||
# SECURITY: the container runs npm lifecycle scripts from the repo, so it is
|
||||
# confined to Gitea's netns — it reaches Gitea on localhost:3000 and the
|
||||
# internet (for `npm ci` when the lockfile changed), but NOT the host's
|
||||
# loopback services (code-server). Never use --network=host here.
|
||||
# loopback services. Never use --network=host here.
|
||||
# node_modules and .deps-hash are untracked, so they survive checkouts.
|
||||
podman run --rm --network="$BUILD_NETNS" --memory=1g \
|
||||
-e ASTRO_TELEMETRY_DISABLED=1 \
|
||||
-e SHA="$sha" -e REPO_URL="$REPO_INTERNAL" -e BRANCH="$BRANCH" \
|
||||
-e CODE_SHA="$code_sha" -e CODE_URL="$CODE_URL" \
|
||||
-e CONTENT_SHA="$content_sha" -e CONTENT_URL="$CONTENT_URL" \
|
||||
-e RELEASE="$release" -e BRANCH="$BRANCH" \
|
||||
-v "$BASE/repo:/work/repo:z" \
|
||||
-v "$BASE/content:/work/content:z" \
|
||||
-v "$BASE/releases:/work/releases:z" \
|
||||
-v "$BASE/npm-cache:/root/.npm:z" \
|
||||
-w /work "$BUILD_IMAGE" sh -ec '
|
||||
git config --global safe.directory /work/repo
|
||||
[ -d repo/.git ] || git clone --branch "$BRANCH" "$REPO_URL" repo
|
||||
git -C repo remote set-url origin "$REPO_URL" # self-heal if the URL changes
|
||||
git -C repo fetch --quiet origin "$BRANCH"
|
||||
# --force: the working copy is disposable; a stray tracked-file edit must
|
||||
# not wedge every future build.
|
||||
git -C repo checkout --quiet --force "$SHA"
|
||||
git config --global safe.directory "/work/repo"
|
||||
git config --global --add safe.directory "/work/content"
|
||||
sync_clone() { # $1 dir $2 url $3 sha
|
||||
[ -d "$1/.git" ] || git clone --branch "$BRANCH" "$2" "$1"
|
||||
git -C "$1" remote set-url origin "$2" # self-heal if the URL changes
|
||||
git -C "$1" fetch --quiet origin "$BRANCH"
|
||||
# --force: the working copy is disposable; a stray tracked-file edit
|
||||
# must not wedge every future build.
|
||||
git -C "$1" checkout --quiet --force "$3"
|
||||
}
|
||||
sync_clone repo "$CODE_URL" "$CODE_SHA"
|
||||
sync_clone content "$CONTENT_URL" "$CONTENT_SHA"
|
||||
# Overlay content onto code (these paths are gitignored in the code repo)
|
||||
rm -rf repo/src/content/news repo/src/content/pages repo/public/uploads
|
||||
cp -a content/news repo/src/content/news
|
||||
cp -a content/pages repo/src/content/pages
|
||||
cp -a content/uploads repo/public/uploads
|
||||
cd repo
|
||||
lock=$(sha256sum package-lock.json | cut -d" " -f1)
|
||||
if [ ! -d node_modules ] || [ "$lock" != "$(cat .deps-hash 2>/dev/null)" ]; then
|
||||
@@ -76,17 +104,17 @@ podman run --rm --network="$BUILD_NETNS" --memory=1g \
|
||||
echo "$lock" > .deps-hash
|
||||
fi
|
||||
npm run build
|
||||
rm -rf "../releases/$SHA"
|
||||
cp -a dist "../releases/$SHA"
|
||||
rm -rf "../releases/$RELEASE"
|
||||
cp -a dist "../releases/$RELEASE"
|
||||
'
|
||||
|
||||
# --- 3. Atomic publish: symlink flip via rename(2) — no half-published moment.
|
||||
# mv -T is GNU (the target host is Rocky); when testing on macOS put a
|
||||
# coreutils `mv` (gmv) first in PATH.
|
||||
rm -f "$BASE/releases/".current.* # stale temps from a crash mid-flip
|
||||
ln -s "$sha" "$BASE/releases/.current.$$"
|
||||
ln -s "$release" "$BASE/releases/.current.$$"
|
||||
mv -Tf "$BASE/releases/.current.$$" "$BASE/releases/current"
|
||||
echo "$sha" > "$BASE/state/last-built"
|
||||
echo "$release" > "$BASE/state/last-built"
|
||||
|
||||
# --- 4. Prune old releases. `current`'s target is excluded explicitly —
|
||||
# mtime ordering makes it newest today, but nothing should depend on that.
|
||||
@@ -98,4 +126,4 @@ ls -1t | grep -vx current | grep -vx -- "$cur" | tail -n +"$((KEEP + 1))" | whil
|
||||
rm -rf -- "$old"
|
||||
done || true
|
||||
|
||||
echo "published $sha in $(( $(date +%s) - t0 ))s"
|
||||
echo "published $release in $(( $(date +%s) - t0 ))s"
|
||||
|
||||
Reference in New Issue
Block a user