This repo is public, so .gitea/workflows/ci.yml was publishing the web host's tailnet address in a plaintext env: block, together with the deploy account, the checkout path and the unit restarted under sudo -- a complete map of the deploy for any anonymous reader. Host, account, path and service now come from repo secrets (DEPLOY_HOST, DEPLOY_USER, DEPLOY_PATH, DEPLOY_SERVICE), which Gitea masks in run logs. The path and unit name are passed to the remote shell as positional args inside a quoted heredoc so they are never interpolated into the log either. The deploy key secret is renamed CLOUD2_SSH_KEY -> DEPLOY_SSH_KEY to match; it was never actually set, which is why every deploy run so far is red. The rest of docs/DEPLOY.md already used the web-host/build-host pseudonyms; this drops the remaining absolute deploy-account paths from its prose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012AaUFYkUTsJn1fnJ89qbvW
128 lines
4.8 KiB
YAML
128 lines
4.8 KiB
YAML
# CI/CD — verify on every push, ship main when green.
|
|
#
|
|
# Runs on the act_runner registered on build-host (label `ubuntu-latest`, Docker-backed).
|
|
# Same shape as music.karti.ai: verify and deploy are two jobs in ONE workflow joined by
|
|
# `needs:`, because Gitea's cross-workflow triggers are less reliable than GitHub's. A red
|
|
# build cannot deploy.
|
|
#
|
|
# This repo is PUBLIC, so nothing here may name the deploy target. The host, the account,
|
|
# the checkout path and the service name all come from repo secrets; the workflow only
|
|
# describes the shape of the deploy.
|
|
#
|
|
# Requires these repo secrets:
|
|
# DEPLOY_SSH_KEY — private half of a deploy key whose public half is in the deploy
|
|
# account's authorized_keys on the web host
|
|
# DEPLOY_HOST — the web host to deploy to
|
|
# DEPLOY_USER — the account to ssh in as
|
|
# DEPLOY_PATH — the checkout on the web host that is reset to origin/main
|
|
# DEPLOY_SERVICE — the systemd unit restarted after the build
|
|
|
|
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: ["**"]
|
|
pull_request:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
verify:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Harness imports and registry parses
|
|
# Catches a malformed models.yaml or a broken target before it reaches
|
|
# the site build, where the failure would be far less legible.
|
|
run: |
|
|
python3 -m pip install --quiet pyyaml
|
|
python3 - <<'PY'
|
|
import sys; sys.path.insert(0, '.')
|
|
from kbench.registry import load_registry
|
|
reg = load_registry()
|
|
for t in reg.targets.values():
|
|
t.inspect_model # raises on an unserveable target
|
|
print(f"registry ok: {len(reg.targets)} targets, {len(reg.hosts)} hosts")
|
|
PY
|
|
|
|
- name: Results files are valid and schema-consistent
|
|
run: |
|
|
python3 - <<'PY'
|
|
import json, pathlib, sys
|
|
bad = 0
|
|
for p in sorted(pathlib.Path('results').glob('*.json')):
|
|
d = json.loads(p.read_text())
|
|
for field in ('run_id', 'timestamp', 'target_id', 'target', 'schema_version'):
|
|
if field not in d:
|
|
print(f"::error::{p.name} missing {field}"); bad += 1
|
|
print(f"checked results files, {bad} problem(s)")
|
|
sys.exit(1 if bad else 0)
|
|
PY
|
|
|
|
- name: Build site
|
|
working-directory: site
|
|
run: |
|
|
npm install --no-audit --no-fund
|
|
npm run build
|
|
|
|
deploy:
|
|
needs: verify
|
|
# Gitea populates the **github** context, not a `gitea` one — `gitea.ref` silently
|
|
# evaluates to nothing and the job is skipped with no error.
|
|
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Set up SSH
|
|
env:
|
|
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
|
run: |
|
|
mkdir -p ~/.ssh
|
|
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key
|
|
chmod 600 ~/.ssh/deploy_key
|
|
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
|
|
|
- name: Deploy to the web host
|
|
env:
|
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
|
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
|
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
|
|
DEPLOY_SERVICE: ${{ secrets.DEPLOY_SERVICE }}
|
|
run: |
|
|
# The path and unit name are passed as positional args, not interpolated:
|
|
# the heredoc is quoted so the runner never expands them into the log.
|
|
ssh -i ~/.ssh/deploy_key -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" \
|
|
bash -s -- "$DEPLOY_PATH" "$DEPLOY_SERVICE" <<'REMOTE'
|
|
set -euo pipefail
|
|
checkout="$1"
|
|
service="$2"
|
|
cd "$checkout"
|
|
git fetch origin -q
|
|
git reset --hard origin/main -q
|
|
cd site
|
|
npm install --no-audit --no-fund
|
|
npm run build
|
|
sudo systemctl restart "$service"
|
|
REMOTE
|
|
|
|
- name: Verify the deploy
|
|
env:
|
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
|
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
|
DEPLOY_SERVICE: ${{ secrets.DEPLOY_SERVICE }}
|
|
run: |
|
|
for i in $(seq 1 20); do
|
|
if curl -sf https://bench.karti.ai/api/health | grep -q '"ok":true'; then
|
|
echo "✓ bench.karti.ai is healthy"
|
|
exit 0
|
|
fi
|
|
sleep 3
|
|
done
|
|
echo "::error::bench.karti.ai did not come back healthy after deploy"
|
|
ssh -i ~/.ssh/deploy_key -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" \
|
|
bash -s -- "$DEPLOY_SERVICE" <<'REMOTE' || true
|
|
sudo journalctl -u "$1" -n 40 --no-pager
|
|
REMOTE
|
|
exit 1
|