commit ddb2f53a5d6185f63f61568758af34a9ebfa700a Author: opencode-agent Date: Thu Aug 6 16:25:31 2026 +0000 feat: initial forgejo + runner-image reproducible setup diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..80edb99 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Forgejo instance URL (no trailing slash) +FORGEJO_URL=https://git.slaid098.dev + +# Forgejo admin token (API + git push + registry auth). Needs package:write scope. +FORGEJO_TOKEN=replace-me + +# act_runner registration token (from `forgejo actions generate-runner-token`). +# Only needed at first registration; afterwards the .runner file holds the credentials. +GITEA_RUNNER_REGISTRATION_TOKEN=replace-me \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1833367 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Reproducible Forgejo + CI runner setup + +This repository contains the declarative configuration for the Forgejo instance +and its self-hosted CI runner at `git.slaid098.dev`. + +## What's here + +- `compose/docker-compose.yml` — Forgejo + act_runner stack (env-var references; real secrets live in the deployment's `.env`). +- `compose/runner-config.yaml` — act_runner config (`capacity: 4`, cache enabled). +- `runner-image/Dockerfile` — custom job image bundling Python 3.13, uv, Node 22, ripgrep, git, docker CLI. Replaces the flaky `data.forgejo.org` action downloads (`setup-uv`, `setup-python`, `setup-node`). +- `.env.example` — placeholder env vars; copy to `.env` and fill in on the target host. +- `setup.sh` — one-command reproducible deploy on a fresh Ubuntu server. + +## Why a custom runner image + +`data.forgejo.org` (the action mirror act_runner uses) intermittently returns 404 +for `astral-sh/setup-uv@v3` and similar actions, which surfaces as +`Error: Cannot find module '.../dist/setup/index.js'`. Bundling the tools in the +image and dropping those `uses:` steps from CI workflows eliminates the flakiness. + +## Quick start + +```bash +# On a fresh Ubuntu 22.04/24.04 server with docker + docker compose + git: +git clone https://git.slaid098.dev/slaid098/forgejo-infra.git +cd forgejo-infra +cp .env.example .env # then edit .env with real tokens +./setup.sh +``` + +After Forgejo is up, register the runner (token from +`docker exec forgejo forgejo actions generate-runner-token`): + +```bash +cd /root/dockers/forgejo +docker compose run --rm runner act_runner register \ + --instance http://forgejo:3000 --token --name linux-2 \ + --labels ubuntu-latest:docker://git.slaid098.dev/slaid098/runner:latest \ + --no-interactive +``` \ No newline at end of file diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml new file mode 100644 index 0000000..dd2455b --- /dev/null +++ b/compose/docker-compose.yml @@ -0,0 +1,62 @@ +services: + forgejo: + image: codeberg.org/forgejo/forgejo:10 + container_name: forgejo + restart: unless-stopped + environment: + - FORGEJO__server__DOMAIN=git.slaid098.dev + - FORGEJO__server__ROOT_URL=https://git.slaid098.dev/ + - FORGEJO__server__SSH_DOMAIN=git.slaid098.dev + - FORGEJO__server__SSH_PORT=2222 + - FORGEJO__service__DISABLE_REGISTRATION=true + - FORGEJO__service__REQUIRE_SIGNIN_VIEW=false + - FORGEJO__actions__ENABLED=true + - FORGEJO__security__INSTALL_LOCK=true + - USER_UID=1000 + - USER_GID=1000 + volumes: + - ./app_data/forgejo:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "127.0.0.1:3000:3000" + - "2222:22" + networks: + - default + - npm_network + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + runner: + image: gitea/act_runner:latest + container_name: forgejo-runner + restart: unless-stopped + depends_on: + - forgejo + volumes: + - ./app_data/runner:/data + - /var/run/docker.sock:/var/run/docker.sock + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + environment: + - GITEA_INSTANCE_URL=http://forgejo:3000 + - GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_REGISTRATION_TOKEN} + - GITEA_RUNNER_NAME=linux-2 + - GITEA_RUNNER_LABELS=ubuntu-latest:docker://git.slaid098.dev/slaid098/runner:latest,ubuntu-24.04:docker://git.slaid098.dev/slaid098/runner:latest,ubuntu-22.04:docker://git.slaid098.dev/slaid098/runner:latest + - CONFIG_FILE=/data/config.yaml + networks: + - default + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +networks: + default: + npm_network: + external: true + name: digital_factory_app_network \ No newline at end of file diff --git a/compose/runner-config.yaml b/compose/runner-config.yaml new file mode 100644 index 0000000..53b63e6 --- /dev/null +++ b/compose/runner-config.yaml @@ -0,0 +1,147 @@ +# Example configuration file, it's safe to copy this as the default config file without any modification. + +# You don't have to copy this file to your instance, +# just run `./act_runner generate-config > config.yaml` to generate a config file. + +log: + # The level of logging, can be trace, debug, info, warn, error, fatal + level: info + +runner: + # Where to store the registration result. + file: /data/.runner + # Execute how many tasks concurrently at the same time. + capacity: 4 + # Extra environment variables to run jobs. + envs: {} + # Extra environment variables to run jobs from a file. + # It will be ignored if it's empty or the file doesn't exist. + env_file: .env + # The timeout for a job to be finished. + # Please note that the Gitea instance also has a timeout (3h by default) for the job. + # So the job could be stopped by the Gitea instance if its timeout is shorter than this. + timeout: 3h + # The timeout for the runner to wait for running jobs to finish when shutting down. + # Any running jobs that haven't finished after this timeout will be cancelled. + shutdown_timeout: 0s + # Whether skip verifying the TLS certificate of the Gitea instance. + insecure: false + # The timeout for fetching the job from the Gitea instance. + fetch_timeout: 5s + # The interval for fetching the job from the Gitea instance. + fetch_interval: 2s + # The maximum interval for fetching the job from the Gitea instance. + # The runner uses exponential backoff when idle, increasing the interval up to this maximum. + # Set to 0 or same as fetch_interval to disable backoff. + fetch_interval_max: 5s + # The base interval for periodic log flush to the Gitea instance. + # Logs may be sent earlier if the buffer reaches log_report_batch_size + # or if log_report_max_latency expires after the first buffered row. + log_report_interval: 5s + # The maximum time a log row can wait before being sent. + # This ensures even a single log line appears on the frontend within this duration. + # Must be less than log_report_interval to have any effect. + log_report_max_latency: 3s + # Flush logs immediately when the buffer reaches this many rows. + # This ensures bursty output (e.g., npm install) is delivered promptly. + log_report_batch_size: 100 + # The interval for reporting task state (step status, timing) to the Gitea instance. + # State is also reported immediately on step transitions (start/stop). + state_report_interval: 5s + # The github_mirror of a runner is used to specify the mirror address of the github that pulls the action repository. + # It works when something like `uses: actions/checkout@v4` is used and DEFAULT_ACTIONS_URL is set to github, + # and github_mirror is not empty. In this case, + # it replaces https://github.com with the value here, which is useful for some special network environments. + github_mirror: '' + # The labels of a runner are used to determine which jobs the runner can run, and how to run them. + # Like: "macos-arm64:host" or "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest" + # Find more images provided by Gitea at https://gitea.com/gitea/runner-images . + # If it's empty when registering, it will ask for inputting labels. + # If it's empty when execute `daemon`, will use labels in `.runner` file. + labels: + - "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest" + - "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04" + - "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04" + +cache: + # Enable cache server to use actions/cache. + enabled: true + # The directory to store the cache data. + # If it's empty, the cache data will be stored in $HOME/.cache/actcache. + dir: "" + # The host of the cache server. + # It's not for the address to listen, but the address to connect from job containers. + # So 0.0.0.0 is a bad choice, leave it empty to detect automatically. + host: "" + # The port of the cache server. + # 0 means to use a random available port. + port: 0 + # The external cache server URL. Valid only when enable is true. + # If it's specified, act_runner will use this URL as the ACTIONS_CACHE_URL rather than start a server by itself. + # The URL should generally end with "/". + # Requires external_secret below to be set to the same value on both this runner and the cache-server. + external_server: "" + # Shared secret between this runner and the external `act_runner cache-server`. Required when external_server + # (or `act_runner cache-server`) is in use: the runner pre-registers each job's ACTIONS_RUNTIME_TOKEN with the + # cache-server, and the cache-server enforces bearer auth + per-repo cache isolation. + external_secret: "" + +container: + # Specifies the network to which the container will connect. + # Could be host, bridge or the name of a custom network. + # If it's empty, act_runner will create a network automatically. + network: "forgejo_default" + # Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker). + privileged: false + # Any other options to be used when the container is started (e.g., --add-host=my.gitea.url:host-gateway). + options: + # The parent directory of a job's working directory. + # NOTE: There is no need to add the first '/' of the path as act_runner will add it automatically. + # If the path starts with '/', the '/' will be trimmed. + # For example, if the parent directory is /path/to/my/dir, workdir_parent should be path/to/my/dir + # If it's empty, /workspace will be used. + workdir_parent: + # Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob + # You can specify multiple volumes. If the sequence is empty, no volumes can be mounted. + # For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to: + # valid_volumes: + # - data + # - /src/*.json + # If you want to allow any volume, please use the following configuration: + # valid_volumes: + # - '**' + valid_volumes: + - "**" + # Overrides the docker client host with the specified one. + # If it's empty, act_runner will find an available docker host automatically. + # If it's "-", act_runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers. + # If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work. + docker_host: "" + # Pull docker image(s) even if already present + force_pull: true + # Rebuild docker image(s) even if already present + force_rebuild: false + # Always require a reachable docker daemon, even if not required by act_runner + require_docker: false + # Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or act_runner + docker_timeout: 0s + # Bind the workspace to the host filesystem instead of using Docker volumes. + # This is required for Docker-in-Docker (DinD) setups when jobs use docker compose + # with bind mounts (e.g., ".:/app"), as volume-based workspaces are not accessible + # from the DinD daemon's filesystem. When enabled, ensure the workspace parent + # directory is also mounted into the runner container and listed in valid_volumes. + bind_workdir: false + +host: + # The parent directory of a job's working directory. + # If it's empty, $HOME/.cache/act/ will be used. + workdir_parent: + +metrics: + # Enable the Prometheus metrics endpoint. + # When enabled, metrics are served at http:///metrics and a liveness check at /healthz. + enabled: false + # The address for the metrics HTTP server to listen on. + # Defaults to localhost only. Set to ":9101" to allow external access, + # but ensure the port is firewall-protected as there is no authentication. + addr: "127.0.0.1:9101" diff --git a/runner-image/Dockerfile b/runner-image/Dockerfile new file mode 100644 index 0000000..b423dfd --- /dev/null +++ b/runner-image/Dockerfile @@ -0,0 +1,38 @@ +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive +ENV HOME=/root + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl git ripgrep build-essential gnupg2 lsb-release \ + software-properties-common apt-transport-https \ + && rm -rf /var/lib/apt/lists/* + +RUN add-apt-repository -y ppa:deadsnakes/ppa \ + && apt-get update \ + && apt-get install -y --no-install-recommends python3.13 python3.13-venv python3.13-dev \ + && rm -rf /var/lib/apt/lists/* \ + && update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.13 1 \ + && python3 --version + +RUN curl -LsSf https://astral.sh/uv/install.sh | sh \ + && mv /root/.local/bin/uv /usr/local/bin/uv \ + && mv /root/.local/bin/uvx /usr/local/bin/uvx \ + && uv --version + +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* \ + && node --version && npm --version + +RUN install -m 0755 -d /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc \ + && chmod a+r /etc/apt/keyrings/docker.asc \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] 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/* \ + && docker --version + +WORKDIR /root +CMD ["sleep", "infinity"] \ No newline at end of file diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..5742ac7 --- /dev/null +++ b/setup.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Reproducible Forgejo + CI runner setup. +# Run on a fresh Ubuntu 22.04/24.04 server with root. +# Requires: docker, docker compose, git already installed. + +REPO_ORIGIN="https://git.slaid098.dev/slaid098/forgejo-infra.git" +DEPLOY_DIR="/root/dockers/forgejo" + +echo "==> Cloning forgejo-infra config..." +git clone "$REPO_ORIGIN" /tmp/forgejo-infra-clone + +echo "==> Creating $DEPLOY_DIR..." +mkdir -p "$DEPLOY_DIR/app_data/forgejo" "$DEPLOY_DIR/app_data/runner" +cp /tmp/forgejo-infra-clone/compose/docker-compose.yml "$DEPLOY_DIR/docker-compose.yml" +cp /tmp/forgejo-infra-clone/compose/runner-config.yaml "$DEPLOY_DIR/app_data/runner/config.yaml" +cp -r /tmp/forgejo-infra-clone/runner-image "$DEPLOY_DIR/runner-image" + +echo "==> Building runner image..." +cd "$DEPLOY_DIR/runner-image" +docker build -t git.slaid098.dev/slaid098/runner:latest . + +echo "==> Pushing runner image to Forgejo registry..." +# Requires: docker login git.slaid098.dev done beforehand (with a token that has package:write scope) +docker push git.slaid098.dev/slaid098/runner:latest + +echo "==> Starting Forgejo + runner..." +cd "$DEPLOY_DIR" +docker compose up -d + +echo "==> Done. Forgejo will be at http://localhost:3000 — configure NPM/reverse proxy + DNS separately." +echo " Register the runner: docker compose run --rm runner act_runner register --instance http://forgejo:3000 --token --name linux-2 --labels ubuntu-latest:docker://git.slaid098.dev/slaid098/runner:latest --no-interactive" \ No newline at end of file