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 ...
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:
ROADMAP.md— execution tracker (this doc's checklist).PUBLISH_WORKFLOW.md— the publish workflow this runner executes._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:
- Registers with the local Hostinger Gitea instance (
http://localhost:3300) at instance level (one registration covers all repos and orgs). - Picks up Gitea Actions workflows triggered by push, tag, or
workflow_dispatchfrom any@bytelyst/*repo in this Gitea. - 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.
- Has access to the Gitea publish token (
~/.gitea_npm_token) forpnpm publishtohttp://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 publishto localhost — no network hop. - No GitHub Organization migration needed → no Vercel/Netlify pricing entanglements.
- Corp Mac runs an identical
act_runneragainst 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 Test → Run workflow on runner/gitea-smoke).
If the localhost-from-container step fails: add
--add-host=host.docker.internal:host-gatewayto yourcontainer.optionsinconfig.yaml, OR setcontainer.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
sudo systemctl is-active gitea-act-runner.service→ output- Gitea UI confirmation: runner shows "Idle" green at
<gitea>/-/admin/actions/runners - Smoke test workflow URL (passed)
- E2E test workflow URL (passed, all 6 pass criteria green)
- Cross-Gitea SHA comparison output (must show ✅ PASS)
- Installed versions:
/usr/local/bin/act_runner --version docker --version docker run --rm node:20-bookworm node --version - Log paths:
- systemd:
journalctl -u gitea-act-runner.service - act_runner workspaces:
/home/gitea-runner/act_runner/.cache/
- systemd:
- Confirmation that cleanup happened: test version deleted from both Giteas, throwaway package removed, branch deleted.
10. Guardrails
- Do not run
act_runneras root. - Do not persist the registration token to disk — memory only during
register. - Do not use
container.network: hostfor 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_tokeninto 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-testpackage 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:
- Implement
PUBLISH_WORKFLOW.md— the actualpublish-packages.ymlworkflow for@bytelyst/*packages, modeled after the E2E template. - Propagate that workflow to all 20+ repos (script-driven).
- 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_tokenon 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-testpackage, 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.