learning_ai_common_plat/docs/devops/gitea-runner/ACT_RUNNER_SETUP.md
saravanakumardb1 7381d0f5c0 docs(devops): group Gitea runner docs under gitea-runner/ subfolder
Moves 5 related docs into docs/devops/gitea-runner/ to keep this
multi-doc workstream from colliding with future roadmaps and
delegation prompts in docs/devops/.

Renames:
  HOSTINGER_GITEA_RUNNER_ROADMAP.md     -> ROADMAP.md
  HOSTINGER_GITEA_ACT_RUNNER_SETUP.md   -> ACT_RUNNER_SETUP.md
  GITEA_PACKAGES_PUBLISH_WORKFLOW.md    -> PUBLISH_WORKFLOW.md
  HOSTINGER_GITHUB_RUNNER_SETUP.md      -> _PLAN_B_GITHUB_RUNNER.md
  CODEX_DELEGATION_PROMPT.md            -> (same name, moved)

All internal cross-links updated via sed sweep. Verified no stale
references remain.

Adds README.md in the new folder as the index + pattern doc for
future multi-doc workstreams (one-liner handoff, file map,
architecture summary).

Updated one-liner handoff path:
  Read docs/devops/gitea-runner/CODEX_DELEGATION_PROMPT.md ...
2026-05-24 18:33:45 -07:00

26 KiB

Hostinger VM — Gitea Actions act_runner Setup

📋 Track progress in the master roadmap: 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:


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)

# 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)

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

# 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:

# 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

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

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

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)

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

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

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

# 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

# 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

# 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

# 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:

# 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:
    /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

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 — 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.