feat: prerender every route + Gitea Actions deploy
Make the site fully prerendered so a plain curl returns complete content
for every route (crawlers / AI screening tools see real text, not an empty
#root), while humans keep full client interactivity.
Prerender:
- Build-time per-route render: prefetch data, renderToString, inline the
dehydrated react-query cache as window.__RQ_STATE__; client hydrateRoots
and refetches live (activity stays fresh; crawlers get the baked snapshot).
- New entry-server.tsx + prerender/{prefetch,routes,meta}.ts + run-prerender.mjs;
shared lib/ranges.ts keeps SSR and client query keys identical.
- pnpm build now: tsc -b -> vite client build -> ssr build -> prerender.
- API base absolute at build (VITE_API_BASE), relative /api/v1 in the browser.
- CSS imports moved to the client entry so the tree imports under Node.
- schema.org Person + Occupation JSON-LD and per-route title/description/og.
- UTC + explicit field widths on shared date formatting so SSR and client
hydration match byte-for-byte (fixes hydration mismatch on /activity).
- Strip non-text gist content from the CV fetch (1MB -> 25KB gzipped page).
Deploy (Gitea Actions, replaces script/deploy.sh):
- deploy.yml: on push to main, lint/test gate, build api+worker as static
musl binaries (pure-rustls, no glibc skew) + prerendered web, deploy each
over SSH as gitea_ci with scoped sudo.
- refresh.yml: daily cron re-bakes only the web snapshot so gist/activity
edits propagate without a push or bouncing the api/worker.
- script/infra-setup.sh + asset/sudoers.d/{api,worker,web}-host.conf for
one-time per-host provisioning. Secrets: RSYNC_SSH_KEY, QUERY_GITHUB_TOKEN,
QUERY_GITEA_TOKEN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7zF7Kf4JqDwa6M8Qgge9M
This commit is contained in:
354
.gitea/workflows/deploy.yml
Normal file
354
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,354 @@
|
||||
name: deploy
|
||||
|
||||
# Build and roll out all three moments components on a push to main:
|
||||
# - moments-api (static musl binary, read-only API) -> nikola
|
||||
# - moments-worker (static musl binary, ingestion daemon) -> frootmig
|
||||
# - web (prerendered static SPA + nginx vhost) -> oolon
|
||||
#
|
||||
# This workflow is the source of infra truth (hosts, ports, paths live in `env`
|
||||
# below — there is no manifest.yml in this model). It replaces script/deploy.sh:
|
||||
# instead of an operator running it from a workstation with `pass`, a Gitea
|
||||
# Actions runner deploys as the dedicated `gitea_ci` user over SSH, with secrets
|
||||
# from the repo settings and scoped sudo (see asset/sudoers.d/ + script/infra-setup.sh).
|
||||
#
|
||||
# The api/worker binaries are pure-rustls (no openssl), so they build as fully
|
||||
# static musl — a runner newer than the target host can't produce an unloadable
|
||||
# glibc binary (architecture/deployment-gitea-actions.md §6).
|
||||
#
|
||||
# Secrets (Gitea -> repo -> Settings -> Actions -> Secrets):
|
||||
# RSYNC_SSH_KEY private SSH key whose pubkey script/infra-setup.sh installed
|
||||
# QUERY_GITHUB_TOKEN github api token for the worker's poller
|
||||
# QUERY_GITEA_TOKEN git.lair.cafe api token for the worker's poller
|
||||
# (GITHUB_TOKEN / GITEA_TOKEN are reserved Actions names, hence the QUERY_ prefix.)
|
||||
#
|
||||
# One-time per-host provisioning (gitea_ci user, authorized_keys, scoped
|
||||
# sudoers, and the postgres mTLS host cert the api/worker need) is done by
|
||||
# script/infra-setup.sh — run it once per host before this workflow can succeed.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
# Serialize deploys; never cancel an in-flight one (a half-applied rollout is
|
||||
# worse than a slightly stale one).
|
||||
concurrency:
|
||||
group: deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
# --- infra truth (mirrors the former asset/manifest.yml) ---
|
||||
API_HOST: nikola.kosherinata.internal
|
||||
WORKER_HOST: frootmig.kosherinata.internal
|
||||
WEB_HOST: oolon.kosherinata.internal
|
||||
API_BIND: 0.0.0.0:42424
|
||||
API_PORT: "42424"
|
||||
SERVER_NAME: rob.tn
|
||||
WEB_ROOT: /var/www/rob.tn
|
||||
API_UPSTREAM_SCHEME: http
|
||||
API_UPSTREAM_ADDR: nikola.kosherinata.internal:42424
|
||||
MUSL_TARGET: x86_64-unknown-linux-musl
|
||||
# The prerender fetches the live public API at build time; the client bundle
|
||||
# keeps the same-origin relative /api/v1 that nginx proxies.
|
||||
VITE_API_BASE: https://rob.tn/api/v1
|
||||
DEPLOY_KEY: |
|
||||
${{ secrets.RSYNC_SSH_KEY }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build api + worker + web
|
||||
runs-on: rust
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# The `rust` runner image provides cargo/clippy/rustfmt, musl-gcc, the
|
||||
# x86_64-unknown-linux-musl std, and node. No package installs at run time.
|
||||
- name: Lint + test (deploy gate)
|
||||
run: |
|
||||
cargo fmt --check --all
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
cargo test --workspace
|
||||
|
||||
- name: Build moments-api + moments-worker (static musl release)
|
||||
run: cargo build --release --target "${MUSL_TARGET}" -p moments-api -p moments-worker
|
||||
|
||||
- name: Build web (vite client + prerender)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare pnpm@10 --activate
|
||||
pnpm --dir ui install --frozen-lockfile
|
||||
pnpm --dir ui run build
|
||||
|
||||
- name: Stage binaries
|
||||
run: |
|
||||
mkdir --parents artifacts
|
||||
cp "target/${MUSL_TARGET}/release/moments-api" artifacts/moments-api
|
||||
cp "target/${MUSL_TARGET}/release/moments-worker" artifacts/moments-worker
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with: { name: moments-api, path: artifacts/moments-api, retention-days: 1 }
|
||||
- uses: actions/upload-artifact@v3
|
||||
with: { name: moments-worker, path: artifacts/moments-worker, retention-days: 1 }
|
||||
- uses: actions/upload-artifact@v3
|
||||
with: { name: web-dist, path: ui/dist, retention-days: 1 }
|
||||
|
||||
deploy-api:
|
||||
name: Deploy moments-api to nikola
|
||||
needs: build
|
||||
runs-on: fedora-43
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with: { name: moments-api, path: artifact }
|
||||
|
||||
- name: SSH init
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${DEPLOY_KEY}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
|
||||
gitea_ci@"${API_HOST}" 'hostname -f'
|
||||
|
||||
- name: Render config + units from infra truth
|
||||
run: |
|
||||
mkdir -p rendered
|
||||
# HOSTNAME is the host's own fqdn (used for the postgres mTLS cert path).
|
||||
python3 - "$API_HOST" "$API_BIND" "$API_PORT" <<'PY'
|
||||
import sys
|
||||
host, bind, port = sys.argv[1:4]
|
||||
def render(src, dst, subs):
|
||||
t = open(src).read()
|
||||
for k, v in subs.items():
|
||||
t = t.replace("{{%s}}" % k, v)
|
||||
open(dst, "w").write(t)
|
||||
render("asset/config/api.env.tmpl", "rendered/api.env",
|
||||
{"HOSTNAME": host, "BIND": bind})
|
||||
render("asset/systemd/moments-api-cert.path", "rendered/moments-api-cert.path",
|
||||
{"HOSTNAME": host})
|
||||
render("asset/firewalld/moments-api.xml.tmpl", "rendered/moments-api.xml",
|
||||
{"API_PORT": port})
|
||||
PY
|
||||
|
||||
- name: Provision service account + directories
|
||||
run: |
|
||||
# --mkpath: /etc/sysusers.d and /etc/firewalld/services don't exist by
|
||||
# default on Fedora (only the /usr/lib variants ship).
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' \
|
||||
asset/systemd/moments.sysusers.conf \
|
||||
gitea_ci@"${API_HOST}":/etc/sysusers.d/moments.conf
|
||||
ssh gitea_ci@"${API_HOST}" '
|
||||
set -euo pipefail
|
||||
sudo /usr/bin/systemd-sysusers
|
||||
sudo /usr/bin/install -d -o root -g moments -m 0750 /etc/moments
|
||||
sudo /usr/bin/install -d -o moments -g moments -m 0750 /var/lib/moments'
|
||||
|
||||
- name: Sync binary, env, units, firewalld service
|
||||
run: |
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0755 \
|
||||
artifact/moments-api gitea_ci@"${API_HOST}":/usr/local/bin/moments-api
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:moments --chmod=0640 \
|
||||
rendered/api.env gitea_ci@"${API_HOST}":/etc/moments/api.env
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0644 \
|
||||
asset/systemd/moments-api.service \
|
||||
gitea_ci@"${API_HOST}":/etc/systemd/system/moments-api.service
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0644 \
|
||||
rendered/moments-api-cert.path \
|
||||
gitea_ci@"${API_HOST}":/etc/systemd/system/moments-api-cert.path
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0644 \
|
||||
asset/systemd/moments-api-cert-reload.service \
|
||||
gitea_ci@"${API_HOST}":/etc/systemd/system/moments-api-cert-reload.service
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0644 \
|
||||
rendered/moments-api.xml \
|
||||
gitea_ci@"${API_HOST}":/etc/firewalld/services/moments-api.xml
|
||||
|
||||
- name: Apply SELinux + firewalld + cert ACL (idempotent)
|
||||
run: |
|
||||
ssh gitea_ci@"${API_HOST}" '
|
||||
set -euo pipefail
|
||||
fqdn="$(hostname -f)"
|
||||
# Let the moments user read the host private key for postgres mTLS.
|
||||
sudo /usr/bin/setfacl -m u:moments:r "/etc/pki/tls/private/${fqdn}.pem"
|
||||
sudo /usr/sbin/restorecon -R /usr/local/bin/moments-api /etc/moments /var/lib/moments
|
||||
if ! sudo /usr/sbin/semanage port -l | grep -E "^http_port_t" | grep -qw '"${API_PORT}"'; then
|
||||
sudo /usr/sbin/semanage port -a -t http_port_t -p tcp '"${API_PORT}"'
|
||||
fi
|
||||
# firewalld only learns a freshly-shipped custom service after reload.
|
||||
sudo /usr/bin/firewall-cmd --reload
|
||||
if ! sudo /usr/bin/firewall-cmd --query-service=moments-api; then
|
||||
sudo /usr/bin/firewall-cmd --add-service=moments-api --permanent
|
||||
sudo /usr/bin/firewall-cmd --reload
|
||||
fi'
|
||||
|
||||
- name: Restart moments-api
|
||||
run: |
|
||||
ssh gitea_ci@"${API_HOST}" '
|
||||
set -euo pipefail
|
||||
sudo /usr/bin/systemctl daemon-reload
|
||||
sudo /usr/bin/systemctl enable --now moments-api-cert.path
|
||||
sudo /usr/bin/systemctl enable moments-api.service
|
||||
sudo /usr/bin/systemctl restart moments-api.service'
|
||||
|
||||
- name: Health probe
|
||||
run: |
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
if ssh gitea_ci@"${API_HOST}" "curl -fsS http://127.0.0.1:${API_PORT}/v1/healthz"; then
|
||||
echo; echo "moments-api healthy"; exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "moments-api did not become healthy" >&2
|
||||
exit 1
|
||||
|
||||
- name: Capture startup journal
|
||||
if: always()
|
||||
run: |
|
||||
sleep 3
|
||||
ssh gitea_ci@"${API_HOST}" 'journalctl --unit moments-api.service --no-pager -n 200'
|
||||
|
||||
deploy-worker:
|
||||
name: Deploy moments-worker to frootmig
|
||||
needs: build
|
||||
runs-on: fedora-43
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with: { name: moments-worker, path: artifact }
|
||||
|
||||
- name: SSH init
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${DEPLOY_KEY}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
|
||||
gitea_ci@"${WORKER_HOST}" 'hostname -f'
|
||||
|
||||
- name: Render worker config + cert.path from secrets
|
||||
env:
|
||||
# GITHUB_TOKEN / GITEA_TOKEN are reserved Actions secret names, so the
|
||||
# repo secrets are QUERY_*; the rendered worker.env still uses the
|
||||
# GITHUB_TOKEN / GITEA_TOKEN env vars the worker poller expects.
|
||||
GITHUB_TOKEN: ${{ secrets.QUERY_GITHUB_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.QUERY_GITEA_TOKEN }}
|
||||
run: |
|
||||
mkdir -p rendered
|
||||
# Literal substitution via python so tokens with shell-special chars
|
||||
# survive; secrets come from the environment, never a command line.
|
||||
python3 - "$WORKER_HOST" <<'PY'
|
||||
import os, sys
|
||||
host = sys.argv[1]
|
||||
t = open("asset/config/worker.env.tmpl").read()
|
||||
subs = {
|
||||
"HOSTNAME": host,
|
||||
"GITHUB_TOKEN": os.environ.get("GITHUB_TOKEN", ""),
|
||||
"GITEA_TOKEN": os.environ.get("GITEA_TOKEN", ""),
|
||||
}
|
||||
for k, v in subs.items():
|
||||
t = t.replace("{{%s}}" % k, v)
|
||||
open("rendered/worker.env", "w").write(t)
|
||||
c = open("asset/systemd/moments-worker-cert.path").read().replace("{{HOSTNAME}}", host)
|
||||
open("rendered/moments-worker-cert.path", "w").write(c)
|
||||
PY
|
||||
|
||||
- name: Provision service account + directories
|
||||
run: |
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' \
|
||||
asset/systemd/moments.sysusers.conf \
|
||||
gitea_ci@"${WORKER_HOST}":/etc/sysusers.d/moments.conf
|
||||
ssh gitea_ci@"${WORKER_HOST}" '
|
||||
set -euo pipefail
|
||||
sudo /usr/bin/systemd-sysusers
|
||||
sudo /usr/bin/install -d -o root -g moments -m 0750 /etc/moments
|
||||
sudo /usr/bin/install -d -o moments -g moments -m 0750 /var/lib/moments'
|
||||
|
||||
- name: Sync binary, env, units
|
||||
run: |
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0755 \
|
||||
artifact/moments-worker gitea_ci@"${WORKER_HOST}":/usr/local/bin/moments-worker
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:moments --chmod=0640 \
|
||||
rendered/worker.env gitea_ci@"${WORKER_HOST}":/etc/moments/worker.env
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0644 \
|
||||
asset/systemd/moments-worker.service \
|
||||
gitea_ci@"${WORKER_HOST}":/etc/systemd/system/moments-worker.service
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0644 \
|
||||
rendered/moments-worker-cert.path \
|
||||
gitea_ci@"${WORKER_HOST}":/etc/systemd/system/moments-worker-cert.path
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0644 \
|
||||
asset/systemd/moments-worker-cert-reload.service \
|
||||
gitea_ci@"${WORKER_HOST}":/etc/systemd/system/moments-worker-cert-reload.service
|
||||
|
||||
- name: Apply cert ACL + SELinux, restart worker
|
||||
run: |
|
||||
ssh gitea_ci@"${WORKER_HOST}" '
|
||||
set -euo pipefail
|
||||
fqdn="$(hostname -f)"
|
||||
sudo /usr/bin/setfacl -m u:moments:r "/etc/pki/tls/private/${fqdn}.pem"
|
||||
sudo /usr/sbin/restorecon -R /usr/local/bin/moments-worker /etc/moments /var/lib/moments
|
||||
sudo /usr/bin/systemctl daemon-reload
|
||||
sudo /usr/bin/systemctl enable --now moments-worker-cert.path
|
||||
sudo /usr/bin/systemctl enable moments-worker.service
|
||||
sudo /usr/bin/systemctl restart moments-worker.service
|
||||
sudo /usr/bin/systemctl is-active --quiet moments-worker.service'
|
||||
|
||||
- name: Capture startup journal
|
||||
if: always()
|
||||
run: |
|
||||
sleep 3
|
||||
ssh gitea_ci@"${WORKER_HOST}" 'journalctl --unit moments-worker.service --no-pager -n 200'
|
||||
|
||||
deploy-web:
|
||||
name: Deploy web to oolon
|
||||
needs: build
|
||||
runs-on: fedora-43
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with: { name: web-dist, path: dist }
|
||||
|
||||
- name: SSH init
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${DEPLOY_KEY}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
|
||||
gitea_ci@"${WEB_HOST}" 'hostname -f'
|
||||
|
||||
- name: Render nginx vhost
|
||||
run: |
|
||||
mkdir -p rendered
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
t = open("asset/nginx/site.conf.tmpl").read()
|
||||
subs = {
|
||||
"SERVER_NAME": os.environ["SERVER_NAME"],
|
||||
"DOCROOT": os.environ["WEB_ROOT"],
|
||||
"API_UPSTREAM_SCHEME": os.environ["API_UPSTREAM_SCHEME"],
|
||||
"API_UPSTREAM_ADDR": os.environ["API_UPSTREAM_ADDR"],
|
||||
}
|
||||
for k, v in subs.items():
|
||||
t = t.replace("{{%s}}" % k, v)
|
||||
open("rendered/site.conf", "w").write(t)
|
||||
PY
|
||||
|
||||
- name: Sync static site (prerendered)
|
||||
run: |
|
||||
ssh gitea_ci@"${WEB_HOST}" 'sudo /usr/bin/install -d -m 0755 '"${WEB_ROOT}"
|
||||
rsync -az --delete --mkpath --rsync-path='sudo rsync' \
|
||||
--chown=root:root --chmod=D755,F644 \
|
||||
dist/ gitea_ci@"${WEB_HOST}":"${WEB_ROOT}/"
|
||||
ssh gitea_ci@"${WEB_HOST}" 'sudo /usr/sbin/restorecon -R '"${WEB_ROOT}"
|
||||
|
||||
- name: Sync nginx vhost + apply SELinux
|
||||
run: |
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0644 \
|
||||
rendered/site.conf \
|
||||
gitea_ci@"${WEB_HOST}":/etc/nginx/conf.d/"${SERVER_NAME}".conf
|
||||
ssh gitea_ci@"${WEB_HOST}" '
|
||||
set -euo pipefail
|
||||
# nginx proxies /api/ to the api host across the WG mesh.
|
||||
sudo /usr/sbin/setsebool -P httpd_can_network_connect on
|
||||
if ! sudo /usr/sbin/semanage port -l | grep -E "^http_port_t" | grep -qw '"${API_PORT}"'; then
|
||||
sudo /usr/sbin/semanage port -a -t http_port_t -p tcp '"${API_PORT}"'
|
||||
fi
|
||||
sudo /usr/sbin/restorecon -R /etc/nginx/conf.d/'"${SERVER_NAME}"'.conf
|
||||
sudo /usr/sbin/nginx -t
|
||||
sudo /usr/bin/systemctl reload nginx'
|
||||
98
.gitea/workflows/refresh.yml
Normal file
98
.gitea/workflows/refresh.yml
Normal file
@@ -0,0 +1,98 @@
|
||||
name: refresh
|
||||
|
||||
# Daily re-bake of the prerendered site. The crawler-visible HTML is a static
|
||||
# snapshot taken at build time; this job rebuilds it from the current gist (CV)
|
||||
# and activity API and redeploys *only* the web tier — so edits propagate to
|
||||
# what crawlers and AI screeners see without a code push and without bouncing
|
||||
# the api/worker. Humans already get live data via post-hydration refetch.
|
||||
#
|
||||
# Uses the same gitea_ci + scoped-sudo path as deploy.yml's deploy-web job;
|
||||
# see asset/sudoers.d/web-host.conf and script/infra-setup.sh.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 04:17 UTC daily — off-peak, arbitrary minute to avoid the top-of-hour herd.
|
||||
- cron: '17 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy # share the deploy lock so a refresh never races a push deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
WEB_HOST: oolon.kosherinata.internal
|
||||
SERVER_NAME: rob.tn
|
||||
WEB_ROOT: /var/www/rob.tn
|
||||
API_PORT: "42424"
|
||||
API_UPSTREAM_SCHEME: http
|
||||
API_UPSTREAM_ADDR: nikola.kosherinata.internal:42424
|
||||
VITE_API_BASE: https://rob.tn/api/v1
|
||||
DEPLOY_KEY: |
|
||||
${{ secrets.RSYNC_SSH_KEY }}
|
||||
|
||||
jobs:
|
||||
build-web:
|
||||
name: Rebuild prerendered web
|
||||
runs-on: rust # image has node; we only need the web toolchain here
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build web (vite client + prerender)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare pnpm@10 --activate
|
||||
pnpm --dir ui install --frozen-lockfile
|
||||
pnpm --dir ui run build
|
||||
- uses: actions/upload-artifact@v3
|
||||
with: { name: web-dist, path: ui/dist, retention-days: 1 }
|
||||
|
||||
deploy-web:
|
||||
name: Deploy refreshed web to oolon
|
||||
needs: build-web
|
||||
runs-on: fedora-43
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with: { name: web-dist, path: dist }
|
||||
|
||||
- name: SSH init
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${DEPLOY_KEY}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
|
||||
gitea_ci@"${WEB_HOST}" 'hostname -f'
|
||||
|
||||
- name: Render nginx vhost
|
||||
run: |
|
||||
mkdir -p rendered
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
t = open("asset/nginx/site.conf.tmpl").read()
|
||||
for k in ("SERVER_NAME", "API_UPSTREAM_SCHEME", "API_UPSTREAM_ADDR"):
|
||||
t = t.replace("{{%s}}" % k, os.environ[k])
|
||||
t = t.replace("{{DOCROOT}}", os.environ["WEB_ROOT"])
|
||||
open("rendered/site.conf", "w").write(t)
|
||||
PY
|
||||
|
||||
- name: Sync static site (prerendered)
|
||||
run: |
|
||||
ssh gitea_ci@"${WEB_HOST}" 'sudo /usr/bin/install -d -m 0755 '"${WEB_ROOT}"
|
||||
rsync -az --delete --mkpath --rsync-path='sudo rsync' \
|
||||
--chown=root:root --chmod=D755,F644 \
|
||||
dist/ gitea_ci@"${WEB_HOST}":"${WEB_ROOT}/"
|
||||
ssh gitea_ci@"${WEB_HOST}" 'sudo /usr/sbin/restorecon -R '"${WEB_ROOT}"
|
||||
|
||||
- name: Sync nginx vhost + reload
|
||||
run: |
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0644 \
|
||||
rendered/site.conf \
|
||||
gitea_ci@"${WEB_HOST}":/etc/nginx/conf.d/"${SERVER_NAME}".conf
|
||||
ssh gitea_ci@"${WEB_HOST}" '
|
||||
set -euo pipefail
|
||||
sudo /usr/sbin/setsebool -P httpd_can_network_connect on
|
||||
if ! sudo /usr/sbin/semanage port -l | grep -E "^http_port_t" | grep -qw '"${API_PORT}"'; then
|
||||
sudo /usr/sbin/semanage port -a -t http_port_t -p tcp '"${API_PORT}"'
|
||||
fi
|
||||
sudo /usr/sbin/restorecon -R /etc/nginx/conf.d/'"${SERVER_NAME}"'.conf
|
||||
sudo /usr/sbin/nginx -t
|
||||
sudo /usr/bin/systemctl reload nginx'
|
||||
Reference in New Issue
Block a user