# 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