# Hostinger VM — Gitea Actions `act_runner` Setup

> 📋 **Track progress in the master roadmap:** [`ROADMAP.md`](./ROADMAP.md). The roadmap has the checklist; this doc has the implementation detail. Codex updates checkboxes + commit hashes in the roadmap as each phase completes.

> **Delegation prompt for the Codex agent running on the Hostinger VM.**
> Canonical CI runner setup for the ByteLyst ecosystem. Read top-to-bottom before executing. Stop and ask the human if any pre-flight check fails or any deliverable is unclear.

> Companion docs:
>
> - [`ROADMAP.md`](./ROADMAP.md) — **execution tracker (this doc's checklist)**.
> - [`PUBLISH_WORKFLOW.md`](./PUBLISH_WORKFLOW.md) — the publish workflow this runner executes.
> - [`_PLAN_B_GITHUB_RUNNER.md`](./_PLAN_B_GITHUB_RUNNER.md) — Plan B (GitHub Actions runner). Kept for reference.

---

## 1. Goal

Install a `act_runner` daemon on the Hostinger VM that:

1. Registers with the **local Hostinger Gitea instance** (`http://localhost:3300`) at **instance level** (one registration covers all repos and orgs).
2. Picks up Gitea Actions workflows triggered by push, tag, or `workflow_dispatch` from any `@bytelyst/*` repo in this Gitea.
3. Runs jobs inside a deterministic Linux Docker image so tarball builds are byte-identical to what the corp Mac runner produces from the same git tag.
4. Has access to the Gitea publish token (`~/.gitea_npm_token`) for `pnpm publish` to `http://localhost:3300/api/packages/bytelyst/npm/`.

Self-hosted Gitea Actions on Hostinger is preferred over GitHub Actions runners because:

- Workflows already exist at `.gitea/workflows/` across all 20+ repos.
- Zero-latency `pnpm publish` to localhost — no network hop.
- No GitHub Organization migration needed → no Vercel/Netlify pricing entanglements.
- Corp Mac runs an identical `act_runner` against its own local Gitea, so both registries get byte-identical tarballs from the same tag push — **no sync script required**.

---

## 2. Pre-flight checks (run first, do not skip)

```bash
# 1. Confirm Linux VM
hostname && uname -a       # Expected: Linux

# 2. Detect architecture (for act_runner binary download)
ARCH=$(uname -m)
case "$ARCH" in
  x86_64)  export RUNNER_ARCH="linux-amd64";;
  aarch64) export RUNNER_ARCH="linux-arm64";;
  *) echo "Unsupported arch: $ARCH — STOP and report"; ;;
esac
echo "Will install act_runner for: $RUNNER_ARCH"

# 3. Confirm Gitea is running
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3300/   # Expected: 200 or 302

# 4. Confirm Gitea Actions is enabled (check the API exposes the Actions endpoint)
curl -s http://localhost:3300/api/v1/version | head -c 200; echo
# Expected: a JSON {"version":"1.21+ ..."} — Actions requires Gitea 1.21+

# 5. 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

# 6. Confirm Docker is installed and the daemon is running
docker --version 2>/dev/null && docker info >/dev/null 2>&1 && echo "Docker OK" \
  || echo "ERROR: Docker not installed or daemon not running — REQUIRED for act_runner"

# 7. Disk free
df -h /     # Need ~10 GB headroom (Docker images + workspace caches)

# 8. Confirm no existing act_runner
ls -la ~/act_runner 2>/dev/null && echo "act_runner dir exists — STOP and confirm with human" \
  || echo "No existing act_runner — good"

# 9. Confirm Gitea admin or an account with admin scope exists (need it for registration token)
#    Either look up the admin password the human has, or have them pre-create an admin token.

# 10. Confirm the Gitea publish token file exists 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 5.
```

If any check fails or surprises you, **stop and report back** before proceeding.

---

## 3. What you'll create

| Item                       | Path/Identifier                                                    |
| -------------------------- | ------------------------------------------------------------------ |
| Dedicated Linux user       | `gitea-runner`                                                     |
| Binary install path        | `/usr/local/bin/act_runner`                                        |
| Runner config + data dir   | `/home/gitea-runner/act_runner/`                                   |
| systemd service            | `gitea-act-runner.service`                                         |
| Runner labels              | `ubuntu-latest:docker://node:20-bookworm,linux,bytelyst,hostinger` |
| Gitea publish token (copy) | `/home/gitea-runner/.gitea_npm_token` (mode 600)                   |
| Smoke test workflow        | `.gitea/workflows/runner-smoke.yml` (on a branch)                  |
| E2E test workflow          | `.gitea/workflows/runner-e2e-publish.yml` (on a branch)            |

The label `ubuntu-latest:docker://node:20-bookworm` is the magic that maps Gitea Actions' `runs-on: ubuntu-latest` to a deterministic Node 20 container image — every job runs in the same image regardless of host, giving byte-identical builds.

---

## 4. Installation

### Step 1 — Create the dedicated runner user (idempotent)

```bash
if ! getent passwd gitea-runner >/dev/null; then
  sudo useradd -m -s /bin/bash gitea-runner
  echo "Created gitea-runner user"
else
  echo "gitea-runner user already exists — skipping useradd"
fi

# Docker access is required — act_runner shells into Docker for every job
sudo usermod -aG docker gitea-runner
id gitea-runner   # confirm docker group membership
```

### Step 2 — Download and verify `act_runner`

```bash
# Query latest release from Gitea's own act_runner repo
LATEST=$(curl -s https://gitea.com/api/v1/repos/gitea/act_runner/releases/latest \
  | grep -oE '"tag_name":"v[0-9.]+"' | head -1 | cut -d'"' -f4)
echo "Latest act_runner: $LATEST"
# Sanity check: should be vX.Y.Z with X >= 0 and a recent date. STOP and confirm if version looks wrong.

# Download binary + checksum
cd /tmp
BIN_URL="https://gitea.com/gitea/act_runner/releases/download/${LATEST}/act_runner-${LATEST#v}-${RUNNER_ARCH}"
SHA_URL="${BIN_URL}.sha256"

curl -fSL -o act_runner "${BIN_URL}"
curl -fSL -o act_runner.sha256 "${SHA_URL}"

# Verify SHA256
EXPECTED_SHA=$(awk '{print $1}' < act_runner.sha256)
ACTUAL_SHA=$(sha256sum act_runner | awk '{print $1}')
if [ "$EXPECTED_SHA" != "$ACTUAL_SHA" ]; then
  echo "FAIL: SHA mismatch"
  echo "  Expected: $EXPECTED_SHA"
  echo "  Actual:   $ACTUAL_SHA"
  exit 1
fi
echo "PASS: act_runner binary SHA verified"

chmod +x act_runner
sudo mv act_runner /usr/local/bin/act_runner
/usr/local/bin/act_runner --version    # confirm
```

### Step 3 — Get a registration token from Gitea

Three places to register a runner in Gitea:

| Scope                            | Where the token comes from                           | Coverage                                  |
| -------------------------------- | ---------------------------------------------------- | ----------------------------------------- |
| **Instance-level** (recommended) | Site Admin → Actions → Runners → "Create new Runner" | All repos in this Gitea, current + future |
| Org-level                        | Org settings → Actions → Runners                     | All repos in one org                      |
| Repo-level                       | Repo settings → Actions → Runners                    | One repo only                             |

**Recommended: instance-level.** Hand the token off to the codex shell:

```bash
# Either: have the human paste it
read -s REG_TOKEN

# Or: via Gitea API (requires admin token in GITEA_ADMIN_TOKEN env)
REG_TOKEN=$(curl -s -H "Authorization: token $GITEA_ADMIN_TOKEN" \
  -X GET "http://localhost:3300/api/v1/admin/runners/registration-token" \
  | grep -oE '"token":"[^"]+"' | cut -d'"' -f4)
echo "Got registration token (length: ${#REG_TOKEN})"
```

### Step 4 — Register the runner

```bash
sudo -u gitea-runner -E -i bash <<EOF
mkdir -p ~/act_runner && cd ~/act_runner

# Generate default config and customize
act_runner generate-config > config.yaml

# Patch key fields (use sed or rewrite). Important settings:
#   - Use Docker mode (NOT host mode) so jobs run in a deterministic image
#   - Use a labels map that pins ubuntu-latest -> node:20-bookworm
cat > config.yaml <<YAML
log:
  level: info

runner:
  file: .runner
  capacity: 2
  envs:
    NODE_OPTIONS: "--max-old-space-size=4096"
  labels:
    - "ubuntu-latest:docker://node:20-bookworm"
    - "linux:docker://node:20-bookworm"
    - "bytelyst:docker://node:20-bookworm"
    - "hostinger:docker://node:20-bookworm"

cache:
  enabled: true
  dir: ".cache"
  host: ""
  port: 0

container:
  network: bridge
  privileged: false
  options: ""
  workdir_parent: ""
  valid_volumes: []
  docker_host: "unix:///var/run/docker.sock"
  force_pull: false

host:
  workdir_parent: ""
YAML

# Register
act_runner register --no-interactive \
  --instance http://localhost:3300 \
  --token "$REG_TOKEN" \
  --name hostinger-bytelyst-1 \
  --labels "ubuntu-latest:docker://node:20-bookworm,linux:docker://node:20-bookworm,bytelyst,hostinger" \
  --config config.yaml

ls -la .runner config.yaml
EOF

unset REG_TOKEN
```

Verify in Gitea UI: `Site Admin → Actions → Runners`. New runner shows "Idle".

### Step 5 — Give the runner the Gitea publish token

```bash
SRC_TOKEN=$(sudo find /home /root -maxdepth 3 -name ".gitea_npm_token" 2>/dev/null | head -1)
echo "Copying from $SRC_TOKEN"

sudo cp "$SRC_TOKEN" /home/gitea-runner/.gitea_npm_token
sudo chown gitea-runner:gitea-runner /home/gitea-runner/.gitea_npm_token
sudo chmod 600 /home/gitea-runner/.gitea_npm_token

# Verify
sudo -u gitea-runner bash -c 'wc -c < ~/.gitea_npm_token && stat -c "%a %U:%G" ~/.gitea_npm_token'
```

### Step 6 — Install systemd service for auto-start

```bash
sudo tee /etc/systemd/system/gitea-act-runner.service > /dev/null <<'EOF'
[Unit]
Description=Gitea Actions Runner (act_runner)
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service

[Service]
Type=simple
User=gitea-runner
Group=gitea-runner
WorkingDirectory=/home/gitea-runner/act_runner
ExecStart=/usr/local/bin/act_runner daemon --config /home/gitea-runner/act_runner/config.yaml
Restart=always
RestartSec=10

# Resource limits — adjust to your VM
CPUQuota=200%
MemoryMax=4G
TasksMax=2048

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now gitea-act-runner.service

sudo systemctl status gitea-act-runner.service
sudo journalctl -u gitea-act-runner.service -n 50 --no-pager
# Expected: "active (running)" + log line like "Runner registered successfully" + "Polling"
```

---

## 5. Smoke test (basic — runner picks up jobs)

```bash
cd ~/code/mygh/learning_ai_common_plat
git checkout -b runner/gitea-smoke
mkdir -p .gitea/workflows
# (paste workflow below into .gitea/workflows/runner-smoke.yml)
git add .gitea/workflows/runner-smoke.yml
git commit -m "ci: add Gitea act_runner smoke-test workflow"
git push gitea runner/gitea-smoke
```

```yaml
# .gitea/workflows/runner-smoke.yml
name: Gitea Runner Smoke Test
on:
  workflow_dispatch:
  push:
    branches: [runner/gitea-smoke]

jobs:
  smoke:
    runs-on: [ubuntu-latest, bytelyst, hostinger]
    steps:
      - run: echo "host=$(hostname) user=$(whoami) image=$(cat /etc/os-release | grep ^NAME)"
      - run: node --version && npm --version
      - run: |
          # Install pnpm fresh in the container
          npm install -g pnpm@9
          pnpm --version
      - run: |
          # Use host network because gitea is on localhost:3300 outside the container
          # NOTE: container network mode is bridged by default. To reach host's localhost,
          # use host.docker.internal:3300 OR set container.network: host in config.yaml.
          echo "Gitea health (from inside runner container):"
          curl -s -o /dev/null -w "  via host.docker.internal:3300 → %{http_code}\n" \
            http://host.docker.internal:3300/ || echo "(host.docker.internal not available; check network mode)"
      - run: |
          # Check whether the runner-injected token file is mounted (it's not by default;
          # we'll mount it in the publish workflow via volumes:, not here)
          ls -la / | head -30
```

Trigger from Gitea UI (`Actions` tab → `Gitea Runner Smoke Test` → `Run workflow` on `runner/gitea-smoke`).

> **If the localhost-from-container step fails:** add `--add-host=host.docker.internal:host-gateway` to your `container.options` in `config.yaml`, OR set `container.network: host` (less isolated but eliminates the issue entirely). For the publish workflow, host networking is the most reliable choice.

---

## 6. End-to-end validation — proves the actual publish pipeline

Same throwaway-package pattern as the GitHub Actions doc, but in Gitea Actions and with the byte-identical-tarball guarantee verified across both Giteas (the key invariant).

### Create throwaway test package

```bash
cd ~/code/mygh/learning_ai_common_plat
git checkout -b runner/gitea-e2e
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 Gitea runner. Safe to delete.",
  "main": "index.js",
  "files": ["index.js"]
}
EOF
echo "module.exports = { ok: true, builtAt: new Date().toISOString() };" \
  > packages/_runner-e2e-test/index.js
git add packages/_runner-e2e-test/
git commit -m "test: throwaway package for Gitea runner E2E"
git push gitea runner/gitea-e2e
git push origin runner/gitea-e2e   # also push to GitHub for parity
```

### E2E workflow

```yaml
# .gitea/workflows/runner-e2e-publish.yml
name: Gitea Runner E2E — publish + integrity check
on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Test version to publish'
        required: true
        default: '0.0.1-e2e.1'

jobs:
  publish:
    runs-on: [ubuntu-latest, bytelyst, hostinger]

    # Mount the host's Gitea token AND use host networking so we can reach
    # localhost:3300 directly (the Gitea instance on the Hostinger VM).
    container:
      image: node:20-bookworm
      options: --network host -v /home/gitea-runner/.gitea_npm_token:/run/secrets/gitea_npm_token:ro

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install pnpm
        run: npm install -g pnpm@9

      - 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 registry .npmrc
        working-directory: packages/_runner-e2e-test
        run: |
          TOKEN=$(cat /run/secrets/gitea_npm_token)
          cat > .npmrc <<NPMRC
          @bytelyst:registry=http://localhost:3300/api/packages/bytelyst/npm/
          //localhost:3300/api/packages/bytelyst/npm/:_authToken=$TOKEN
          NPMRC
          echo ".npmrc (token masked):"
          sed 's|_authToken=.*|_authToken=***|' .npmrc

      - name: Publish to local Gitea
        working-directory: packages/_runner-e2e-test
        run: pnpm publish --no-git-checks --registry http://localhost:3300/api/packages/bytelyst/npm/

      - name: Pack tarball for hash comparison
        working-directory: packages/_runner-e2e-test
        run: pnpm pack --pack-destination /tmp

      - name: Verify package returned by Gitea
        run: |
          TOKEN=$(cat /run/secrets/gitea_npm_token)
          RESP=$(curl -s -H "Authorization: Bearer $TOKEN" \
            "http://localhost:3300/api/packages/bytelyst/npm/@bytelyst%2F_runner-e2e-test")
          echo "$RESP" | head -300
          echo "$RESP" | grep -q '"${{ inputs.version }}"' || { echo "FAIL: version missing from Gitea"; exit 1; }
          echo "PASS: version in Gitea"

      - name: Verify pnpm install + require works from a clean dir
        run: |
          TOKEN=$(cat /run/secrets/gitea_npm_token)
          mkdir -p /tmp/runner-e2e-consumer && cd /tmp/runner-e2e-consumer
          cat > .npmrc <<NPMRC
          @bytelyst:registry=http://localhost:3300/api/packages/bytelyst/npm/
          //localhost:3300/api/packages/bytelyst/npm/:_authToken=$TOKEN
          NPMRC
          cat > package.json <<JSON
          { "name": "consumer", "version": "1.0.0", "dependencies": { "@bytelyst/_runner-e2e-test": "${{ inputs.version }}" } }
          JSON
          pnpm install --no-frozen-lockfile
          node -e "const m=require('@bytelyst/_runner-e2e-test'); console.log(m); process.exit(m.ok?0:1);"
          echo "PASS: install + require works"

      - name: Compute tarball SHA512 (will be compared against corp Gitea after sync)
        run: |
          TARBALL=$(ls /tmp/bytelyst-_runner-e2e-test-*.tgz)
          echo "Tarball: $TARBALL"
          sha512sum "$TARBALL" | tee /tmp/hostinger-tarball.sha512
          # ALSO record the SHA512 reported by Gitea registry — these must match
          TOKEN=$(cat /run/secrets/gitea_npm_token)
          GITEA_SHA=$(curl -s -H "Authorization: Bearer $TOKEN" \
            "http://localhost:3300/api/packages/bytelyst/npm/@bytelyst%2F_runner-e2e-test/${{ inputs.version }}" \
            | grep -oE '"shasum":"[^"]*"' | head -1 | cut -d'"' -f4)
          echo "Gitea-reported shasum (SHA1): $GITEA_SHA"
          # Verify the locally-packed tarball matches what Gitea stored
          LOCAL_SHA=$(sha1sum "$TARBALL" | awk '{print $1}')
          [ "$LOCAL_SHA" = "$GITEA_SHA" ] || { echo "FAIL: pnpm pack tarball doesn't match Gitea-stored tarball"; exit 1; }
          echo "PASS: byte-identical tarball stored in Gitea"
```

### Cross-Gitea byte-identical-tarball verification

This is the **architectural invariant** that makes the dual-Gitea model work. After Hostinger publishes `0.0.1-e2e.1`, the same tag should land on corp Mac's Gitea and produce **the same tarball SHA**.

```bash
# After Hostinger E2E completes, push the same branch to corp Gitea
# (assumes you're on a Mac that has both `gitea` (corp) and is connected to home network OR the human runs this on corp Mac)
cd ~/code/mygh/learning_ai_common_plat
git checkout runner/gitea-e2e
git push gitea runner/gitea-e2e   # triggers corp Gitea Actions

# Wait for corp Mac Gitea Actions to finish publishing
# Then compare:
HOSTINGER_SHA=$(curl -s "http://gitea.bytelyst.com/api/packages/bytelyst/npm/@bytelyst%2F_runner-e2e-test/0.0.1-e2e.1" \
  | grep -oE '"shasum":"[^"]*"' | head -1 | cut -d'"' -f4)
CORP_SHA=$(curl -s -H "Authorization: token $(cat ~/.gitea_npm_token)" \
  "http://localhost:3300/api/packages/bytelyst/npm/@bytelyst%2F_runner-e2e-test/0.0.1-e2e.1" \
  | grep -oE '"shasum":"[^"]*"' | head -1 | cut -d'"' -f4)

echo "Hostinger: $HOSTINGER_SHA"
echo "Corp:      $CORP_SHA"
[ "$HOSTINGER_SHA" = "$CORP_SHA" ] && echo "✅ PASS: cross-Gitea byte-identical guarantee holds" \
                                   || echo "❌ FAIL: builds diverged — investigate Node/pnpm/lockfile version differences"
```

### E2E pass criteria — ALL must succeed

| Check                                                | Where it lives                                                 |
| ---------------------------------------------------- | -------------------------------------------------------------- |
| Hostinger runner picks up the job                    | Gitea Actions UI, run logs show `runner: hostinger-bytelyst-1` |
| `pnpm publish` to Hostinger Gitea succeeds           | Workflow step exits 0                                          |
| Gitea registry returns the new version               | `Verify package returned by Gitea` step                        |
| Consumer `pnpm install` works                        | `Verify pnpm install + require works` step                     |
| Tarball stored in Gitea matches local pack           | `Compute tarball SHA512` step's SHA1 match                     |
| **Hostinger tarball SHA1 = corp Gitea tarball SHA1** | Cross-Gitea comparison above                                   |

The last one is the killer feature: same git tag → byte-identical tarballs on both Giteas → lockfiles work everywhere with no sync mechanism.

### Cleanup after E2E passes

```bash
# Delete the test version from BOTH Gitea instances (via UI: Packages → @bytelyst/_runner-e2e-test → Delete)
# Or via API:
curl -X DELETE -H "Authorization: token $(cat ~/.gitea_npm_token)" \
  "http://localhost:3300/api/packages/bytelyst/npm/@bytelyst%2F_runner-e2e-test/-/@bytelyst%2F_runner-e2e-test-0.0.1-e2e.1.tgz"

# Delete the test branch
git checkout main
git push gitea --delete runner/gitea-e2e
git push origin --delete runner/gitea-e2e
rm -rf packages/_runner-e2e-test
git checkout main
```

Leave both workflow files (`runner-smoke.yml`, `runner-e2e-publish.yml`) on `main` — idempotent re-validation tools.

---

## 7. Monitoring + observability

### a. Gitea Actions UI (per repo)

`http://gitea.bytelyst.com/<owner>/<repo>/actions` — every workflow run with status, logs, runner name.

### b. Runner pool health (instance-wide)

`http://gitea.bytelyst.com/-/admin/actions/runners` (admin only). Shows status (`Idle` / `Active` / `Offline`) and last seen.

### c. Scripted monitoring

```bash
# List runners
curl -s -H "Authorization: token $GITEA_ADMIN_TOKEN" \
  "http://localhost:3300/api/v1/admin/runners" | jq .

# List recent workflow runs in a repo
curl -s -H "Authorization: token $GITEA_NPM_TOKEN" \
  "http://localhost:3300/api/v1/repos/bytelyst/learning_ai_common_plat/actions/tasks" | jq '.workflow_runs[] | {id, status, conclusion, created_at}'
```

### d. Host-side

```bash
# Live tail
sudo journalctl -u gitea-act-runner.service -f

# Per-job logs (act_runner stores them under workspace dir)
ls -la /home/gitea-runner/act_runner/.cache/

# systemd state
sudo systemctl status gitea-act-runner.service
```

---

## 8. Hardening

Same systemd `[Service]` directives as in §4 Step 6 (CPUQuota, MemoryMax, TasksMax). Plus:

```bash
# Log rotation
sudo tee /etc/logrotate.d/gitea-act-runner > /dev/null <<'EOF'
/var/log/journal/*/system.journal {
    # journalctl handles its own rotation; nothing custom needed for systemd logs
}
EOF

# Restrict the runner's Docker access — it should NOT be able to mount arbitrary host paths
# The config.yaml `valid_volumes: []` already restricts this. Verify.

# Pin Docker image digest in config.yaml (optional but strongly recommended for production)
# Instead of: ubuntu-latest:docker://node:20-bookworm
# Use:        ubuntu-latest:docker://node:20-bookworm@sha256:<exact-digest>
```

---

## 9. Deliverables — report back to the human

1. `sudo systemctl is-active gitea-act-runner.service` → output
2. Gitea UI confirmation: runner shows "Idle" green at `<gitea>/-/admin/actions/runners`
3. Smoke test workflow URL (passed)
4. E2E test workflow URL (passed, all 6 pass criteria green)
5. Cross-Gitea SHA comparison output (must show ✅ PASS)
6. Installed versions:
   ```bash
   /usr/local/bin/act_runner --version
   docker --version
   docker run --rm node:20-bookworm node --version
   ```
7. Log paths:
   - systemd: `journalctl -u gitea-act-runner.service`
   - act_runner workspaces: `/home/gitea-runner/act_runner/.cache/`
8. Confirmation that cleanup happened: test version deleted from both Giteas, throwaway package removed, branch deleted.

---

## 10. Guardrails

- **Do not** run `act_runner` as root.
- **Do not** persist the registration token to disk — memory only during `register`.
- **Do not** use `container.network: host` for workflows that run untrusted code (host network gives the container full access to localhost services). For our publish workflow it's intentional and fine — the workflow only runs trusted code on tag pushes.
- **Do not** mount `/home/gitea-runner/.gitea_npm_token` into the smoke workflow; only the publish workflow needs it.
- **Do not** skip the cross-Gitea SHA comparison — it's the architectural invariant.
- **Do not** mark E2E as passed unless all 6 pass criteria succeed.
- **Do not** leave the throwaway `@bytelyst/_runner-e2e-test` package in either Gitea registry.

---

## 11. Rollback

```bash
sudo systemctl stop gitea-act-runner.service
sudo systemctl disable gitea-act-runner.service
sudo rm /etc/systemd/system/gitea-act-runner.service
sudo systemctl daemon-reload

# Unregister from Gitea (need an admin token)
RUNNER_ID=$(curl -s -H "Authorization: token $GITEA_ADMIN_TOKEN" \
  "http://localhost:3300/api/v1/admin/runners" | grep -oE '"id":[0-9]+,"name":"hostinger-bytelyst-1"' | head -1 | grep -oE '[0-9]+' | head -1)
curl -X DELETE -H "Authorization: token $GITEA_ADMIN_TOKEN" \
  "http://localhost:3300/api/v1/admin/runners/$RUNNER_ID"

sudo userdel -r gitea-runner
sudo rm /usr/local/bin/act_runner
```

---

## 12. Follow-up

Once this runner passes all E2E criteria:

1. Implement [`PUBLISH_WORKFLOW.md`](./PUBLISH_WORKFLOW.md) — the actual `publish-packages.yml` workflow for `@bytelyst/*` packages, modeled after the E2E template.
2. Propagate that workflow to all 20+ repos (script-driven).
3. Add a SKILL doc at `AI.dev/SKILLS/gitea-package-publishing.md`.

---

## 13. Questions to ask the human BEFORE starting if anything is ambiguous

- "Which Gitea instance am I registering with? (default: `http://localhost:3300`, the local Hostinger Gitea)"
- "What is the Gitea admin token, or can you generate a registration token from Site Admin → Actions?"
- "Confirm: registering at **instance level** (one runner, all repos)? Default yes."
- "What user currently owns `~/.gitea_npm_token` on this VM? (pre-flight check #10)"
- "Should I pin the Node image to a specific digest (e.g., `node:20.18.0-bookworm@sha256:...`) for build determinism, or use the floating tag for now?"
- "Are you OK with me creating a throwaway `@bytelyst/_runner-e2e-test` package, publishing it to both Giteas, and then deleting it as part of E2E validation?"

If any of these are unclear, stop and ask before installing anything.
