diff --git a/docs/devops/HOSTINGER_GITHUB_RUNNER_SETUP.md b/docs/devops/HOSTINGER_GITHUB_RUNNER_SETUP.md new file mode 100644 index 00000000..a4fa0815 --- /dev/null +++ b/docs/devops/HOSTINGER_GITHUB_RUNNER_SETUP.md @@ -0,0 +1,531 @@ +# Hostinger VM — GitHub Actions Self-Hosted Runner Setup + +> **Delegation prompt for the Codex agent running on the Hostinger VM.** +> Read top-to-bottom before executing. Stop and ask the human if any pre-flight check fails or any deliverable is unclear. + +--- + +## 1. Goal + +Set up a GitHub Actions self-hosted runner on the Hostinger VM that can: + +1. Receive workflow triggers from `saravanakumardb1/learning_ai_common_plat` (and, later, all `@bytelyst/*` repos). +2. Build `@bytelyst/*` npm packages from a tagged release. +3. Publish them to **the local Gitea instance on this VM** (`http://localhost:3300/api/packages/bytelyst/npm/`). +4. Upload the same tarballs as **GitHub Release assets** so a corp-network Mac can sync them into its own local Gitea (via a separate `bytelyst-sync` script described in a follow-up prompt). + +Self-hosted on Hostinger beats GitHub-hosted runners because: + +- No GitHub Actions minute cap. +- Gitea is on `localhost` from this VM → zero-latency publish, no public TLS needed. +- VM is always on; runner is reachable indefinitely. + +--- + +## 2. Pre-flight checks (run first, do not skip) + +```bash +# 1. Confirm Linux VM +hostname && uname -a # Expected: Linux + +# 2. Confirm Gitea is running locally +curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3300/ # Expected: 200 or 302 + +# 3. Confirm Gitea npm registry endpoint reachable +curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3300/api/packages/bytelyst/npm/ # Expected: 200 or 401 + +# 4. Confirm Node 20 and pnpm 9 installable +node --version 2>/dev/null || echo "Node not installed (will install in Step 5)" +pnpm --version 2>/dev/null || echo "pnpm not installed (will install in Step 5)" + +# 5. Confirm gh CLI exists +gh --version 2>/dev/null || echo "gh CLI not installed (will install in Step 5)" + +# 6. Disk free +df -h / # Need ~5 GB headroom + +# 7. Confirm no existing runner +ls -la ~/actions-runner 2>/dev/null && echo "Runner dir exists — STOP and confirm with human" || echo "No existing runner" + +# 8. Confirm github.com reachable +curl -s -o /dev/null -w "%{http_code}\n" https://api.github.com/ # Expected: 200 + +# 9. Confirm the Gitea token file exists somewhere on this VM +sudo find /home /root -maxdepth 3 -name ".gitea_npm_token" 2>/dev/null | head -5 +# Expected: at least one path. Note the owning user — needed in Step 6. +``` + +If any check fails or surprises you, **stop and report back** before proceeding. + +--- + +## 3. What you'll create + +| Item | Path/Identifier | +| -------------------------- | -------------------------------------------------------------------------------------- | +| Dedicated Linux user | `gha-runner` | +| Runner installation | `/home/gha-runner/actions-runner/` | +| systemd service | `actions.runner.saravanakumardb1-learning_ai_common_plat.hostinger-bytelyst-1.service` | +| Runner labels | `self-hosted, linux, x64, hostinger, bytelyst` | +| Gitea publish token (copy) | `/home/gha-runner/.gitea_npm_token` (mode 600) | +| Smoke-test workflow | `.github/workflows/runner-smoke.yml` (commit on a branch) | +| E2E test workflow | `.github/workflows/runner-e2e-publish.yml` (commit on a branch) | + +--- + +## 4. Installation + +### Step 1 — Create the dedicated runner user + +```bash +sudo useradd -m -s /bin/bash gha-runner +sudo usermod -aG docker gha-runner # only if any workflow uses Docker +id gha-runner +``` + +### Step 2 — Get a GitHub runner registration token (one-time, ~1h TTL) + +**Preferred (via `gh` CLI auth'd as `saravanakumardb1`):** + +```bash +gh api -X POST /repos/saravanakumardb1/learning_ai_common_plat/actions/runners/registration-token --jq .token +``` + +**Alternative (browser):** Open `https://github.com/saravanakumardb1/learning_ai_common_plat/settings/actions/runners/new?arch=x64&os=linux` and copy the token from the `./config.sh` command shown. + +Hold the token in shell memory only: + +```bash +read -s RUNNER_TOKEN # paste, press Enter (no echo) +``` + +### Step 3 — Download, verify, configure the runner + +```bash +sudo -iu gha-runner bash <<'EOF' +mkdir -p ~/actions-runner && cd ~/actions-runner +RUNNER_VERSION="2.319.1" + +# Download +curl -fSL -o "actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" \ + "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" + +# Verify SHA against the GitHub release page (https://github.com/actions/runner/releases/tag/v2.319.1). +# If the sha doesn't match, STOP and report. + +tar xzf "./actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" +EOF +``` + +Register: + +```bash +sudo -u gha-runner -E -i bash -c " +cd ~/actions-runner && \ +./config.sh \ + --url https://github.com/saravanakumardb1/learning_ai_common_plat \ + --token $RUNNER_TOKEN \ + --name hostinger-bytelyst-1 \ + --labels self-hosted,linux,x64,hostinger,bytelyst \ + --work _work \ + --replace \ + --unattended +" + +unset RUNNER_TOKEN +``` + +Verify in GitHub UI: runner should show "Idle" green under `Settings → Actions → Runners`. + +### Step 4 — Install as a systemd service + +```bash +sudo bash -c " +cd /home/gha-runner/actions-runner && \ +./svc.sh install gha-runner && \ +./svc.sh start +" + +SVC_NAME='actions.runner.saravanakumardb1-learning_ai_common_plat.hostinger-bytelyst-1.service' +sudo systemctl status "$SVC_NAME" +sudo journalctl -u "$SVC_NAME" -n 30 --no-pager +# Expected: "active (running)" + "Listening for Jobs" +``` + +### Step 5 — Install Node 20, pnpm 9, gh CLI system-wide + +```bash +# Node 20 via Nodesource +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash - +sudo apt-get install -y nodejs + +# gh CLI (if not already) +sudo apt-get install -y gh # or follow https://cli.github.com/manual/installation if 'gh' isn't in the apt repos + +# pnpm +sudo npm install -g pnpm@9 + +# Verify all reachable from the runner user +sudo -u gha-runner bash -c 'node --version && pnpm --version && gh --version' +``` + +### Step 6 — Give the runner access to the local Gitea publish token + +```bash +# Identify the source token file (from pre-flight check #9) +SRC_TOKEN=/home//.gitea_npm_token + +# Copy and lock down +sudo cp "$SRC_TOKEN" /home/gha-runner/.gitea_npm_token +sudo chown gha-runner:gha-runner /home/gha-runner/.gitea_npm_token +sudo chmod 600 /home/gha-runner/.gitea_npm_token + +# Verify +sudo -u gha-runner bash -c 'wc -c < ~/.gitea_npm_token && stat -c "%a %U:%G" ~/.gitea_npm_token' +# Expected: nonzero byte count, mode 600, owner gha-runner:gha-runner +``` + +--- + +## 5. Smoke test (basic — runner picks up jobs) + +Create branch `runner/smoke` in `learning_ai_common_plat` with this file: + +```yaml +# .github/workflows/runner-smoke.yml +name: Runner Smoke Test +on: + workflow_dispatch: + push: + branches: [runner/smoke] +jobs: + smoke: + runs-on: [self-hosted, linux, hostinger] + steps: + - run: echo "host=$(hostname) user=$(whoami) cwd=$(pwd)" + - run: node --version && pnpm --version && gh --version + - run: | + echo "Gitea health:" + curl -s -o /dev/null -w " http://localhost:3300/ → %{http_code}\n" http://localhost:3300/ + curl -s -o /dev/null -w " /api/packages/bytelyst/npm/ → %{http_code}\n" \ + http://localhost:3300/api/packages/bytelyst/npm/ + - run: | + if [ -f ~/.gitea_npm_token ]; then + echo "Gitea token present, $(wc -c < ~/.gitea_npm_token) bytes, mode $(stat -c %a ~/.gitea_npm_token)" + else + echo "ERROR: Gitea token missing" + exit 1 + fi +``` + +Trigger from GitHub UI (Actions tab → Runner Smoke Test → Run workflow on `runner/smoke`). All steps must pass. + +--- + +## 6. End-to-end validation (CRITICAL — proves the actual use case) + +The smoke test only proves the runner can execute. The **E2E test** proves the **whole publish pipeline** works: package builds, publishes to local Gitea, uploads to GitHub Releases, and is installable on a different machine. + +### E2E test workflow + +Create a **temporary test package** in `learning_ai_common_plat` (will be removed after validation): + +```bash +# On Hostinger or from human's machine — does NOT need to run on the runner +mkdir -p packages/_runner-e2e-test +cat > packages/_runner-e2e-test/package.json <<'EOF' +{ + "name": "@bytelyst/_runner-e2e-test", + "version": "0.0.1", + "description": "Throwaway package for E2E validating the Hostinger runner. Safe to delete after validation.", + "main": "index.js", + "files": ["index.js"] +} +EOF +echo "module.exports = { ok: true, builtAt: new Date().toISOString() };" \ + > packages/_runner-e2e-test/index.js +git checkout -b runner/e2e +git add packages/_runner-e2e-test/ +git commit -m "test: add throwaway package for runner E2E validation" +git push origin runner/e2e +``` + +Create the E2E workflow on the same branch: + +```yaml +# .github/workflows/runner-e2e-publish.yml +name: Runner E2E — publish + release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to publish (semver)' + required: true + default: '0.0.1-e2e.1' + +jobs: + publish: + runs-on: [self-hosted, linux, hostinger] + permissions: + contents: write # for GitHub Release creation + steps: + - uses: actions/checkout@v4 + + - name: Set test version + working-directory: packages/_runner-e2e-test + run: | + npm version "${{ inputs.version }}" --no-git-tag-version --allow-same-version + cat package.json + + - name: Configure pnpm registry for Gitea + working-directory: packages/_runner-e2e-test + run: | + cat > .npmrc < .npmrc < package.json < /dev/null <<'EOF' +/home/gha-runner/actions-runner/_diag/*.log { + weekly + rotate 4 + compress + missingok + notifempty + copytruncate +} +EOF +``` + +### d. Update mechanism + +GitHub auto-updates the runner agent unless disabled. Keep auto-update on. Pin a minimum version in workflows if you need a specific feature: + +```yaml +runs-on: [self-hosted, linux, hostinger] +``` + +### e. Auth scope of the runner + +The runner's `GITHUB_TOKEN` (provided by GitHub Actions automatically) is scoped per-workflow. Verify in `runner-e2e-publish.yml` that `permissions:` is set narrowly (we set `contents: write` only for release creation). + +--- + +## 8. Scaling to more repos later + +A single runner installation can serve multiple repos **only if** registered at org level. For your personal-account setup: + +- **Recommended:** Move all 20+ repos to a free GitHub organization. Register the runner once at org level. Single runner serves everyone. +- **Workaround for now:** Add the same runner to additional repos by re-running `config.sh` with each repo's URL and a fresh token (creates separate registrations sharing the same physical binary). Acceptable up to 2–3 repos. + +Recommend evaluating the org migration before scaling beyond 2 actively-publishing repos. + +--- + +## 9. Deliverables — report back to the human + +When complete: + +1. **Service status:** + ```bash + sudo systemctl is-active "$SVC_NAME" + ``` +2. **GitHub UI confirmation:** screenshot or text "Runner shows 'Idle' green at github.com/saravanakumardb1/learning_ai_common_plat/settings/actions/runners". +3. **Smoke test run URL** — workflow passed. +4. **E2E test run URL** — workflow passed, all 6 pass criteria green. +5. **Installed versions:** + ```bash + sudo -u gha-runner bash -c 'node --version; pnpm --version; gh --version; docker --version 2>/dev/null || echo "no docker"' + ``` +6. **Log paths:** + - systemd: `journalctl -u ` + - runner diag: `/home/gha-runner/actions-runner/_diag/` +7. **Confirmation that cleanup happened:** test release deleted, test package removed from Gitea, throwaway package deleted from repo. + +--- + +## 10. Guardrails + +- **Do not** run the runner as root. +- **Do not** persist the GitHub registration token to disk — memory only. +- **Do not** install under the `gitea` user or any other service user — keep concerns separated. +- **Do not** open inbound ports on the VM firewall — the runner is outbound-only long-poll. +- **Do not** skip the E2E test. The smoke test alone does not prove the publish pipeline works. +- **Do not** mark E2E as passed unless all 6 pass criteria succeed, especially the byte-identical tarball check. +- **Do not** leave the throwaway `@bytelyst/_runner-e2e-test` package in the Gitea registry — it pollutes the namespace. + +--- + +## 11. Rollback + +```bash +SVC_NAME='actions.runner.saravanakumardb1-learning_ai_common_plat.hostinger-bytelyst-1.service' + +# Stop and uninstall systemd service +sudo bash -c "cd /home/gha-runner/actions-runner && ./svc.sh stop && ./svc.sh uninstall" + +# Unregister from GitHub (need a fresh removal token) +REMOVAL_TOKEN=$(gh api -X POST /repos/saravanakumardb1/learning_ai_common_plat/actions/runners/remove-token --jq .token) +sudo -u gha-runner bash -c "cd ~/actions-runner && ./config.sh remove --token $REMOVAL_TOKEN" + +# Remove the user and all its files +sudo userdel -r gha-runner +``` + +--- + +## 12. Follow-up prompts (separate tasks) + +Once this runner is verified end-to-end, the next prompts to issue: + +1. **`publish-packages.yml`** — the real production workflow in `learning_ai_common_plat`, modeled on the E2E template above, that triggers on `v*` tags and publishes all changed `@bytelyst/*` packages. +2. **`bytelyst-sync` script** — runs on the corp Mac; downloads GitHub Release tarballs and republishes to the corp local Gitea. Verifies sha256 against Gitea before considering sync successful. +3. **SKILL doc** at `AI.dev/SKILLS/gitea-package-sync.md` — describes the full three-system flow for future contributors. + +--- + +## 13. Questions to ask the human BEFORE starting if anything is ambiguous + +- "Which GitHub repo am I registering this runner for? (default: `saravanakumardb1/learning_ai_common_plat`)" +- "Is Docker required on the runner — i.e., does any planned workflow run `docker` commands? (default: no, only Gitea uses Docker)" +- "What user currently owns `~/.gitea_npm_token` on this VM? (pre-flight check #9 will tell us)" +- "Do you have a runner registration token, or should I fetch one via `gh api`?" +- "Are you OK with me creating a throwaway `@bytelyst/_runner-e2e-test` package, publishing it, and then deleting it as part of E2E validation?" + +If any of these are unclear, stop and ask before installing anything.