ver 0.1
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# custom-gitea-runner environment template
|
||||
# Copy this file to .env and fill in real values.
|
||||
# Never commit .env to version control.
|
||||
|
||||
# ─── Required for bootstrap registration (Mode B) ────────────────────────────
|
||||
GITEA_INSTANCE_URL=https://git.example.com
|
||||
GITEA_RUNNER_TOKEN=your-registration-token-here
|
||||
GITEA_RUNNER_NAME=unraid-runner-01
|
||||
|
||||
# ─── Optional registration settings ─────────────────────────────────────────
|
||||
# Comma-separated label list. Each label maps a GitHub Actions runner label
|
||||
# to an act_runner execution environment.
|
||||
# Format: <label>:<scheme>://<image>
|
||||
# For host-based execution (no inner container): label:host
|
||||
GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:20-bullseye,host:host
|
||||
|
||||
# ─── Runner runtime ───────────────────────────────────────────────────────────
|
||||
# Path inside the container where the runner config YAML lives.
|
||||
RUNNER_CONFIG_PATH=/config/runner.yaml
|
||||
|
||||
# Working directory for job runs inside the container.
|
||||
RUNNER_WORKDIR=/work
|
||||
|
||||
# ─── Optional: Timezone ───────────────────────────────────────────────────────
|
||||
TZ=America/Chicago
|
||||
|
||||
# ─── Optional: Registry credentials (used by workflows, not the runner itself)
|
||||
# These are typically set as Gitea secrets in your repository, not here.
|
||||
# REGISTRY_USER=your-registry-user
|
||||
# REGISTRY_TOKEN=your-registry-token
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# Runtime secrets and local config
|
||||
.env
|
||||
.runner
|
||||
|
||||
# Generated runner config (mount, don't commit)
|
||||
config/runner.yaml
|
||||
|
||||
# Local work directory
|
||||
work/
|
||||
|
||||
# Custom CA files (mount, don't commit)
|
||||
config/trusted-ca/*.crt
|
||||
config/trusted-ca/*.pem
|
||||
|
||||
# Build artifacts
|
||||
*.tar
|
||||
*.tar.gz
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
@@ -0,0 +1,25 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Initial repository scaffold: directory structure, Dockerfile, entrypoint, healthcheck, smoke-test, config examples, compose example, and docs.
|
||||
- Phase 0 planning documents: `AGENT.md`, `README.md`, `ROADMAP.md`.
|
||||
- First successful local image build: `custom-gitea-runner:v0.1.0` (act_runner v0.2.11, Node 20, Docker CLI). Runtime smoke test passes 17/17.
|
||||
- First live registration validated against `git.alwisp.com` (Gitea 1.26.4): instance-level runner registers, comes online, runs healthy, and restart reuses the existing config without creating a duplicate.
|
||||
|
||||
### Fixed
|
||||
- `docker/runner/Dockerfile`: `COPY` paths for the entrypoint, healthcheck, and smoke-test scripts now resolve from the repo-root build context used by `scripts/build-image.sh` (previously assumed the build context was `docker/runner/`, so the build failed at the `COPY` steps).
|
||||
- `scripts/verify-runtime.sh`: now runs the smoke test via `--entrypoint` so it bypasses the `tini`/bootstrap `ENTRYPOINT` instead of triggering the registration flow and failing on a missing `GITEA_INSTANCE_URL`.
|
||||
- `docker/runner/docker-entrypoint.sh`: `GITEA_RUNNER_LABELS` is now honored. `act_runner register` ignores its `--labels` flag when a config already exists, so the bootstrap generated a config with default labels and the env var was silently a no-op. The entrypoint now injects the labels into the generated config before registering.
|
||||
- `scripts/deregister-runner.sh`: rewritten to deregister via the Gitea REST API. The previous version called `act_runner daemon --unregister`, a flag/command that does not exist in act_runner — deregistration must go through `DELETE /api/v1/admin/actions/runners/{id}`. Now takes a runner name or id and an admin API token.
|
||||
|
||||
---
|
||||
|
||||
<!-- Releases appear below this line, newest first. -->
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Jason Stedwell
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,20 @@
|
||||
# labels.example.env — runner label configuration reference
|
||||
#
|
||||
# Labels tell act_runner which GitHub Actions runner environments to emulate.
|
||||
# Format: <label>:<scheme>://<image>
|
||||
# docker:// — run the job step inside the specified container image
|
||||
# host — run the job step directly on the runner host (no inner container)
|
||||
#
|
||||
# Set GITEA_RUNNER_LABELS in .env or docker-compose to one of the examples below.
|
||||
|
||||
# ─── Match the ubuntu-latest label used in most GitHub Actions workflows ────
|
||||
GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:20-bullseye
|
||||
|
||||
# ─── Multiple labels (comma-separated) ─────────────────────────────────────
|
||||
# GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:20-bullseye,ubuntu-22.04:docker://node:20-bullseye,host:host
|
||||
|
||||
# ─── Host-only mode (no inner Docker containers) ────────────────────────────
|
||||
# GITEA_RUNNER_LABELS=host:host
|
||||
|
||||
# ─── Custom image on self-hosted registry ───────────────────────────────────
|
||||
# GITEA_RUNNER_LABELS=ubuntu-latest:docker://registry.alwisp.com/ci/runner-base:latest
|
||||
@@ -0,0 +1,45 @@
|
||||
# runner.example.yaml — act_runner configuration template
|
||||
# Copy to config/runner.yaml and adjust for your environment.
|
||||
# This file is generated/modified by `act_runner register`. Fields below
|
||||
# reflect commonly tuned settings; see act_runner docs for the full reference.
|
||||
|
||||
log:
|
||||
# Log verbosity: trace, debug, info, warn, error, fatal
|
||||
level: info
|
||||
|
||||
runner:
|
||||
# How many jobs this runner can execute concurrently
|
||||
capacity: 1
|
||||
# Extra environment variables injected into every job
|
||||
envs: {}
|
||||
# Maximum runtime for a single job
|
||||
timeout: 3h0m0s
|
||||
# Allow insecure (non-TLS) connections to the Gitea instance
|
||||
insecure: false
|
||||
# Fetch interval for new jobs
|
||||
fetch_timeout: 5s
|
||||
fetch_interval: 2s
|
||||
|
||||
cache:
|
||||
# Enable the built-in cache server (speeds up actions/cache steps)
|
||||
enabled: true
|
||||
dir: ""
|
||||
host: ""
|
||||
port: 0
|
||||
external_server: ""
|
||||
|
||||
container:
|
||||
# Docker network mode for job containers
|
||||
network: bridge
|
||||
# Run job containers with --privileged (avoid unless required)
|
||||
privileged: false
|
||||
# Additional docker run options (space-separated)
|
||||
options: ""
|
||||
# Override parent directory for job workspaces
|
||||
workdir_parent: ""
|
||||
# Uncomment to use a custom Docker daemon socket
|
||||
# docker_host: unix:///var/run/docker.sock
|
||||
|
||||
host:
|
||||
# Override parent directory for host-mode workspaces
|
||||
workdir_parent: ""
|
||||
@@ -0,0 +1,48 @@
|
||||
# docker-compose.example.yml — custom-gitea-runner deployment
|
||||
# Copy and adjust for your environment. Use pinned image tags in production.
|
||||
#
|
||||
# Modes:
|
||||
# Mode A (pre-generated config): mount config/runner.yaml — set RUNNER_CONFIG_PATH
|
||||
# Mode B (bootstrap on first run): set GITEA_INSTANCE_URL, GITEA_RUNNER_TOKEN, GITEA_RUNNER_NAME
|
||||
|
||||
services:
|
||||
gitea-runner:
|
||||
image: registry.alwisp.com/custom-gitea-runner:v0.1.0 # pin your version here
|
||||
container_name: gitea-runner
|
||||
restart: unless-stopped
|
||||
|
||||
environment:
|
||||
# ── Required for Mode B (bootstrap registration) ───────────────────────
|
||||
GITEA_INSTANCE_URL: https://git.example.com
|
||||
GITEA_RUNNER_NAME: unraid-runner-01
|
||||
GITEA_RUNNER_TOKEN: ${GITEA_RUNNER_TOKEN} # set in .env file
|
||||
|
||||
# ── Optional ──────────────────────────────────────────────────────────
|
||||
GITEA_RUNNER_LABELS: ubuntu-latest:docker://node:20-bullseye
|
||||
RUNNER_CONFIG_PATH: /config/runner.yaml
|
||||
RUNNER_WORKDIR: /work
|
||||
TZ: America/Chicago
|
||||
|
||||
volumes:
|
||||
# Runner config (persisted across restarts; generated on first boot in Mode B)
|
||||
- ./config:/config
|
||||
|
||||
# Job workspace (writable scratch space for running jobs)
|
||||
- ./work:/work
|
||||
|
||||
# Docker socket — required for docker build/push workflows
|
||||
# Grants the runner full Docker daemon access. Understand the trust model.
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
# Limit resources to avoid runaway jobs starving the host
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "4"
|
||||
memory: 4G
|
||||
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,75 @@
|
||||
# Unraid Template Notes — custom-gitea-runner
|
||||
|
||||
This document covers the recommended Unraid Docker container configuration for the custom-gitea-runner.
|
||||
|
||||
---
|
||||
|
||||
## Container basics
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Repository | `registry.alwisp.com/custom-gitea-runner:v0.1.0` |
|
||||
| Container name | `gitea-runner` |
|
||||
| Network type | `Bridge` |
|
||||
| Privileged | `No` (leave unchecked — socket mount handles Docker access) |
|
||||
|
||||
---
|
||||
|
||||
## Environment variables
|
||||
|
||||
Set these in the "Environment Variables" section of the Unraid container template.
|
||||
|
||||
| Variable | Required | Value |
|
||||
|---|---|---|
|
||||
| `GITEA_INSTANCE_URL` | Yes | `https://git.example.com` |
|
||||
| `GITEA_RUNNER_TOKEN` | Yes | Registration token from Gitea admin |
|
||||
| `GITEA_RUNNER_NAME` | Yes | e.g. `unraid-runner-01` |
|
||||
| `GITEA_RUNNER_LABELS` | No | `ubuntu-latest:docker://node:20-bullseye` |
|
||||
| `RUNNER_CONFIG_PATH` | No | `/config/runner.yaml` |
|
||||
| `RUNNER_WORKDIR` | No | `/work` |
|
||||
| `TZ` | No | `America/Chicago` |
|
||||
|
||||
---
|
||||
|
||||
## Volume mounts
|
||||
|
||||
| Container path | Host path | Notes |
|
||||
|---|---|---|
|
||||
| `/config` | `/mnt/user/appdata/gitea-runner/config` | Config persistence. Generated on first boot. |
|
||||
| `/work` | `/mnt/user/appdata/gitea-runner/work` | Job workspace. Can be cleaned between runs. |
|
||||
| `/var/run/docker.sock` | `/var/run/docker.sock` | Docker socket. Required for build/push workflows. |
|
||||
|
||||
---
|
||||
|
||||
## Post-install checklist
|
||||
|
||||
1. Set all required environment variables.
|
||||
2. Start the container.
|
||||
3. Check logs — look for `[BOOTSTRAP] Registration complete` or `[START] Starting act_runner daemon`.
|
||||
4. In Gitea admin panel, verify the runner appears under **Admin → Runners**.
|
||||
5. Trigger a test workflow and confirm job pickup.
|
||||
|
||||
---
|
||||
|
||||
## Update workflow
|
||||
|
||||
1. Pull the new image tag from the registry.
|
||||
2. In Unraid, update the Repository field to the new pinned tag.
|
||||
3. Stop → Start the container (Unraid recreates from the new image).
|
||||
4. Confirm registration still valid in Gitea admin panel.
|
||||
5. Run a smoke workflow.
|
||||
|
||||
---
|
||||
|
||||
## Rollback workflow
|
||||
|
||||
1. Stop the container.
|
||||
2. Change the Repository field back to the prior known-good tag.
|
||||
3. Start the container.
|
||||
4. Verify registration and smoke workflow.
|
||||
|
||||
---
|
||||
|
||||
## Notes on Docker socket
|
||||
|
||||
Mounting `/var/run/docker.sock` gives the runner access to the host Docker daemon. This is intentional for build/push workflows. Understand this means jobs running on this runner can interact with all containers on the Unraid host. For a self-owned, trusted environment this is acceptable. Future hardening options are documented in `docs/architecture.md`.
|
||||
@@ -0,0 +1,83 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# custom-gitea-runner — self-owned Gitea Actions runner
|
||||
# All dependencies installed at build time; no runtime apt-get.
|
||||
|
||||
ARG UBUNTU_VERSION=22.04
|
||||
FROM ubuntu:${UBUNTU_VERSION}
|
||||
|
||||
ARG ACT_RUNNER_VERSION=0.2.11
|
||||
ARG NODE_MAJOR=20
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG VERSION=dev
|
||||
|
||||
LABEL org.opencontainers.image.title="custom-gitea-runner" \
|
||||
org.opencontainers.image.description="Self-owned Gitea Actions runner with bundled tooling" \
|
||||
org.opencontainers.image.version="${VERSION}" \
|
||||
org.opencontainers.image.created="${BUILD_DATE}" \
|
||||
org.opencontainers.image.revision="${VCS_REF}"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
IMAGE_VERSION="${VERSION}" \
|
||||
GIT_REVISION="${VCS_REF}" \
|
||||
RUNNER_CONFIG_PATH=/config/runner.yaml \
|
||||
RUNNER_WORKDIR=/work
|
||||
|
||||
# ─── Base packages ────────────────────────────────────────────────────────────
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
bash \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
gnupg \
|
||||
jq \
|
||||
tini \
|
||||
tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ─── Node.js (NodeSource — Ubuntu 22.04 default is 12.x, too old) ─────────────
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ─── Docker CLI ───────────────────────────────────────────────────────────────
|
||||
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
|
||||
| gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \
|
||||
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
||||
> /etc/apt/sources.list.d/docker.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends docker-ce-cli \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ─── act_runner binary (pinned) ───────────────────────────────────────────────
|
||||
# To update: bump ACT_RUNNER_VERSION in the build command or here.
|
||||
# Releases: https://gitea.com/gitea/act_runner/releases
|
||||
RUN ARCH=$(dpkg --print-architecture) \
|
||||
&& curl -fsSL \
|
||||
"https://gitea.com/gitea/act_runner/releases/download/v${ACT_RUNNER_VERSION}/act_runner-${ACT_RUNNER_VERSION}-linux-${ARCH}" \
|
||||
-o /usr/local/bin/act_runner \
|
||||
&& chmod +x /usr/local/bin/act_runner
|
||||
|
||||
# ─── Runtime directories and scripts ─────────────────────────────────────────
|
||||
RUN mkdir -p /config /work /config/trusted-ca
|
||||
|
||||
COPY docker/runner/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
COPY docker/runner/healthcheck.sh /usr/local/bin/healthcheck.sh
|
||||
COPY docker/runner/smoke-test.sh /usr/local/bin/smoke-test.sh
|
||||
|
||||
RUN chmod +x \
|
||||
/usr/local/bin/docker-entrypoint.sh \
|
||||
/usr/local/bin/healthcheck.sh \
|
||||
/usr/local/bin/smoke-test.sh
|
||||
|
||||
VOLUME ["/config", "/work"]
|
||||
|
||||
HEALTHCHECK \
|
||||
--interval=30s \
|
||||
--timeout=10s \
|
||||
--start-period=60s \
|
||||
--retries=3 \
|
||||
CMD ["/usr/local/bin/healthcheck.sh"]
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"]
|
||||
Executable
+139
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env bash
|
||||
# docker-entrypoint.sh — custom-gitea-runner bootstrap and daemon startup
|
||||
# Runs as PID 1 via tini. Handles validation, optional CA trust, registration, and daemon start.
|
||||
set -euo pipefail
|
||||
|
||||
RUNNER_CONFIG_PATH="${RUNNER_CONFIG_PATH:-/config/runner.yaml}"
|
||||
RUNNER_WORKDIR="${RUNNER_WORKDIR:-/work}"
|
||||
|
||||
# ─── Banner ───────────────────────────────────────────────────────────────────
|
||||
|
||||
print_banner() {
|
||||
echo "======================================================"
|
||||
echo " custom-gitea-runner"
|
||||
echo " Image version : ${IMAGE_VERSION:-unknown}"
|
||||
echo " Git revision : ${GIT_REVISION:-unknown}"
|
||||
echo "======================================================"
|
||||
echo "Runtime:"
|
||||
echo " act_runner : $(act_runner --version 2>&1 | head -1 || echo 'unknown')"
|
||||
echo " node : $(node --version 2>&1 || echo 'unknown')"
|
||||
echo " npm : $(npm --version 2>&1 || echo 'unknown')"
|
||||
echo " git : $(git --version 2>&1 | awk '{print $3}' || echo 'unknown')"
|
||||
echo " docker : $(docker --version 2>&1 | awk '{print $3}' | tr -d ',' || echo 'unavailable')"
|
||||
echo "Config:"
|
||||
echo " Config path : ${RUNNER_CONFIG_PATH}"
|
||||
echo " Work dir : ${RUNNER_WORKDIR}"
|
||||
echo " Gitea URL : ${GITEA_INSTANCE_URL:-[not set]}"
|
||||
echo " Runner name : ${GITEA_RUNNER_NAME:-[not set]}"
|
||||
echo " Labels : ${GITEA_RUNNER_LABELS:-[not set]}"
|
||||
echo "======================================================"
|
||||
}
|
||||
|
||||
# ─── Binary validation ────────────────────────────────────────────────────────
|
||||
|
||||
validate_binaries() {
|
||||
local missing=0
|
||||
for bin in act_runner git node npm; do
|
||||
if ! command -v "$bin" &>/dev/null; then
|
||||
echo "[ERROR] Required binary not found: ${bin}"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
if [ "$missing" -ne 0 ]; then
|
||||
echo "[FATAL] Missing required binaries. This is an image build issue."
|
||||
exit 1
|
||||
fi
|
||||
echo "[OK] All required binaries present."
|
||||
}
|
||||
|
||||
# ─── Custom CA trust (optional) ───────────────────────────────────────────────
|
||||
|
||||
install_custom_ca() {
|
||||
local ca_dir="/config/trusted-ca"
|
||||
if [ -d "$ca_dir" ] && compgen -G "${ca_dir}/*.crt" >/dev/null 2>&1; then
|
||||
echo "[CA] Installing custom CA certificates from ${ca_dir} ..."
|
||||
cp "${ca_dir}"/*.crt /usr/local/share/ca-certificates/ 2>/dev/null || true
|
||||
update-ca-certificates --fresh 2>&1 | grep -v "^$" || true
|
||||
echo "[CA] Custom CA trust updated."
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Registration (Mode B: bootstrap from env vars) ───────────────────────────
|
||||
|
||||
require_env() {
|
||||
local var="$1"
|
||||
if [ -z "${!var:-}" ]; then
|
||||
echo "[FATAL] Required environment variable not set: ${var}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Rewrite the `labels:` list in the runner config. act_runner reads labels
|
||||
# from the config file at registration time and IGNORES the `register --labels`
|
||||
# flag whenever the config already exists — so to honor GITEA_RUNNER_LABELS we
|
||||
# must edit the generated config before registering.
|
||||
apply_labels() {
|
||||
local labels_csv="$1" config="$2" tmp
|
||||
echo "[BOOTSTRAP] Applying labels: ${labels_csv}"
|
||||
tmp="$(mktemp)"
|
||||
awk -v csv="${labels_csv}" '
|
||||
BEGIN { n = split(csv, arr, ",") }
|
||||
/^[[:space:]]+labels:[[:space:]]*$/ {
|
||||
print
|
||||
for (i = 1; i <= n; i++) {
|
||||
gsub(/^[ \t]+|[ \t]+$/, "", arr[i])
|
||||
printf " - \"%s\"\n", arr[i]
|
||||
}
|
||||
inlabels = 1
|
||||
next
|
||||
}
|
||||
inlabels == 1 {
|
||||
if ($0 ~ /^[[:space:]]+-[[:space:]]/) { next }
|
||||
inlabels = 0
|
||||
}
|
||||
{ print }
|
||||
' "${config}" > "${tmp}" && mv "${tmp}" "${config}"
|
||||
}
|
||||
|
||||
register_runner() {
|
||||
echo "[BOOTSTRAP] No config found at ${RUNNER_CONFIG_PATH}. Initiating registration."
|
||||
|
||||
require_env GITEA_INSTANCE_URL
|
||||
require_env GITEA_RUNNER_TOKEN
|
||||
require_env GITEA_RUNNER_NAME
|
||||
|
||||
mkdir -p "$(dirname "${RUNNER_CONFIG_PATH}")"
|
||||
|
||||
# Generate the default config template (includes cache/container defaults),
|
||||
# then inject the operator's labels into it before registering.
|
||||
act_runner generate-config > "${RUNNER_CONFIG_PATH}"
|
||||
|
||||
local labels="${GITEA_RUNNER_LABELS:-ubuntu-latest:docker://node:20-bullseye}"
|
||||
apply_labels "${labels}" "${RUNNER_CONFIG_PATH}"
|
||||
|
||||
act_runner register \
|
||||
--config "${RUNNER_CONFIG_PATH}" \
|
||||
--instance "${GITEA_INSTANCE_URL}" \
|
||||
--token "${GITEA_RUNNER_TOKEN}" \
|
||||
--name "${GITEA_RUNNER_NAME}" \
|
||||
--no-interactive
|
||||
|
||||
echo "[BOOTSTRAP] Registration complete. Config at ${RUNNER_CONFIG_PATH}."
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
print_banner
|
||||
validate_binaries
|
||||
install_custom_ca
|
||||
|
||||
mkdir -p "${RUNNER_WORKDIR}"
|
||||
|
||||
if [ ! -f "${RUNNER_CONFIG_PATH}" ]; then
|
||||
register_runner
|
||||
else
|
||||
echo "[INFO] Existing config found at ${RUNNER_CONFIG_PATH}. Skipping registration."
|
||||
fi
|
||||
|
||||
echo "[START] Starting act_runner daemon ..."
|
||||
exec act_runner daemon --config "${RUNNER_CONFIG_PATH}"
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# healthcheck.sh — reports runner health for Docker HEALTHCHECK instruction
|
||||
set -euo pipefail
|
||||
|
||||
RUNNER_CONFIG_PATH="${RUNNER_CONFIG_PATH:-/config/runner.yaml}"
|
||||
exit_code=0
|
||||
|
||||
check_ok() { echo "[HEALTH] OK : $1"; }
|
||||
check_warn() { echo "[HEALTH] WARN : $1"; }
|
||||
check_fail() { echo "[HEALTH] FAIL : $1"; exit_code=1; }
|
||||
|
||||
# Runner process
|
||||
if pgrep -x act_runner > /dev/null 2>&1; then
|
||||
check_ok "act_runner process is running"
|
||||
else
|
||||
check_fail "act_runner process not found"
|
||||
fi
|
||||
|
||||
# Config file
|
||||
if [ -f "${RUNNER_CONFIG_PATH}" ]; then
|
||||
check_ok "Config file present at ${RUNNER_CONFIG_PATH}"
|
||||
else
|
||||
check_warn "Config file not found at ${RUNNER_CONFIG_PATH}"
|
||||
fi
|
||||
|
||||
# Required binaries
|
||||
for bin in act_runner git node npm; do
|
||||
if command -v "$bin" &>/dev/null; then
|
||||
check_ok "Binary present: ${bin}"
|
||||
else
|
||||
check_fail "Binary missing: ${bin}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Docker socket (warn only — some label configurations don't need it)
|
||||
if [ -S /var/run/docker.sock ]; then
|
||||
if docker info &>/dev/null 2>&1; then
|
||||
check_ok "Docker socket accessible"
|
||||
else
|
||||
check_warn "Docker socket present but not accessible (permission issue?)"
|
||||
fi
|
||||
else
|
||||
check_warn "Docker socket not mounted — Docker build/push workflows will fail"
|
||||
fi
|
||||
|
||||
exit $exit_code
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# smoke-test.sh — quick runtime self-test for the runner image
|
||||
# Run inside the container to validate the tool chain before production use.
|
||||
set -euo pipefail
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
ok() { echo "[SMOKE] PASS : $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo "[SMOKE] FAIL : $1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
echo "======================================"
|
||||
echo " custom-gitea-runner smoke test"
|
||||
echo "======================================"
|
||||
|
||||
# Binary presence and basic execution
|
||||
for bin in bash curl git jq node npm act_runner tini docker; do
|
||||
if command -v "$bin" &>/dev/null; then
|
||||
ok "${bin} found: $(command -v "$bin")"
|
||||
else
|
||||
fail "${bin} not found"
|
||||
fi
|
||||
done
|
||||
|
||||
# Version output checks
|
||||
echo "--- versions ---"
|
||||
git --version && ok "git --version" || fail "git --version failed"
|
||||
node --version && ok "node --version" || fail "node --version failed"
|
||||
npm --version && ok "npm --version" || fail "npm --version failed"
|
||||
act_runner --version 2>&1 | head -1 && ok "act_runner --version" || fail "act_runner --version failed"
|
||||
docker --version && ok "docker --version" || fail "docker --version failed"
|
||||
|
||||
# Node.js can run a basic script
|
||||
if node -e "process.exit(0)" 2>/dev/null; then
|
||||
ok "node can execute scripts"
|
||||
else
|
||||
fail "node cannot execute scripts"
|
||||
fi
|
||||
|
||||
# jq parses JSON
|
||||
if echo '{"test":1}' | jq .test > /dev/null 2>&1; then
|
||||
ok "jq parses JSON"
|
||||
else
|
||||
fail "jq cannot parse JSON"
|
||||
fi
|
||||
|
||||
# curl is usable (don't actually make a request in smoke test)
|
||||
if curl --version &>/dev/null; then
|
||||
ok "curl is usable"
|
||||
else
|
||||
fail "curl is not usable"
|
||||
fi
|
||||
|
||||
echo "======================================"
|
||||
echo " Results: ${PASS} passed, ${FAIL} failed"
|
||||
echo "======================================"
|
||||
|
||||
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
||||
@@ -0,0 +1,84 @@
|
||||
# Architecture
|
||||
|
||||
## Design summary
|
||||
|
||||
The runner is a Docker image that wraps the official `act_runner` binary in a controlled, predictable environment. All dependencies are installed at image build time. The running container is long-lived and operator-managed.
|
||||
|
||||
---
|
||||
|
||||
## Layers
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Gitea instance (git.alwisp.com) │ ← dispatches jobs via act protocol
|
||||
└───────────────────┬─────────────────────────┘
|
||||
│ HTTP / WebSocket
|
||||
┌───────────────────▼─────────────────────────┐
|
||||
│ custom-gitea-runner container │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ tini (PID 1) │ │
|
||||
│ │ └── docker-entrypoint.sh │ │
|
||||
│ │ ├── validate binaries │ │
|
||||
│ │ ├── install custom CAs │ │
|
||||
│ │ ├── register (first boot only) │ │
|
||||
│ │ └── act_runner daemon │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Bundled tools (installed at build time): │
|
||||
│ bash, curl, git, jq, node, npm, │
|
||||
│ docker CLI, ca-certificates, tini │
|
||||
└───────────────────┬─────────────────────────┘
|
||||
│ /var/run/docker.sock mount
|
||||
┌───────────────────▼─────────────────────────┐
|
||||
│ Host Docker daemon │ ← runs job containers
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key design decisions
|
||||
|
||||
See `docs/decisions/` for ADR-style records of each major choice.
|
||||
|
||||
| Decision | Choice | Doc |
|
||||
|---|---|---|
|
||||
| Base image | Ubuntu 22.04 | `decisions/0001-base-image.md` |
|
||||
| act_runner strategy | Wrap official binary | — |
|
||||
| Docker access | Socket mount (`/var/run/docker.sock`) | — |
|
||||
| PID 1 | `tini` | — |
|
||||
| Node.js source | NodeSource (Node 20 LTS) | — |
|
||||
| v1 user model | root (Docker socket requires it) | — |
|
||||
|
||||
---
|
||||
|
||||
## Registration modes
|
||||
|
||||
**Mode A — Pre-generated config (preferred for stable production)**
|
||||
|
||||
Operator mounts a pre-generated `runner.yaml` at `RUNNER_CONFIG_PATH`. The entrypoint detects the file and skips registration, going straight to daemon start.
|
||||
|
||||
**Mode B — Bootstrap registration (first boot)**
|
||||
|
||||
If `RUNNER_CONFIG_PATH` does not exist, the entrypoint reads `GITEA_INSTANCE_URL`, `GITEA_RUNNER_TOKEN`, and `GITEA_RUNNER_NAME` from the environment, generates a default config, and calls `act_runner register`. Subsequent starts use the persisted config (Mode A path).
|
||||
|
||||
This flow is idempotent — reboots with an existing config will not re-register.
|
||||
|
||||
---
|
||||
|
||||
## Docker socket model
|
||||
|
||||
The runner container mounts the host Docker socket (`/var/run/docker.sock`). This allows:
|
||||
- `docker build` and `docker push` in workflow steps
|
||||
- `docker/login-action` against external registries
|
||||
|
||||
**Trust implication:** a workflow running on this runner has full access to the host Docker daemon. This is acceptable for a trusted, self-hosted environment. Future hardening options (DinD, rootless, network isolation) are tracked in `ROADMAP.md` Phase 6.
|
||||
|
||||
---
|
||||
|
||||
## Future hardening tracks
|
||||
|
||||
- Rootless container execution
|
||||
- Docker-in-Docker (DinD) variant for true isolation
|
||||
- Dedicated BuildKit daemon
|
||||
- Ephemeral single-job runners
|
||||
- Network policy enforcement
|
||||
@@ -0,0 +1,49 @@
|
||||
# ADR 0001 — Base Image Selection
|
||||
|
||||
**Status:** Accepted
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The runner needs a base OS image that provides:
|
||||
- A working apt package ecosystem for installing `git`, `curl`, `jq`, `tini`, `ca-certificates`, `tzdata`.
|
||||
- Compatibility with Docker's official apt repository (for Docker CLI).
|
||||
- Compatibility with NodeSource (for Node.js 20 LTS).
|
||||
- A label environment the existing workflows reference: `ubuntu-latest`.
|
||||
|
||||
Candidates considered:
|
||||
1. `ubuntu:22.04` — LTS, wide ecosystem support, good compatibility.
|
||||
2. `ubuntu:24.04` — Newer LTS, some tooling still stabilizing.
|
||||
3. `debian:bookworm-slim` — Smaller footprint, good compatibility, but `ubuntu-latest` label mismatch.
|
||||
4. `debian:bullseye-slim` — Older, stable, but further from `ubuntu-latest` expectations.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
Use `ubuntu:22.04` as the base image, pinned by exact tag.
|
||||
|
||||
---
|
||||
|
||||
## Rationale
|
||||
|
||||
- Existing Gitea workflows use `runs-on: ubuntu-latest`. Basing the runner image on Ubuntu 22.04 maximizes compatibility with GitHub Actions steps written for that environment.
|
||||
- Ubuntu 22.04 LTS has long-term support through 2027, reducing forced base image churn.
|
||||
- Docker's apt repository and NodeSource both officially support Ubuntu 22.04.
|
||||
- Ubuntu 22.04 is the most common self-hosted runner base image in community examples, so troubleshooting resources are abundant.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
- Final image size will be larger than a minimal Alpine or Debian slim base, which is acceptable given the goal of bundling all runtime dependencies.
|
||||
- When the workflow ecosystem moves to `ubuntu-24.04`, the base image can be bumped with a version argument change (`ARG UBUNTU_VERSION=24.04`).
|
||||
|
||||
---
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Debian bookworm-slim:** Smaller image, but the `ubuntu-latest` label in existing workflows creates an expectation of Ubuntu compatibility. Switching to Debian may cause subtle incompatibilities with actions that check the OS ID.
|
||||
|
||||
**Ubuntu 24.04:** Not yet the default for `ubuntu-latest` as of the initial build date; 22.04 remains the more tested choice for self-hosted runners.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Operations
|
||||
|
||||
## Normal startup sequence
|
||||
|
||||
When the container starts, expect to see:
|
||||
|
||||
```
|
||||
======================================================
|
||||
custom-gitea-runner
|
||||
Image version : v0.1.0
|
||||
Git revision : abc1234
|
||||
======================================================
|
||||
Runtime:
|
||||
act_runner : v0.2.11
|
||||
node : v20.x.x
|
||||
npm : 10.x.x
|
||||
git : 2.x.x
|
||||
docker : 24.x.x
|
||||
Config:
|
||||
Config path : /config/runner.yaml
|
||||
Work dir : /work
|
||||
Gitea URL : https://git.example.com
|
||||
Runner name : unraid-runner-01
|
||||
Labels : ubuntu-latest:docker://node:20-bullseye
|
||||
======================================================
|
||||
[OK] All required binaries present.
|
||||
[INFO] Existing config found at /config/runner.yaml. Skipping registration.
|
||||
[START] Starting act_runner daemon ...
|
||||
```
|
||||
|
||||
On first boot with no config:
|
||||
|
||||
```
|
||||
[BOOTSTRAP] No config found at /config/runner.yaml. Initiating registration.
|
||||
[BOOTSTRAP] Registration complete. Config at /config/runner.yaml.
|
||||
[START] Starting act_runner daemon ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Health check
|
||||
|
||||
The container runs a health check every 30s. Check status with:
|
||||
|
||||
```bash
|
||||
docker inspect --format='{{.State.Health.Status}}' gitea-runner
|
||||
```
|
||||
|
||||
Or view health check output:
|
||||
|
||||
```bash
|
||||
docker inspect --format='{{json .State.Health}}' gitea-runner | jq .
|
||||
```
|
||||
|
||||
Expected healthy output from `healthcheck.sh`:
|
||||
|
||||
```
|
||||
[HEALTH] OK : act_runner process is running
|
||||
[HEALTH] OK : Config file present at /config/runner.yaml
|
||||
[HEALTH] OK : Binary present: act_runner
|
||||
[HEALTH] OK : Binary present: git
|
||||
[HEALTH] OK : Binary present: node
|
||||
[HEALTH] OK : Binary present: npm
|
||||
[HEALTH] OK : Docker socket accessible
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Smoke test
|
||||
|
||||
Before production use, run the smoke test against the image:
|
||||
|
||||
```bash
|
||||
./scripts/verify-runtime.sh custom-gitea-runner:v0.1.0
|
||||
```
|
||||
|
||||
Or directly:
|
||||
|
||||
```bash
|
||||
docker run --rm custom-gitea-runner:v0.1.0 /usr/local/bin/smoke-test.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checking runner registration in Gitea
|
||||
|
||||
1. Go to your Gitea instance.
|
||||
2. Navigate to **Site Administration → Runners** (admin panel) or **Repository Settings → Runners**.
|
||||
3. The runner should appear with status **Online**.
|
||||
|
||||
---
|
||||
|
||||
## Upgrade procedure
|
||||
|
||||
1. Build or pull the new pinned image tag.
|
||||
2. Review `CHANGELOG.md` for breaking changes.
|
||||
3. Stop the existing container.
|
||||
4. Update the image tag in your compose file or Unraid template.
|
||||
5. Start the container.
|
||||
6. Verify startup logs and health check status.
|
||||
7. Run a smoke workflow to confirm job execution works.
|
||||
|
||||
---
|
||||
|
||||
## Rollback procedure
|
||||
|
||||
1. Stop the container.
|
||||
2. Revert the image tag to the prior known-good version.
|
||||
3. Start the container.
|
||||
4. Confirm the registration config is still valid (it should be — it's persisted in the config volume).
|
||||
5. Run a smoke workflow.
|
||||
|
||||
---
|
||||
|
||||
## Re-registration
|
||||
|
||||
If you need to re-register the runner (e.g. after a token reset or renaming):
|
||||
|
||||
1. Stop the container.
|
||||
2. Delete `config/runner.yaml`.
|
||||
3. Set the new `GITEA_RUNNER_TOKEN` in your environment.
|
||||
4. Start the container — it will register fresh on boot.
|
||||
|
||||
---
|
||||
|
||||
## Log access
|
||||
|
||||
```bash
|
||||
# Tail live logs
|
||||
docker logs -f gitea-runner
|
||||
|
||||
# Last 100 lines
|
||||
docker logs --tail 100 gitea-runner
|
||||
```
|
||||
@@ -0,0 +1,145 @@
|
||||
# Troubleshooting
|
||||
|
||||
Symptom-driven guide for common runner failures.
|
||||
|
||||
---
|
||||
|
||||
## Runner not appearing in Gitea admin panel
|
||||
|
||||
**Check:** Container logs for registration errors.
|
||||
|
||||
```bash
|
||||
docker logs gitea-runner 2>&1 | grep -E "BOOTSTRAP|FATAL|ERROR"
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
|
||||
| Cause | Fix |
|
||||
|---|---|
|
||||
| `GITEA_RUNNER_TOKEN` is invalid or expired | Generate a fresh token in Gitea admin → Runners |
|
||||
| `GITEA_INSTANCE_URL` wrong or unreachable | Verify URL is reachable from inside the container: `docker exec gitea-runner curl -I "$GITEA_INSTANCE_URL"` |
|
||||
| TLS cert error (self-signed Gitea) | Mount your CA cert to `/config/trusted-ca/` and restart |
|
||||
| Runner name already registered | Use a unique `GITEA_RUNNER_NAME` or deregister the old one |
|
||||
|
||||
---
|
||||
|
||||
## `node: not found` in job logs
|
||||
|
||||
The runner image bundles Node.js 20 LTS. If you see this error it usually means:
|
||||
|
||||
- The job is running inside a container image that does not have Node.js (e.g. `alpine`).
|
||||
- The `ubuntu-latest` label is mapped to a minimal image.
|
||||
|
||||
**Fix:** Ensure the label mapping uses a full Node image:
|
||||
|
||||
```
|
||||
ubuntu-latest:docker://node:20-bullseye
|
||||
```
|
||||
|
||||
Or use a host-mode label:
|
||||
|
||||
```
|
||||
ubuntu-latest:host
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker socket permission error
|
||||
|
||||
**Symptom:**
|
||||
|
||||
```
|
||||
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
|
||||
```
|
||||
|
||||
**Causes and fixes:**
|
||||
|
||||
| Cause | Fix |
|
||||
|---|---|
|
||||
| Socket not mounted | Add `-v /var/run/docker.sock:/var/run/docker.sock` |
|
||||
| Socket GID mismatch | Container runs as root in v1 — this should not occur. If using a non-root custom image, add the runner user to the `docker` group or set `--group-add $(stat -c '%g' /var/run/docker.sock)` |
|
||||
|
||||
---
|
||||
|
||||
## Workflow checkout failures
|
||||
|
||||
**Symptom:** `actions/checkout` fails with an auth or host error.
|
||||
|
||||
**Check:**
|
||||
1. Gitea URL is accessible from inside the container.
|
||||
2. If using self-signed TLS, the CA cert is trusted inside the container.
|
||||
|
||||
```bash
|
||||
docker exec gitea-runner curl -v https://git.example.com
|
||||
```
|
||||
|
||||
If you see certificate errors, add your CA cert to `/config/trusted-ca/` and restart.
|
||||
|
||||
---
|
||||
|
||||
## Registry login failures (`docker/login-action`)
|
||||
|
||||
**Symptom:** Login to `registry.alwisp.com` fails.
|
||||
|
||||
**Check:**
|
||||
1. Registry URL is correct.
|
||||
2. Credentials are valid.
|
||||
3. If using self-signed TLS on the registry, the CA cert needs to be trusted inside the container AND the Docker daemon config must also trust the registry.
|
||||
|
||||
For Docker daemon CA trust on the Unraid host:
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/docker/certs.d/registry.alwisp.com
|
||||
cp my-ca.crt /etc/docker/certs.d/registry.alwisp.com/ca.crt
|
||||
```
|
||||
|
||||
Then restart the Docker daemon and the runner container.
|
||||
|
||||
---
|
||||
|
||||
## Container job image issues
|
||||
|
||||
**Symptom:** Jobs that run inside container images fail to pull or execute.
|
||||
|
||||
**Check:**
|
||||
- The container image referenced in the label mapping is pullable from inside the runner container.
|
||||
- If using a self-hosted registry image, the runner container can authenticate to it.
|
||||
|
||||
---
|
||||
|
||||
## CA / certificate trust issues
|
||||
|
||||
**Symptom:** TLS errors when connecting to Gitea or the registry.
|
||||
|
||||
**Fix:**
|
||||
1. Copy your CA cert (`.crt` format, PEM encoded) to `config/trusted-ca/`.
|
||||
2. Restart the container — the entrypoint installs certs from that directory on startup.
|
||||
3. Verify: `docker exec gitea-runner curl -v https://git.example.com` should show cert chain OK.
|
||||
|
||||
---
|
||||
|
||||
## Unraid-specific: container exits immediately
|
||||
|
||||
**Check:** Container logs before exit:
|
||||
|
||||
```bash
|
||||
docker logs gitea-runner
|
||||
```
|
||||
|
||||
Look for `[FATAL]` lines. Common cause is a missing required env var (`GITEA_INSTANCE_URL`, `GITEA_RUNNER_TOKEN`, `GITEA_RUNNER_NAME`) when no config file exists.
|
||||
|
||||
**Fix:** Set required env vars in the Unraid container template.
|
||||
|
||||
---
|
||||
|
||||
## Startup validation failures
|
||||
|
||||
**Symptom:** Container exits with `[FATAL] Missing required binaries`.
|
||||
|
||||
This is an image build problem — the binary should always be present if the image was built correctly.
|
||||
|
||||
**Fix:** Rebuild the image from the `docker/runner/Dockerfile` and verify:
|
||||
|
||||
```bash
|
||||
./scripts/verify-runtime.sh custom-gitea-runner:latest
|
||||
```
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
# Unraid Deployment Guide
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This guide covers deploying the custom-gitea-runner as a Docker container on an Unraid host. The recommended approach is Docker Compose via Unraid's compose manager, or manual template entry via the Unraid Docker UI.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Unraid with Docker enabled.
|
||||
- Access to your Gitea instance admin panel to generate a runner token.
|
||||
- The runner image pushed to your registry (e.g. `registry.alwisp.com`), or built locally on the Unraid host.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Get a runner token
|
||||
|
||||
1. Log into your Gitea instance as an admin.
|
||||
2. Go to **Site Administration → Runners → Create new runner**.
|
||||
3. Copy the registration token. It will be used as `GITEA_RUNNER_TOKEN`.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Set up appdata directories
|
||||
|
||||
```bash
|
||||
mkdir -p /mnt/user/appdata/gitea-runner/config
|
||||
mkdir -p /mnt/user/appdata/gitea-runner/work
|
||||
```
|
||||
|
||||
If using custom CA certificates:
|
||||
|
||||
```bash
|
||||
mkdir -p /mnt/user/appdata/gitea-runner/config/trusted-ca
|
||||
# Copy your CA cert:
|
||||
cp my-ca.crt /mnt/user/appdata/gitea-runner/config/trusted-ca/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Docker UI container settings
|
||||
|
||||
In Unraid's Docker tab, click **Add Container** and fill in:
|
||||
|
||||
**Basic**
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Name | `gitea-runner` |
|
||||
| Repository | `registry.alwisp.com/custom-gitea-runner:v0.1.0` |
|
||||
| Network Type | `Bridge` |
|
||||
| Privileged | `No` |
|
||||
| Restart | `Unless Stopped` |
|
||||
|
||||
**Environment variables**
|
||||
|
||||
| Key | Value |
|
||||
|---|---|
|
||||
| `GITEA_INSTANCE_URL` | `https://git.example.com` |
|
||||
| `GITEA_RUNNER_TOKEN` | *(your registration token)* |
|
||||
| `GITEA_RUNNER_NAME` | `unraid-runner-01` |
|
||||
| `GITEA_RUNNER_LABELS` | `ubuntu-latest:docker://node:20-bullseye` |
|
||||
| `TZ` | `America/Chicago` |
|
||||
|
||||
**Volumes**
|
||||
|
||||
| Container path | Host path | Mode |
|
||||
|---|---|---|
|
||||
| `/config` | `/mnt/user/appdata/gitea-runner/config` | Read/Write |
|
||||
| `/work` | `/mnt/user/appdata/gitea-runner/work` | Read/Write |
|
||||
| `/var/run/docker.sock` | `/var/run/docker.sock` | Read/Write |
|
||||
|
||||
Click **Apply**.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Verify startup
|
||||
|
||||
View container logs in the Unraid Docker UI or via terminal:
|
||||
|
||||
```bash
|
||||
docker logs gitea-runner
|
||||
```
|
||||
|
||||
First boot should show `[BOOTSTRAP] Registration complete`.
|
||||
Subsequent starts show `[INFO] Existing config found`.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Verify in Gitea
|
||||
|
||||
Go to Gitea **Site Administration → Runners**. The runner should appear as **Online**.
|
||||
|
||||
---
|
||||
|
||||
## Update procedure
|
||||
|
||||
1. Pull the new image tag to the Unraid host (or use the Community Applications plugin if listed).
|
||||
2. In the Docker UI, edit the container and update the Repository to the new tag.
|
||||
3. Click **Apply** — Unraid stops and recreates the container from the new image.
|
||||
4. Check logs to confirm startup, then test a workflow.
|
||||
|
||||
## Rollback procedure
|
||||
|
||||
Repeat the update procedure but specify the previous known-good tag.
|
||||
|
||||
---
|
||||
|
||||
## Appdata backup
|
||||
|
||||
The `/config` volume (at `/mnt/user/appdata/gitea-runner/config/`) contains the runner registration config. Include it in Unraid's Appdata Backup plugin to preserve registration between host moves.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- The Docker socket mount (`/var/run/docker.sock`) is intentional and required for build/push workflows. On a single-tenant, self-owned Unraid host this is the standard approach.
|
||||
- The `/work` directory grows as jobs run. You can safely clear it when no jobs are active to reclaim space.
|
||||
+54
-44
@@ -27,7 +27,7 @@ The project is considered successful when it can:
|
||||
|
||||
## Phase 0 — Research and design
|
||||
|
||||
**Status:** Active
|
||||
**Status:** Complete
|
||||
|
||||
### Goals
|
||||
|
||||
@@ -40,13 +40,13 @@ The project is considered successful when it can:
|
||||
- [x] Write `AGENT.md`
|
||||
- [x] Write `README.md`
|
||||
- [x] Write `ROADMAP.md`
|
||||
- [ ] Create `CHANGELOG.md`
|
||||
- [ ] Create `.env.example`
|
||||
- [ ] Create ADR template under `docs/decisions/`
|
||||
- [ ] Write `docs/architecture.md`
|
||||
- [ ] Decide base image strategy
|
||||
- [ ] Decide socket-mount vs DinD position for v1
|
||||
- [ ] Decide registration mode defaults
|
||||
- [x] Create `CHANGELOG.md`
|
||||
- [x] Create `.env.example`
|
||||
- [x] Create ADR template under `docs/decisions/`
|
||||
- [x] Write `docs/architecture.md`
|
||||
- [x] Decide base image strategy
|
||||
- [x] Decide socket-mount vs DinD position for v1
|
||||
- [x] Decide registration mode defaults
|
||||
|
||||
### Exit criteria
|
||||
|
||||
@@ -59,7 +59,7 @@ The project is considered successful when it can:
|
||||
|
||||
## Phase 1 — Base image and bootstrap
|
||||
|
||||
**Status:** Planned
|
||||
**Status:** Complete
|
||||
|
||||
### Goals
|
||||
|
||||
@@ -69,24 +69,29 @@ The project is considered successful when it can:
|
||||
|
||||
### Tasks
|
||||
|
||||
- [ ] Create `docker/runner/Dockerfile`
|
||||
- [ ] Pin base image tag
|
||||
- [ ] Pin `act_runner` source/version
|
||||
- [ ] Install required dependencies at build time:
|
||||
- [ ] bash
|
||||
- [ ] curl
|
||||
- [ ] git
|
||||
- [ ] jq
|
||||
- [ ] nodejs
|
||||
- [ ] npm
|
||||
- [ ] docker CLI
|
||||
- [ ] ca-certificates
|
||||
- [ ] tini
|
||||
- [ ] tzdata
|
||||
- [ ] Create `docker/runner/docker-entrypoint.sh`
|
||||
- [ ] Create `docker/runner/healthcheck.sh`
|
||||
- [ ] Add image metadata labels
|
||||
- [ ] Verify startup prints tool versions
|
||||
- [x] Create `docker/runner/Dockerfile`
|
||||
- [x] Pin base image tag
|
||||
- [x] Pin `act_runner` source/version
|
||||
- [x] Install required dependencies at build time:
|
||||
- [x] bash
|
||||
- [x] curl
|
||||
- [x] git
|
||||
- [x] jq
|
||||
- [x] nodejs
|
||||
- [x] npm
|
||||
- [x] docker CLI
|
||||
- [x] ca-certificates
|
||||
- [x] tini
|
||||
- [x] tzdata
|
||||
- [x] Create `docker/runner/docker-entrypoint.sh`
|
||||
- [x] Create `docker/runner/healthcheck.sh`
|
||||
- [x] Add image metadata labels
|
||||
- [x] Verify startup prints tool versions
|
||||
|
||||
### Notes
|
||||
|
||||
- First successful build: `custom-gitea-runner:v0.1.0` (act_runner v0.2.11, node v20.20.2, npm 10.8.2, git 2.34.1, docker CLI 29.6.1).
|
||||
- Runtime smoke test passes 17/17 via `./scripts/verify-runtime.sh`.
|
||||
|
||||
### Exit criteria
|
||||
|
||||
@@ -99,7 +104,7 @@ The project is considered successful when it can:
|
||||
|
||||
## Phase 2 — Registration and config flows
|
||||
|
||||
**Status:** Planned
|
||||
**Status:** Complete
|
||||
|
||||
### Goals
|
||||
|
||||
@@ -109,15 +114,15 @@ The project is considered successful when it can:
|
||||
|
||||
### Tasks
|
||||
|
||||
- [ ] Create `config/runner.example.yaml`
|
||||
- [ ] Create `config/labels.example.env`
|
||||
- [ ] Create `scripts/register-runner.sh`
|
||||
- [ ] Create `scripts/deregister-runner.sh`
|
||||
- [ ] Add config path validation
|
||||
- [ ] Add first-boot registration flow
|
||||
- [ ] Add re-run safe bootstrap logic
|
||||
- [ ] Document required environment variables
|
||||
- [ ] Validate restart behavior with existing config
|
||||
- [x] Create `config/runner.example.yaml`
|
||||
- [x] Create `config/labels.example.env`
|
||||
- [x] Create `scripts/register-runner.sh`
|
||||
- [x] Create `scripts/deregister-runner.sh`
|
||||
- [x] Add config path validation
|
||||
- [x] Add first-boot registration flow
|
||||
- [x] Add re-run safe bootstrap logic
|
||||
- [x] Document required environment variables
|
||||
- [x] Validate restart behavior with existing config
|
||||
|
||||
### Exit criteria
|
||||
|
||||
@@ -125,6 +130,11 @@ The project is considered successful when it can:
|
||||
- Existing config is reused without duplicate registration.
|
||||
- Missing configuration fails fast with clear logs.
|
||||
|
||||
### Notes
|
||||
|
||||
- Validated live against `git.alwisp.com` (Gitea 1.26.4): runner `forgerunner-01` registered as instance-level runner (id 5), came online and healthy, and a container restart reused the existing config with no duplicate registration.
|
||||
- Registration token obtained via `POST /api/v1/admin/actions/runners/registration-token`; deregistration via `DELETE /api/v1/admin/actions/runners/{id}` (act_runner has no unregister command).
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Docker workflow support
|
||||
@@ -167,13 +177,13 @@ The project is considered successful when it can:
|
||||
|
||||
### Tasks
|
||||
|
||||
- [ ] Create `docker/runner/smoke-test.sh`
|
||||
- [ ] Create `scripts/verify-runtime.sh`
|
||||
- [ ] Create `scripts/test-local-runner.sh`
|
||||
- [ ] Add `test/sample-workflows/checkout.yml`
|
||||
- [ ] Add `test/sample-workflows/node-action.yml`
|
||||
- [ ] Add `test/sample-workflows/docker-login.yml`
|
||||
- [ ] Add `test/sample-workflows/docker-build-push.yml`
|
||||
- [x] Create `docker/runner/smoke-test.sh`
|
||||
- [x] Create `scripts/verify-runtime.sh`
|
||||
- [x] Create `scripts/test-local-runner.sh`
|
||||
- [x] Add `test/sample-workflows/checkout.yml`
|
||||
- [x] Add `test/sample-workflows/node-action.yml`
|
||||
- [x] Add `test/sample-workflows/docker-login.yml`
|
||||
- [x] Add `test/sample-workflows/docker-build-push.yml`
|
||||
- [ ] Add regression test cases for:
|
||||
- [ ] missing `node`
|
||||
- [ ] broken Docker socket access
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-image.sh — build the custom-gitea-runner Docker image locally
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE_NAME="${IMAGE_NAME:-custom-gitea-runner}"
|
||||
VERSION="${VERSION:-dev}"
|
||||
VCS_REF="$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')"
|
||||
BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
echo "Building ${IMAGE_NAME}:${VERSION} ..."
|
||||
echo " VCS_REF : ${VCS_REF}"
|
||||
echo " BUILD_DATE : ${BUILD_DATE}"
|
||||
|
||||
docker build \
|
||||
--build-arg "VERSION=${VERSION}" \
|
||||
--build-arg "VCS_REF=${VCS_REF}" \
|
||||
--build-arg "BUILD_DATE=${BUILD_DATE}" \
|
||||
--tag "${IMAGE_NAME}:${VERSION}" \
|
||||
--tag "${IMAGE_NAME}:latest" \
|
||||
--file docker/runner/Dockerfile \
|
||||
.
|
||||
|
||||
echo "Build complete: ${IMAGE_NAME}:${VERSION}"
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# deregister-runner.sh — remove a runner registration from Gitea via the API.
|
||||
#
|
||||
# act_runner has no "unregister" command, so deregistration is done through the
|
||||
# Gitea REST API. This requires an *API* token (not a registration token) with
|
||||
# admin rights, plus the runner id or name. Run this before retiring or renaming
|
||||
# a runner to keep the Gitea UI clean, then delete the local config to start fresh.
|
||||
#
|
||||
# Usage:
|
||||
# GITEA_INSTANCE_URL=https://git.example.com \
|
||||
# GITEA_API_TOKEN=<admin-api-token> \
|
||||
# ./scripts/deregister-runner.sh <runner-name|runner-id>
|
||||
set -euo pipefail
|
||||
|
||||
: "${GITEA_INSTANCE_URL:?GITEA_INSTANCE_URL must be set (e.g. https://git.example.com)}"
|
||||
: "${GITEA_API_TOKEN:?GITEA_API_TOKEN must be set (an admin API token, not a registration token)}"
|
||||
|
||||
TARGET="${1:-}"
|
||||
if [ -z "${TARGET}" ]; then
|
||||
echo "[FATAL] Provide the runner name or id as the first argument." >&2
|
||||
echo "Usage: $0 <runner-name|runner-id>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
API="${GITEA_INSTANCE_URL%/}/api/v1"
|
||||
AUTH="Authorization: token ${GITEA_API_TOKEN}"
|
||||
|
||||
# Resolve a runner name to an id if a non-numeric target was given.
|
||||
if [[ "${TARGET}" =~ ^[0-9]+$ ]]; then
|
||||
RUNNER_ID="${TARGET}"
|
||||
else
|
||||
echo "Resolving runner id for name '${TARGET}' ..."
|
||||
RUNNER_ID="$(curl -fsS -H "${AUTH}" "${API}/admin/actions/runners" \
|
||||
| jq -r --arg n "${TARGET}" '.runners[] | select(.name==$n) | .id' | head -1)"
|
||||
if [ -z "${RUNNER_ID}" ]; then
|
||||
echo "[FATAL] No runner found with name '${TARGET}'." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Deregistering runner id ${RUNNER_ID} from ${GITEA_INSTANCE_URL} ..."
|
||||
http_code="$(curl -fsS -o /dev/null -w '%{http_code}' \
|
||||
-X DELETE -H "${AUTH}" "${API}/admin/actions/runners/${RUNNER_ID}")"
|
||||
|
||||
if [ "${http_code}" = "204" ]; then
|
||||
echo "Deregistration complete (runner id ${RUNNER_ID} removed)."
|
||||
echo "Remove the local config to start fresh: rm config/runner.yaml"
|
||||
else
|
||||
echo "[FATAL] Unexpected HTTP status ${http_code} from delete request." >&2
|
||||
exit 1
|
||||
fi
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# publish-image.sh — tag and push the runner image to a registry
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE_NAME="${IMAGE_NAME:-custom-gitea-runner}"
|
||||
REGISTRY="${REGISTRY:-registry.alwisp.com}"
|
||||
VERSION="${VERSION:-}"
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "[ERROR] VERSION must be set (e.g. VERSION=v0.1.0 ./scripts/publish-image.sh)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOCAL_TAG="${IMAGE_NAME}:${VERSION}"
|
||||
REMOTE_TAG="${REGISTRY}/${IMAGE_NAME}:${VERSION}"
|
||||
REMOTE_LATEST="${REGISTRY}/${IMAGE_NAME}:latest"
|
||||
|
||||
echo "Tagging ${LOCAL_TAG} as ${REMOTE_TAG} ..."
|
||||
docker tag "${LOCAL_TAG}" "${REMOTE_TAG}"
|
||||
docker tag "${LOCAL_TAG}" "${REMOTE_LATEST}"
|
||||
|
||||
echo "Pushing ${REMOTE_TAG} ..."
|
||||
docker push "${REMOTE_TAG}"
|
||||
docker push "${REMOTE_LATEST}"
|
||||
|
||||
echo "Published: ${REMOTE_TAG}"
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# register-runner.sh — standalone registration helper (run outside container)
|
||||
# Useful for pre-generating config before first container start.
|
||||
set -euo pipefail
|
||||
|
||||
: "${GITEA_INSTANCE_URL:?GITEA_INSTANCE_URL must be set}"
|
||||
: "${GITEA_RUNNER_TOKEN:?GITEA_RUNNER_TOKEN must be set}"
|
||||
: "${GITEA_RUNNER_NAME:?GITEA_RUNNER_NAME must be set}"
|
||||
|
||||
RUNNER_CONFIG_PATH="${RUNNER_CONFIG_PATH:-./config/runner.yaml}"
|
||||
GITEA_RUNNER_LABELS="${GITEA_RUNNER_LABELS:-ubuntu-latest:docker://node:20-bullseye}"
|
||||
|
||||
echo "Registering runner: ${GITEA_RUNNER_NAME}"
|
||||
echo " Instance : ${GITEA_INSTANCE_URL}"
|
||||
echo " Config : ${RUNNER_CONFIG_PATH}"
|
||||
echo " Labels : ${GITEA_RUNNER_LABELS}"
|
||||
|
||||
mkdir -p "$(dirname "${RUNNER_CONFIG_PATH}")"
|
||||
|
||||
docker run --rm \
|
||||
-v "$(pwd)/config:/config" \
|
||||
-e GITEA_INSTANCE_URL="${GITEA_INSTANCE_URL}" \
|
||||
-e GITEA_RUNNER_TOKEN="${GITEA_RUNNER_TOKEN}" \
|
||||
-e GITEA_RUNNER_NAME="${GITEA_RUNNER_NAME}" \
|
||||
-e GITEA_RUNNER_LABELS="${GITEA_RUNNER_LABELS}" \
|
||||
-e RUNNER_CONFIG_PATH="/config/runner.yaml" \
|
||||
custom-gitea-runner:latest \
|
||||
act_runner register \
|
||||
--config /config/runner.yaml \
|
||||
--instance "${GITEA_INSTANCE_URL}" \
|
||||
--token "${GITEA_RUNNER_TOKEN}" \
|
||||
--name "${GITEA_RUNNER_NAME}" \
|
||||
--labels "${GITEA_RUNNER_LABELS}" \
|
||||
--no-interactive
|
||||
|
||||
echo "Registration complete. Config written to ${RUNNER_CONFIG_PATH}."
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# test-local-runner.sh — start a short-lived runner for local workflow testing
|
||||
# Requires a running Gitea instance and a valid registration token.
|
||||
set -euo pipefail
|
||||
|
||||
: "${GITEA_INSTANCE_URL:?GITEA_INSTANCE_URL must be set}"
|
||||
: "${GITEA_RUNNER_TOKEN:?GITEA_RUNNER_TOKEN must be set}"
|
||||
: "${GITEA_RUNNER_NAME:?GITEA_RUNNER_NAME must be set}"
|
||||
|
||||
IMAGE="${IMAGE:-custom-gitea-runner:latest}"
|
||||
CONFIG_DIR="$(mktemp -d /tmp/runner-test-config.XXXXXX)"
|
||||
WORK_DIR="$(mktemp -d /tmp/runner-test-work.XXXXXX)"
|
||||
|
||||
cleanup() {
|
||||
echo "Cleaning up temp dirs ..."
|
||||
rm -rf "$CONFIG_DIR" "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "Starting local test runner ..."
|
||||
echo " Image : ${IMAGE}"
|
||||
echo " Config : ${CONFIG_DIR}"
|
||||
echo " Work : ${WORK_DIR}"
|
||||
|
||||
docker run --rm \
|
||||
-v "${CONFIG_DIR}:/config" \
|
||||
-v "${WORK_DIR}:/work" \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e GITEA_INSTANCE_URL="${GITEA_INSTANCE_URL}" \
|
||||
-e GITEA_RUNNER_TOKEN="${GITEA_RUNNER_TOKEN}" \
|
||||
-e GITEA_RUNNER_NAME="${GITEA_RUNNER_NAME}" \
|
||||
-e GITEA_RUNNER_LABELS="${GITEA_RUNNER_LABELS:-ubuntu-latest:docker://node:20-bullseye}" \
|
||||
"${IMAGE}"
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# verify-runtime.sh — validate the runner image tool chain without starting the daemon
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE="${1:-custom-gitea-runner:latest}"
|
||||
|
||||
echo "Running smoke test against image: ${IMAGE}"
|
||||
|
||||
docker run --rm \
|
||||
--entrypoint /usr/local/bin/smoke-test.sh \
|
||||
"${IMAGE}"
|
||||
Vendored
@@ -0,0 +1,17 @@
|
||||
name: Smoke — Checkout
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
checkout:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Verify working directory
|
||||
run: |
|
||||
echo "Repo: $(pwd)"
|
||||
ls -la
|
||||
git log --oneline -5
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Smoke — Docker Build and Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: registry.alwisp.com
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build and Push
|
||||
run: |
|
||||
docker build \
|
||||
-t registry.alwisp.com/${{ gitea.repository_owner }}/${{ gitea.repository }}:latest \
|
||||
-t registry.alwisp.com/${{ gitea.repository_owner }}/${{ gitea.repository }}:${{ gitea.sha }} \
|
||||
-f docker/runner/Dockerfile \
|
||||
.
|
||||
|
||||
docker push registry.alwisp.com/${{ gitea.repository_owner }}/${{ gitea.repository }}:latest
|
||||
docker push registry.alwisp.com/${{ gitea.repository_owner }}/${{ gitea.repository }}:${{ gitea.sha }}
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Smoke — Docker Login
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
login:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Log in to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: registry.alwisp.com
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Confirm Docker login
|
||||
run: docker info
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Smoke — Node Action
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
node:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Print Node version
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Run inline Node script
|
||||
run: node -e "console.log('Node.js works:', process.version)"
|
||||
Reference in New Issue
Block a user