From f82543f518c6c092f6bb07bb1ff1bad2a4584c67 Mon Sep 17 00:00:00 2001 From: Worph Date: Sun, 4 Oct 2026 18:13:11 +0000 Subject: [PATCH] IPCR Forge v2: Radicle-only with rad-actions Snapshot of the Radicle-only forge (cib + act, DinD staging registry) before the Gitea-centric v3 POC replaces it. Also carries the gateway's IMPORT_PUBLISHER mode, multi-segment IPNS paths and IPCR 1.2.0. --- Apps/IPCR-Forge/docker-compose.yml | 457 +++++++----------- Apps/IPCR-Forge/rationale.md | 168 +++---- .../seed/gitea/runner-config/act_runner-run | 32 -- .../seed/gitea/runner-config/config.yaml | 40 -- Apps/IPCR-Forge/seed/site/index.html | 77 +-- Apps/IPCR-Forge/seed/web/default.conf | 12 +- docs/forge.md | 185 +++++++ 7 files changed, 509 insertions(+), 462 deletions(-) delete mode 100644 Apps/IPCR-Forge/seed/gitea/runner-config/act_runner-run delete mode 100644 Apps/IPCR-Forge/seed/gitea/runner-config/config.yaml create mode 100644 docs/forge.md diff --git a/Apps/IPCR-Forge/docker-compose.yml b/Apps/IPCR-Forge/docker-compose.yml index 50152ca..675894e 100644 --- a/Apps/IPCR-Forge/docker-compose.yml +++ b/Apps/IPCR-Forge/docker-compose.yml @@ -4,35 +4,41 @@ name: ipcr-forge # # git push rad main v1.2.3 (developer) # → radicle-node / radicle-api source of truth, peer-to-peer -# → radicle-gitea-bridge copies repos that commit .gitea/workflows/ -# → gitea + gitea-runner GitHub-style Actions build the image -# → gitea registry staging: gitea-/gitea_admin/:1.2.3 -# → ipcr-gateway follows Gitea's registry imports every tag into IPFS, publishes IPNS +# → rad-actions (cib + act) the node's own CI: GitHub-style workflows, run on the +# commit straight from the node's storage +# → rad-actions-registry staging: localhost:5000/:1.2.3 (internal only) +# → ipcr-gateway follows that registry imports every tag into IPFS, publishes IPNS # → ipcr-kubo / ipcr-registry # docker pull ipcr.localhost:4767/ipns/:1.2.3 (anyone) # +# Radicle is the only place anything originates. There is no mirror, no second forge and no +# bridge: the CI broker reads the node's events and its storage, and writes each result back +# into the repository as a job COB signed by the node. The CI's pages (cib's reports and the run +# logs) are static files the landing page serves under /ci/. +# # The tile opens ipcr-forge-: a static landing page (nginx) that explains the pipeline, -# links Radicle and Gitea, and lists every published image with the exact `image:` line to use -# (IPCR's published.json, served as /images.json). is the IPNS key IPCR minted for the +# links Radicle and the CI runs, and lists every published image with the exact `image:` line to +# use (IPCR's published.json, served as /images.json). is the IPNS key IPCR minted for the # repository (k51…), not the repository's name, which is why the page lists it. # -# Every service is copied verbatim from its standalone listing (Apps/Radicle, Apps/Gitea, -# Apps/Radicle-Gitea-Bridge, Apps/IPCR), so each keeps its own comments and rationale.md. -# Only the data folders move, one subfolder per part: $AppID/{radicle,gitea,bridge,ipcr}/. +# Radicle and IPCR services are copied verbatim from their standalone listings (Apps/Radicle, +# Apps/IPCR), so each keeps its own comments and rationale.md. Only the data folders move, one +# subfolder per part: $AppID/{radicle,ci,ipcr}/. # -# Replaces those four apps; it cannot sit beside them. It uses the same container names, -# domains (radicle-, radicle-api-, gitea-) and host ports (8776, 4768, 127.0.0.1:4767). +# Replaces the Radicle and IPCR apps; it cannot sit beside them. It uses the same container +# names, domains (radicle-, radicle-api-) and host ports (8776, 4768, 127.0.0.1:4767). # -# The handoff from Gitea's registry to IPFS is a pull, run by IPCR: the runner's jobs run in a -# nested Docker that cannot reach ipcr.localhost (the *host's* loopback), so CI never pushes to -# IPCR. Instead ipcr-gateway polls gitea:3000 over gitea-internal, with a read-only token the -# bridge-credentials step mints. Gitea does not know IPCR exists. +# The handoff from CI to IPFS is a pull, run by IPCR: jobs run in the CI's own Docker daemon, +# which cannot reach ipcr.localhost (the *host's* loopback). So a workflow pushes to the staging +# registry, which shares that daemon's network namespace and is therefore `localhost:5000` to +# every job, and ipcr-gateway polls it over ci-internal. services: # ═══ Landing page — the tile's web UI ══════════════════════════════════════════ - # Static: the page, its icon, and IPCR's list of published images (read-only). It shows only - # links, documentation and image names that are public on IPFS anyway, so it is not behind - # the SSO gate; Gitea keeps its own login, Radicle is public by design. + # Static: the page, its icon, IPCR's list of published images and the CI's pages (read-only). + # It shows only links, documentation, image names that are public on IPFS anyway, and build + # logs of public repositories (the adapter does not run private ones), so it is not behind + # the SSO gate; Radicle is public by design. ipcr-forge: image: nginxinc/nginx-unprivileged:1.29.3-alpine container_name: ipcr-forge @@ -56,6 +62,8 @@ services: - ${DATA_ROOT:-/DATA}/AppData/$AppID/web/default.conf:/etc/nginx/conf.d/default.conf:ro # published.json → /images.json. Read-only; the location in default.conf serves that file only. - ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/state:/srv/ipcr-state:ro + # The CI broker's report pages and the run logs → /ci/. Read-only. + - ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/html:/srv/ci:ro healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8080/"] interval: 30s @@ -202,143 +210,104 @@ services: networks: - radicle-internal - # ═══ Gitea — CI and the staging registry ═══════════════════════════════════════ - # Verbatim from Apps/Gitea; only its data folder moves to $AppID/gitea/. + # ═══ CI — Radicle CI broker + act ═════════════════════════════════════════════════ - # Gitea itself — git hosting, the Actions controller, and the OCI package - # registry, all in one process. SQLite: a personal forge never has the write - # concurrency that would justify a second container for Postgres. - gitea: - image: gitea/gitea:1.27.3-rootless - container_name: gitea + # cib (radicle-ci-broker) follows the node's events over its control socket, keeps the ones its + # filters allow (changes signed by a delegate, in a repository with a workflow folder — see + # ci/ci-broker.yaml in the IPCR repository), and runs the rad-actions adapter for each. The + # adapter checks the commit out of the node's storage and runs the matching GitHub-style + # workflows with act, against the Docker daemon below. cib writes HTML reports, and the result + # of every run back into the repository as a job COB signed by this node. + rad-actions: + image: ghcr.io/yundera/rad-actions:0.1.0 + container_name: rad-actions user: $PUID:$PGID + # Reaps act's and git's children: cib is not an init. + init: true + cap_drop: [ALL] + security_opt: ["no-new-privileges:true"] restart: unless-stopped - cpu_shares: 70 + cpu_shares: 20 deploy: resources: limits: - memory: 1G + memory: 512M environment: TZ: $TZ - # Every URL Gitea hands out — clone URLs, the registry host, and the - # server_url the CI runner passes to jobs — is derived from ROOT_URL, so - # it has to be the address that works from outside as well as inside. - GITEA__server__ROOT_URL: https://gitea-${APP_DOMAIN}/ - GITEA__server__DOMAIN: gitea-${APP_DOMAIN} - GITEA__server__HTTP_PORT: "3000" - GITEA__server__PROTOCOL: http - # The address Gitea's own CLI uses to reach the running server's internal - # API. It defaults to localhost, which is right for the server itself and - # useless from the init container that runs - # `gitea actions generate-runner-token`. It has to be set here rather than - # there: the CLI reads it out of the shared app.ini, and only this - # container's entrypoint renders environment into app.ini. - GITEA__server__LOCAL_ROOT_URL: http://gitea:3000/ - # Git over SSH would need a published host port of its own. Everything - # here — clone, push, CI, registry — goes over HTTPS instead. - GITEA__server__DISABLE_SSH: "true" - GITEA__server__START_SSH_SERVER: "false" - # Skips the first-run install wizard: every value it would ask for is set - # here, and the admin account is created by an init step below. - GITEA__security__INSTALL_LOCK: "true" - GITEA__database__DB_TYPE: sqlite3 - GITEA__database__PATH: /var/lib/gitea/data/gitea.db - GITEA__service__DISABLE_REGISTRATION: "true" - GITEA__service__REQUIRE_SIGNIN_VIEW: "true" - GITEA__actions__ENABLED: "true" - # Where a bare `uses: actions/checkout@v4` is resolved from. - GITEA__actions__DEFAULT_ACTIONS_URL: github - GITEA__packages__ENABLED: "true" - # Lets a pull mirror name a repository on this server's own Docker - # network — a Radicle node's git endpoint, for instance. See rationale.md. - GITEA__migrations__ALLOW_LOCALNETWORKS: "true" - # How often a pull mirror is re-fetched. The sync emits the same push - # event a real push would, so this interval is also how quickly CI - # reacts to a commit that arrives from the mirrored source. - GITEA__mirror__DEFAULT_INTERVAL: 10m - GITEA__mirror__MIN_INTERVAL: 10m + # The daemon jobs run in. Its socket is the only thing shared with it. + DOCKER_HOST: unix:///run/dind/docker.sock + # The images act runs a `runs-on:` label in. act's own medium images (Ubuntu with Node and + # the Docker CLI), the ones act users run on their laptops. + RAD_ACTIONS_PLATFORMS: ubuntu-latest=catthehacker/ubuntu:act-latest,ubuntu-24.04=catthehacker/ubuntu:act-24.04,ubuntu-22.04=catthehacker/ubuntu:act-22.04 + # `vars` every workflow sees. IMAGE = IMAGE_PREFIX/, the name IPCR imports. + RAD_ACTIONS_VARS: REGISTRY=localhost:5000,IMAGE_PREFIX=localhost:5000 + # cib's pages link each run to its log there (served by ipcr-forge). + RAD_ACTIONS_LOG_URL: /ci/logs/ + # The repository's web page, as the adapter's GitHub API stand-in reports it (html_url, which + # docker/metadata-action turns into the image's org.opencontainers.image.source label). + RAD_ACTIONS_REPO_URL: https://radicle-${APP_DOMAIN}/nodes/radicle-api-${APP_DOMAIN}/{rid} volumes: - - ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/data:/var/lib/gitea - - ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/config:/etc/gitea - healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/healthz"] - interval: 30s - timeout: 10s - retries: 5 - start_period: 60s - expose: - - 3000 - labels: - caddy_0: gitea-${APP_DOMAIN} - caddy_0.import: gateway_tls - caddy_0.reverse_proxy: "{{upstreams 3000}}" - caddy_1: gitea-${APP_PUBLIC_IP_DASH}.nip.io - caddy_1.import: gateway_tls - caddy_1.reverse_proxy: "{{upstreams 3000}}" - caddy_2: gitea-${APP_PUBLIC_IP_DASH}.sslip.io - caddy_2.reverse_proxy: "{{upstreams 3000}}" + # The node's home: its control socket (events), its storage (checkouts, job COBs) and its + # key, which signs the job COBs. The same mount radicle-api has. + - ${DATA_ROOT:-/DATA}/AppData/$AppID/radicle/home/:/radicle-home/ + # cib's database, its pages and the run logs, act's action cache, and scratch checkouts. + - ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/state:/ci/state + - ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/html:/ci/html + - ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/cache:/ci/cache + - ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/home:/ci/home + - ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/work:/ci/work + - ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/socket:/run/dind + depends_on: + - radicle-node + - rad-actions-docker networks: - - pcs - - gitea-internal + # Egress only: act fetches actions (actions/checkout, docker/*) from GitHub. + - ci-internal - # The CI runner: act_runner with its own Docker daemon inside it (dind), so - # workflow jobs and the images they build never touch the host's daemon. - # See rationale.md for what `privileged` buys and what it costs. - gitea-runner: - image: gitea/act_runner:0.6.1-dind - container_name: gitea-runner - restart: unless-stopped + # The daemon jobs run in: Docker-in-Docker, so jobs and the images they build never touch the + # host's daemon. See rationale.md for what `privileged` buys and what it costs. No TCP listener: + # its only API is a unix socket in a folder shared with rad-actions, owned by $PGID. + rad-actions-docker: + image: docker:29.1-dind + container_name: rad-actions-docker privileged: true + restart: unless-stopped cpu_shares: 20 + command: ["--host=unix:///run/dind/docker.sock", "--group=$PGID"] deploy: resources: limits: memory: 2G environment: TZ: $TZ - # This address is not just how the runner daemon reaches Gitea — it is - # handed to every job as GITHUB_SERVER_URL, and a job runs inside the - # runner's *nested* daemon, on a network where `gitea` does not resolve. - # So it has to be an address that works from the outside: checkout and - # `docker login` from a workflow both go out and back over HTTPS. - GITEA_INSTANCE_URL: https://gitea-${APP_DOMAIN} - GITEA_RUNNER_REGISTRATION_TOKEN_FILE: /data/token - GITEA_RUNNER_NAME: gitea-runner - # Registration-time labels. runner-config/config.yaml repeats them, and - # that copy wins on every start, so an installed runner follows store updates. - GITEA_RUNNER_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,docker:docker://docker:28.5-cli - CONFIG_FILE: /config/config.yaml - # DOCKER_TLS_CERTDIR is deliberately left at the image's default. Emptying - # it does not turn the nested daemon's TCP listener off — it turns the - # listener's TLS off, and 2375 then answers unauthenticated to anything - # that can reach this container. + # No certificates: with an explicit --host the entrypoint adds no TCP listener to protect. + DOCKER_TLS_CERTDIR: "" volumes: - # .runner (the registration this runner is identified by) and the - # registration token the init step leaves for it. - - ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/runner:/data - - ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/runner-config:/config:ro - # Replaces the image's own start-up wait — see the comments in the - # script. It is declared under x-compose-app.files rather than left to - # the seed tree, because it has to arrive executable. - - ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/runner-config/act_runner-run:/etc/s6/act_runner/run:ro - # The inner daemon's image store. Without this every restart re-pulls - # every base image a workflow uses. - - ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/runner-docker:/var/lib/docker + - ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/socket:/run/dind + # The daemon's image store. Without it every restart re-pulls every job image. + - ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/docker:/var/lib/docker + healthcheck: + test: ["CMD", "docker", "-H", "unix:///run/dind/docker.sock", "info"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s networks: - - gitea-internal + - ci-internal - # ═══ Radicle → Gitea bridge ════════════════════════════════════════════════════ - # Verbatim from Apps/Radicle-Gitea-Bridge; only its data folder moves to $AppID/bridge/. - - radicle-gitea-bridge: - image: ghcr.io/worph/radicle-gitea-bridge:1.0.0 - container_name: radicle-gitea-bridge + # The staging registry, in the daemon's network namespace: `localhost:5000` to the daemon and to + # every job (act runs jobs on the host network, which is this namespace), so a plain + # `docker push` or buildx with `network=host` works without TLS or credentials — Docker allows + # plain HTTP to localhost only. ipcr-gateway reaches it as rad-actions-docker:5000. Nothing + # outside ci-internal can. + rad-actions-registry: + image: registry:3.0.0 + container_name: rad-actions-registry user: $PUID:$PGID - # Reads two HTTP APIs and runs git. Needs no capability at all. cap_drop: [ALL] security_opt: ["no-new-privileges:true"] restart: unless-stopped - command: ["bridge"] + network_mode: service:rad-actions-docker cpu_shares: 20 deploy: resources: @@ -346,31 +315,14 @@ services: memory: 128M environment: TZ: $TZ - # git wants a home; the image's is root's and this runs as $PUID. - HOME: /tmp - RAD_API: http://radicle-api:8080 - GITEA_URL: http://gitea:3000 - # The Gitea account that owns the copies — the one the Gitea app creates. - GITEA_OWNER: gitea_admin - GITEA_TOKEN_FILE: /secrets/gitea-token - # A repository opts in by containing this path at its head. - OPT_IN_PATH: .gitea/workflows - # Seconds between passes. Releases are not latency-sensitive. - INTERVAL: "30" + REGISTRY_HTTP_ADDR: 0.0.0.0:5000 + # Staging only: IPCR has its own copy once imported, so old tags may be deleted. + REGISTRY_STORAGE_DELETE_ENABLED: "true" + OTEL_TRACES_EXPORTER: none volumes: - # One bare repository per Radicle repository: a staging copy between fetch and - # push, so a push only sends what changed. Disposable: rebuilt if deleted. - - ${DATA_ROOT:-/DATA}/AppData/$AppID/bridge/cache:/cache - # The Gitea token the init step mints. Read-only here. - - ${DATA_ROOT:-/DATA}/AppData/$AppID/bridge/secrets:/secrets:ro - healthcheck: - test: ["CMD-SHELL", "test -s /secrets/gitea-token && curl -fsS -o /dev/null http://radicle-api:8080/api/v1 && curl -fsS -o /dev/null http://gitea:3000/api/healthz"] - interval: 60s - timeout: 10s - retries: 3 - start_period: 30s - networks: - - pcs + - ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/registry:/var/lib/registry + depends_on: + - rad-actions-docker # ═══ IPCR — the IPFS container registry ════════════════════════════════════════ # Verbatim from Apps/IPCR; only its data folder moves to $AppID/ipcr/. @@ -379,18 +331,13 @@ services: # ipcrd: TLS front door on 127.0.0.1:4767. Passes /ipfs/ through to the stock nerdctl # registry below, resolves /ipns/: itself, and pins every image it serves. ipcr-gateway: - image: ghcr.io/yundera/ipcr:1.1.0 + image: ghcr.io/yundera/ipcr:1.2.0 container_name: ipcr-gateway # Root for one reason: it writes its CA into the host's Docker trust store (the narrow # bind below), which is root-owned. See rationale.md. user: "0:0" # Root needs no capabilities to write root-owned files it owns; drop them all. cap_drop: [ALL] - # Forge only: one back, read-only. The import token below is written by bridge-credentials as - # $PUID, mode 0600, in a 0700 folder; a root with no capabilities is bound by those modes like - # anyone else (verified: "permission denied"). DAC_READ_SEARCH bypasses read and search checks - # only, never write, and only on what this container mounts. See rationale.md. - cap_add: [DAC_READ_SEARCH] security_opt: ["no-new-privileges:true"] restart: unless-stopped command: ["gateway"] @@ -405,21 +352,20 @@ services: # IPCR_IMPORT_REPOS) is copied into IPFS and published as /ipns/:; # a tag is re-imported when it moves. Empty = off. IPCR_IMPORT_AUTH is user:token, # a read-only (read:package) token is enough. - # Forge: watch this app's own Gitea, over gitea-internal (not through Caddy). - IMPORT_WATCH: ${IPCR_IMPORT_WATCH:-http://gitea:3000} + # Forge: watch the CI's staging registry, over ci-internal. Internal, so no credentials. + IMPORT_WATCH: ${IPCR_IMPORT_WATCH:-http://rad-actions-docker:5000} IMPORT_AUTH: ${IPCR_IMPORT_AUTH:-} - # Forge: a read:package token the bridge-credentials step mints. Re-read every poll - # and preferred over IMPORT_AUTH, so it may appear after the gateway is up. - IMPORT_AUTH_FILE: /data/secrets/registry-auth IMPORT_REPOS: ${IPCR_IMPORT_REPOS:-} + # Forge: every repository this node builds is published under one IPNS key, "forge", as + # /ipns//:. One name for the whole forge, which a DNSLink domain or an + # ENS name can point at: ipcr.localhost:4767/ipns/example.eth/:. + IMPORT_PUBLISHER: ${IPCR_IMPORT_PUBLISHER:-forge} IMPORT_INTERVAL: 60s STATE_DIR: /data/state volumes: - ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/tls:/data/tls # What the watcher already imported (tag → manifest digest). - ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/state:/data/state - # Forge only: the credential above. Read-only here. - - ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/secrets:/data/secrets:ro # The host's Docker trust store, so the CA above can be dropped into it: no daemon # reload, no daemon.json edit. The parent is mounted because Compose cannot create a # bind source whose name contains a colon (ipcr.localhost:4767). ipcrd writes exactly @@ -439,8 +385,8 @@ services: start_period: 10s networks: - ipcr-internal - # Forge only: reaches gitea:3000 for the import poller above. - - gitea-internal + # Forge only: reaches the staging registry for the import poller above. + - ci-internal cpu_shares: 50 deploy: resources: @@ -450,7 +396,7 @@ services: # ── Stock nerdctl IPFS registry ───────────────────────────────────────────── # `nerdctl ipfs registry serve`, unmodified: translates the registry API into IPFS reads. ipcr-registry: - image: ghcr.io/yundera/ipcr:1.1.0 + image: ghcr.io/yundera/ipcr:1.2.0 container_name: ipcr-registry # Root because nerdctl run as any other uid assumes rootless mode and refuses to start # without a rootless containerd — which `ipfs registry serve` never uses. The service has @@ -531,8 +477,8 @@ networks: # From Apps/Radicle: egress only, for the node. radicle-internal: driver: bridge - # From Apps/Gitea: gitea ↔ runner, nothing else. - gitea-internal: + # CI: rad-actions ↔ its daemon (and staging registry) ↔ ipcr-gateway; egress for builds. + ci-internal: driver: bridge # From Apps/IPCR: gateway ↔ registry ↔ kubo, nothing else. ipcr-internal: @@ -554,77 +500,90 @@ x-casaos: title: en_us: IPCR Forge tagline: - en_us: A decentralised forge in one install — Radicle, Gitea Actions and IPFS images. - fr_fr: Une forge décentralisée en une installation — Radicle, Gitea Actions et images IPFS. + en_us: A decentralised forge in one install — Radicle, GitHub-style CI and IPFS images. + fr_fr: Une forge décentralisée en une installation — Radicle, CI au format GitHub et images IPFS. description: en_us: | IPCR Forge is the full path from code to running container, without a central service: • **Radicle** holds your repositories on a peer-to-peer network. You push with `git push rad`. - • **Gitea Actions** builds them with GitHub-style workflows, triggered by a release tag. + • **Radicle CI** on this node builds them with GitHub-style workflows, triggered by a release tag. • **IPCR** serves the resulting images from IPFS, so anyone can `docker pull` them by name. - A repository joins the pipeline by committing a workflow under `.gitea/workflows/`: there - is nothing to configure per project. + Radicle is the single source of truth: builds read the commit straight from this node, and + their results go back into the repository as Radicle jobs. There is no mirror to keep in sync. - Its own page explains how to publish a project and lists every published image with the - line to put in a compose file. + A repository joins the pipeline by committing a workflow under `.github/workflows/`, as on + GitHub: there is nothing to configure per project. - This app bundles the Radicle, Gitea, Radicle Gitea Bridge and IPCR apps. Install it instead - of them, not beside them. + Its own page explains how to publish a project, links every build and its log, and lists + every published image with the line to put in a compose file. + + This app bundles the Radicle and IPCR apps. Install it instead of them, not beside them. fr_fr: | IPCR Forge couvre tout le chemin du code au conteneur en production, sans service central : • **Radicle** garde vos dépôts sur un réseau pair-à-pair. Vous poussez avec `git push rad`. - • **Gitea Actions** les construit avec des workflows au format GitHub, déclenchés par un tag. + • **Radicle CI** sur ce nœud les construit avec des workflows au format GitHub, déclenchés par un tag. • **IPCR** sert les images produites depuis IPFS : n'importe qui peut les `docker pull`. - Un dépôt rejoint la chaîne en ajoutant un workflow sous `.gitea/workflows/` : rien à - configurer par projet. + Radicle est l'unique source de vérité : les builds lisent le commit directement sur ce nœud, + et leurs résultats retournent dans le dépôt sous forme de jobs Radicle. Aucun miroir à + synchroniser. - Sa page explique comment publier un projet et liste chaque image publiée avec la ligne à - mettre dans un fichier compose. + Un dépôt rejoint la chaîne en ajoutant un workflow sous `.github/workflows/`, comme sur + GitHub : rien à configurer par projet. - Cette application regroupe les applications Radicle, Gitea, Radicle Gitea Bridge et IPCR. - Installez-la à leur place, pas à côté. + Sa page explique comment publier un projet, donne accès à chaque build et à son journal, et + liste chaque image publiée avec la ligne à mettre dans un fichier compose. + + Cette application regroupe les applications Radicle et IPCR. Installez-la à leur place, pas à + côté. tips: before_install: en_us: | - **Replaces four apps.** Uninstall Radicle, Gitea, Radicle Gitea Bridge and IPCR first: - this app uses the same addresses and ports. + **Replaces two apps.** Uninstall Radicle and IPCR first: this app uses the same addresses + and ports. | Address | What | | --- | --- | - | `https://ipcr-forge-$APP_DOMAIN` | This app: how to publish, and every published image | + | `https://ipcr-forge-$APP_DOMAIN` | This app: how to publish, every published image | + | `https://ipcr-forge-$APP_DOMAIN/ci/` | CI runs and their logs (public) | | `https://radicle-$APP_DOMAIN` | Radicle explorer (public) | | `https://radicle-api-$APP_DOMAIN` | Radicle API and read-only git (public) | - | `https://gitea-$APP_DOMAIN` | Gitea: builds, logs, staging registry (`gitea_admin`) | | `ipcr.localhost:4767` | IPCR registry, this server's Docker only | **Ports.** TCP 8776 (Radicle peers) and 4768 TCP+UDP (IPFS peers) are published. - **Images reach IPFS on their own.** IPCR follows this app's Gitea registry: about a - minute after a build pushes `gitea_admin/:`, it can be pulled as + **Who can build.** Only changes signed by one of a repository's delegates trigger a build: + release tags, the default branch and their patches. Builds run in their own Docker daemon. + + **Images reach IPFS on their own.** A workflow pushes to `vars.IMAGE` (this node's + staging registry); about a minute later the image can be pulled as `ipcr.localhost:4767/ipns/:`, where `` is the IPNS name (`k51…`) IPCR creates for the repository. The app's page lists each one with the line to copy. fr_fr: | - **Remplace quatre applications.** Désinstallez d'abord Radicle, Gitea, Radicle Gitea - Bridge et IPCR : cette application utilise les mêmes adresses et ports. + **Remplace deux applications.** Désinstallez d'abord Radicle et IPCR : cette application + utilise les mêmes adresses et ports. | Adresse | Quoi | | --- | --- | - | `https://ipcr-forge-$APP_DOMAIN` | Cette application : publier, et toutes les images publiées | + | `https://ipcr-forge-$APP_DOMAIN` | Cette application : publier, toutes les images publiées | + | `https://ipcr-forge-$APP_DOMAIN/ci/` | Builds et leurs journaux (public) | | `https://radicle-$APP_DOMAIN` | Explorateur Radicle (public) | | `https://radicle-api-$APP_DOMAIN` | API Radicle et git en lecture seule (public) | - | `https://gitea-$APP_DOMAIN` | Gitea : builds, journaux, registre de transit (`gitea_admin`) | | `ipcr.localhost:4767` | Registre IPCR, pour le Docker de ce serveur uniquement | **Ports.** TCP 8776 (pairs Radicle) et 4768 TCP+UDP (pairs IPFS) sont publiés. - **Les images arrivent sur IPFS d'elles-mêmes.** IPCR suit le registre Gitea de cette - application : environ une minute après qu'un build a poussé `gitea_admin/:`, - elle se tire avec `ipcr.localhost:4767/ipns/:`, où `` est le nom IPNS - (`k51…`) qu'IPCR crée pour le dépôt. La page de l'application les liste, prêts à copier. + **Qui peut lancer un build.** Seuls les changements signés par un délégué du dépôt + déclenchent un build : tags de version, branche par défaut et leurs patches. Les builds + tournent dans leur propre démon Docker. + + **Les images arrivent sur IPFS d'elles-mêmes.** Un workflow pousse vers `vars.IMAGE` + (le registre de transit de ce nœud) ; environ une minute plus tard, l'image se tire avec + `ipcr.localhost:4767/ipns/:`, où `` est le nom IPNS (`k51…`) qu'IPCR crée + pour le dépôt. La page de l'application les liste, prêts à copier. index: / webui_port: 8080 store_app_id: ipcr-forge @@ -646,29 +605,35 @@ x-compose-app: - path: /DATA/AppData/$AppID/radicle/caddy user: $PUID group: $PGID - # ── Gitea ── - - path: /DATA/AppData/$AppID/gitea/data - user: $PUID - group: $PGID - - path: /DATA/AppData/$AppID/gitea/config - user: $PUID - group: $PGID - - path: /DATA/AppData/$AppID/gitea/runner + # ── CI ── + - path: /DATA/AppData/$AppID/ci/state user: $PUID group: $PGID mode: "0700" - - path: /DATA/AppData/$AppID/gitea/runner-config + - path: /DATA/AppData/$AppID/ci/html/logs user: $PUID group: $PGID - - path: /DATA/AppData/$AppID/gitea/runner-docker + - path: /DATA/AppData/$AppID/ci/cache + user: $PUID + group: $PGID + - path: /DATA/AppData/$AppID/ci/home + user: $PUID + group: $PGID + mode: "0700" + - path: /DATA/AppData/$AppID/ci/work + user: $PUID + group: $PGID + mode: "0700" + # The daemon's socket: created by root, group $PGID (dockerd --group), mode 0660. + - path: /DATA/AppData/$AppID/ci/socket + user: $PUID + group: $PGID + mode: "0750" + - path: /DATA/AppData/$AppID/ci/docker user: $PUID group: $PGID mode: "0710" - # ── Bridge ── - - path: /DATA/AppData/$AppID/bridge/cache - user: $PUID - group: $PGID - - path: /DATA/AppData/$AppID/bridge/secrets + - path: /DATA/AppData/$AppID/ci/registry user: $PUID group: $PGID mode: "0700" @@ -680,11 +645,6 @@ x-compose-app: - path: /DATA/AppData/$AppID/ipcr/init.d user: $PUID group: $PGID - # Written by bridge-credentials as $PUID, read by ipcr-gateway (root). - - path: /DATA/AppData/$AppID/ipcr/secrets - user: $PUID - group: $PGID - mode: "0700" files: # The landing page follows store updates: re-rendered on every up. None of the three holds a # dollar-brace sequence, so rendering cannot fail on an unresolved variable. @@ -709,17 +669,12 @@ x-compose-app: from: radicle/caddy/Caddyfile.tmpl ensure: always mode: "0644" - - path: /DATA/AppData/$AppID/gitea/runner-config/act_runner-run - from: gitea/runner-config/act_runner-run - ensure: always - mode: "0755" - path: /DATA/AppData/$AppID/ipcr/init.d/01-ipcr-node.sh from: ipcr/init.d/01-ipcr-node.sh ensure: always mode: "0755" init: - # In order: Radicle's identity before its node starts, then Gitea's admin, then the - # runner token and the bridge's credentials, which both need that admin to exist. + # Radicle's identity before its node starts. The CI needs nothing: it uses the node's. - name: radicle-identity image: dirk1980/radicle-seed-node@sha256:78ccee526ad4b4f4d7c79c2323b032a7209858a48e1dc9c077272ff1660050e2 user: $PUID:$PGID @@ -727,53 +682,5 @@ x-compose-app: - /DATA/AppData/$AppID/radicle/home:/radicle-home command: ["sh", "-c", "yes '' | rad auth --alias radicle-${APP_PUBLIC_IP_DASH} --stdin"] when: absent:/DATA/AppData/$AppID/radicle/home/keys/radicle - - name: gitea-admin - image: gitea/gitea:1.27.3-rootless - user: $PUID:$PGID - phase: post_up - volumes: - - /DATA/AppData/$AppID/gitea/data:/var/lib/gitea - - /DATA/AppData/$AppID/gitea/config:/etc/gitea - command: - - sh - - -c - - | - timeout 240 sh -c 'until gitea admin user list >/dev/null 2>&1; do sleep 3; done' - gitea admin user create --admin --username gitea_admin \ - --password "$APP_DEFAULT_PASSWORD" --email "$APP_EMAIL" \ - --must-change-password=false \ - || echo "admin account already present, leaving it alone" - when: always - - name: gitea-runner-token - image: gitea/gitea:1.27.3-rootless - user: $PUID:$PGID - phase: post_up - network: $APP_NET - volumes: - - /DATA/AppData/$AppID/gitea/data:/var/lib/gitea - - /DATA/AppData/$AppID/gitea/config:/etc/gitea - - /DATA/AppData/$AppID/gitea/runner:/runner - command: - - sh - - -c - - | - timeout 240 sh -c 'until gitea actions generate-runner-token > /runner/token.new 2>/dev/null; do sleep 3; done' - mv /runner/token.new /runner/token - chmod 600 /runner/token - when: absent:/DATA/AppData/$AppID/gitea/runner/.runner - - name: bridge-credentials - image: ghcr.io/worph/radicle-gitea-bridge:1.0.0 - user: $PUID:$PGID - phase: post_up - network: $APP_NET - volumes: - - /DATA/AppData/$AppID/bridge/secrets:/secrets - - /DATA/AppData/$AppID/ipcr/secrets:/ipcr-secrets - # Also mints IPCR's read-only token for following Gitea's registry. - command: - - sh - - -c - - GITEA_PASSWORD="$APP_DEFAULT_PASSWORD" IPCR_AUTH_FILE=/ipcr-secrets/registry-auth bridge-init - when: absent:/DATA/AppData/$AppID/bridge/secrets/gitea-token webui-host: ipcr-forge-${domain} webui-path: / diff --git a/Apps/IPCR-Forge/rationale.md b/Apps/IPCR-Forge/rationale.md index eba68af..5d60065 100644 --- a/Apps/IPCR-Forge/rationale.md +++ b/Apps/IPCR-Forge/rationale.md @@ -1,9 +1,13 @@ # IPCR Forge — Rationale -IPCR Forge bundles four store apps (Radicle, Gitea, Radicle Gitea Bridge, IPCR) and a static -landing page. Their services are copied verbatim, so each part keeps the deviations its -standalone listing already argues. This document lists them, then covers only what the -bundle adds. +IPCR Forge bundles two store apps (Radicle, IPCR), the node's own CI (`rad-actions`) and a static +landing page. The Radicle and IPCR services are copied verbatim, so each keeps the deviations its +standalone listing already argues. This document lists them, then covers only what the bundle adds. + +Earlier versions built with Gitea Actions, fed by a bridge that copied every Radicle repository +into Gitea. That made two places where state could start (Gitea held a second copy of each +repository, with its own PRs, issues and registry) and needed a glue service to keep them in step. +Gitea, its runner and the bridge are gone; the CI now runs on the Radicle node itself. ## What deviation / exception is being requested @@ -12,98 +16,97 @@ bundle adds. | From | Deviation | | --- | --- | | Radicle | No authentication gate on `radicle-` and `radicle-api-`; host port `8776/tcp` | -| Gitea | `gitea-runner` is `privileged` (Docker-in-Docker); `ALLOW_LOCALNETWORKS`; admin account `gitea_admin` | -| Radicle Gitea Bridge | An install step signs in once with the default app password to mint scoped tokens | | IPCR | `ipcr-gateway` and `ipcr-registry` run as root (no capabilities); `/etc/docker/certs.d` mounted; no authentication; ports `127.0.0.1:4767` and `4768/tcp+udp` | **Added by the bundle:** -1. **A cross-app network link.** `ipcr-gateway` joins `gitea-internal`, the network the - standalone Gitea app reserves for Gitea and its runner. -2. **A cross-app credential.** The bridge's install step writes a token into IPCR's folder - (`ipcr/secrets/registry-auth`), which `ipcr-gateway` reads. -3. **A public landing page.** `ipcr-forge-` is not behind the SSO gate. -4. **It replaces four apps** and cannot be installed beside them: same container names, - domains and host ports. -5. **One added capability.** `ipcr-gateway` gets `cap_add: [DAC_READ_SEARCH]` on top of the - standalone listing's `cap_drop: [ALL]`. +1. **`rad-actions-docker` runs `privileged`** (Docker-in-Docker), as Gitea's runner did. +2. **`rad-actions` mounts the Radicle node's home read-write**, including the node's key. +3. **A cross-app network link.** `ipcr-gateway` joins `ci-internal`, where the staging registry is. +4. **A public landing page, including the CI's pages.** `ipcr-forge-` and its `/ci/` + (run reports and logs) are not behind the SSO gate. +5. **It replaces two apps** and cannot be installed beside them: same container names, domains + and host ports. ## Why it is necessary -**1. The network link.** IPCR publishes images by *pulling* them from Gitea's registry: it -polls the registry and imports every new tag into IPFS. The pull has to come from IPCR's -side, because Gitea's build jobs run in the runner's nested Docker, which cannot reach -`ipcr.localhost` (that name is the host's loopback). In the standalone apps the gateway pulls -over Gitea's public HTTPS address. In the bundle both are on the same server, so it uses -`http://gitea:3000` over `gitea-internal` instead: no round trip through Caddy, and it keeps -working on a box with no public address. Gitea's registry hands out its bearer tokens at the -address the request came in on (`http://gitea:3000/v2/token`), so authentication stays -internal too. +**1. Privileged Docker-in-Docker.** Building an image needs a Docker daemon. The two ways to give +a CI job one are the host's socket (root on the PCS for every workflow) or a daemon of its own, +which needs `privileged`. The second confines a build to its own container and image store. The +rootless variant does not start on this platform (`kernel.apparmor_restrict_unprivileged_userns=1`; +see Apps/Gitea/rationale.md, where it was tried first). -**2. The credential.** Gitea's registry requires sign-in (`REQUIRE_SIGNIN_VIEW`), so IPCR -needs a token. Gitea mints tokens only for a caller holding the account password, and the -bridge's install step is the one place that already does that. It mints one more token, -`ipcr-import`, with `read:package` only, and writes it as a file because an install step can -produce files but not environment variables. `ipcrd` reads `IMPORT_AUTH_FILE` at the start of -every poll, so the file may appear after the gateway has started. +**2. The node's home.** The CI broker (`cib`, upstream `radicle-ci-broker`) is a client of the +node: it subscribes to the node's events over the control socket in that folder, reads repository +storage to check out a commit, and writes each run's result back into the repository as a job COB, +which the node's key signs. This is the mount `radicle-api` already has. It is what removes the +mirror: the build reads the commit where it lives, so there is no copy to drift. -**3. The landing page.** It holds no data that isn't public already: links to Radicle (public -by design) and Gitea (behind its own login), documentation, and the list of images IPCR has -published, which are public on IPFS by definition. A login in front of it would protect -nothing and would stand between a developer and the `image:` line they came for. +**3. The network link.** IPCR publishes images by *pulling* them: it polls a registry and imports +every new tag into IPFS. Jobs run in the CI's own daemon, which cannot reach `ipcr.localhost` (that +name is the host's loopback), so a workflow pushes to a staging registry instead. That registry +shares the daemon's network namespace, so it is `localhost:5000` to every job — the only address +Docker and BuildKit accept plain HTTP for, so no TLS or credentials are involved. `ipcr-gateway` +reaches it as `rad-actions-docker:5000` over `ci-internal`. -**4. Replacing the four apps.** The bundle is the same services, not a different product. -Renaming every container, domain and port to coexist would break the defaults the bridge -and IPCR rely on (`radicle-api:8080`, `gitea:3000`) and the documented addresses users type, -for no use case: running two copies of the same forge on one server. +**4. The public pages.** The landing page holds links, documentation and image names that are +public on IPFS anyway. `/ci/` shows `cib`'s report pages and the run logs. Radicle repositories are +public by design, and the adapter refuses to run a private repository, so a log never shows code +that is not already public. A login in front would stand between a developer and their build log. -**5. `DAC_READ_SEARCH`.** The gateway runs as root (for the CA it drops into the host's Docker -trust store) with every capability dropped, and a root with no capabilities is bound by file -modes like any other user. The import token is written by the bridge's install step as `$PUID`, -mode `0600`, in a `0700` folder, so without the capability the gateway cannot open it (verified -on a fresh install: `open /data/secrets/registry-auth: permission denied`, then `401` from -Gitea). `DAC_READ_SEARCH` restores exactly the missing half: reading files and traversing -directories regardless of their modes. +**5. Replacing two apps.** The bundle is the same services, not a different product. Renaming every +container, domain and port to coexist would break the defaults the services rely on +(`radicle-api:8080`) and the documented addresses users type. ## Security mitigations in place -- **The added capability is read-only and narrow.** `DAC_READ_SEARCH` bypasses read and - directory-search checks only. It grants no write, ownership or privilege change, and reaches - only what the gateway mounts: its own TLS folder and state, the token (mounted read-only) and - the Docker trust store it already writes to. - -- **The network link is narrow.** `gitea-internal` holds only `gitea`, `gitea-runner` and now - `ipcr-gateway`. The gateway's only listener is the read-only registry API. Kubo's - unauthenticated RPC API stays on `ipcr-internal`, which nothing from Gitea joins. Jobs run - in the runner's nested daemon, on a network where none of these names resolve. -- **The credential is read-only and contained.** `read:package` cannot push, delete or change - anything. The file is written as `$PUID` into a `0700` folder and mounted read-only into the - gateway. It is written then renamed, so a half-written token is never read. Re-running the - install step replaces the token in Gitea, which revokes the old one. -- **The admin password is used once and never stored.** It is passed to `bridge-init` on its - command line, as the Gitea app's own `create-admin` step does, and only the scoped tokens - are written to disk. +- **Only delegates can start a build.** `cib`'s filter (ci/ci-broker.yaml) runs CI only for changes + that come from a delegate of the repository (`AnyDelegate`): release tags, the default branch and + their patches. A stranger's patch is never run. This matters because a build can publish: what a + job pushes to the staging registry ends up on IPFS under the repository's IPNS name. A build + therefore gets exactly the trust of a release. +- **The privileged daemon has no network listener.** `dockerd` gets an explicit + `--host=unix:///run/dind/docker.sock` and `DOCKER_TLS_CERTDIR` is empty, so the entrypoint adds no + TCP socket. The unix socket sits in a folder shared only with `rad-actions`, group `$PGID`. +- **`rad-actions` itself runs unprivileged:** `$PUID`, every capability dropped, + `no-new-privileges`, 512 MB. Whoever controls it controls the daemon (that is the point of the + socket), so it accepts no input but the node's events and a workflow from a delegate. +- **The node's key never enters a job.** Jobs run in the nested daemon from a self-contained, + depth-1 checkout that the adapter fetched; the Radicle home is not mounted into them. +- **The staging registry is unreachable from outside.** No port, no Caddy route, on `ci-internal` + only (`rad-actions`, the daemon and `ipcr-gateway`). It runs as `$PUID` with no capabilities. +- **Bounded.** One run at a time (`concurrent_adapters: 1`), 60 minutes per run, 2 GB for the daemon. - **The landing page cannot change anything.** `nginx-unprivileged` runs as `$PUID` with a - read-only root filesystem (a tmpfs `/tmp` only), no capabilities, `no-new-privileges` and a - 32 MB limit. It mounts the page, its config and IPCR's `state/` read-only, and serves static - files only. -- **It publishes one file of IPCR's state, not the folder.** `state/` is mounted outside the - web root, and `default.conf` maps exactly one path to it: - `location = /images.json { alias /srv/ipcr-state/published.json; }`. The watcher's own - bookkeeping (`imported.json`, tag → digest) and anything else in `state/` answers 404. -- **Every inherited mitigation still applies,** because the services are unchanged. For - example, the privileged runner can still be stopped without affecting the rest of the forge. + read-only root filesystem, no capabilities, `no-new-privileges` and 32 MB. It mounts the page, + its config, IPCR's `state/` and the CI's `html/`, all read-only, and serves static files only. + From `state/` it maps exactly one file (`/images.json` → `published.json`). +- **The CI is severable.** Stopping `rad-actions` and `rad-actions-docker` removes the privileged + container and leaves a working Radicle node and IPCR registry. +- **Smaller than what it replaces.** Gone: an admin account with the default password, two minted + tokens, the `DAC_READ_SEARCH` capability IPCR needed to read one of them, and a second git server. ## Alternatives considered and rejected | Alternative | Why not | | --- | --- | -| Gitea pushes to IPCR (webhook, or a workflow step) | A workflow step cannot reach `ipcr.localhost`. A webhook would give IPCR a write endpoint on a shared network; a pull needs no inbound access at all. | -| Gateway pulls over the public `gitea-` address, as standalone | Works, but routes a same-box fetch out through Caddy and back, and fails on a box with no public address. | -| An admin-password credential for IPCR | Simpler, but gives the gateway full control of Gitea for a job that only reads packages. | -| `IMPORT_AUTH` as an environment variable | An install step cannot set one, so it would be a manual step after every install. | -| Renaming everything so the bundle can sit beside the four apps | Breaks the defaults and the documented addresses (see 4). | -| No landing page, Radicle explorer as the tile | Leaves newcomers without the pipeline explained or the IPNS name to pull, which IPCR mints and nobody can guess. | +| Keep Gitea, fed by a bridge (previous version) | Two sources of state (a second copy of each repository, with its own PRs and registry) and a glue service to keep them in step. | +| Gitea as a strict read-only mirror | Still a second git server and a sync to babysit, for one thing it adds: a web view of builds, which `cib`'s pages now provide. | +| Gitea's `act_runner` in `exec` mode | Same engine as `act` (a fork), but built for Gitea; nektos `act` is what people run on their laptops. | +| `cib`'s native adapter, or Ambient | Neither runs GitHub-style workflows; Ambient needs KVM. | +| `ipcrd` accepting pushes itself | A push endpoint would be a second registry implementation. The staging registry plus IPCR's existing import is byte-identical to `nerdctl push` and needed no change. | +| Host Docker socket for jobs | Any workflow would get root on the host. | +| Run patches from anyone | Their build could publish under the repository's IPNS name. Needs a separate daemon with no registry first. | + +## Known limitations + +- **No job COB for tag builds.** cib 0.32.1 keys a job by the event's tip, which for an annotated + tag is the tag object rather than a commit, and creating that job fails (silently; the log only + shows `NoJob` when the run finishes). Branch and patch builds get their job COB. Tag builds still + appear in `/ci/` with their log. Upstream fix: peel the tag before creating the job. +- **Tags arrive as pushes.** cib sends a tag event as `event_type: push` with the tag's name in + `branch`. The adapter tells them apart by looking for that name under `refs/tags/` in storage. +- **`HasFile` matches files only**, not folders, so cib cannot pre-filter on a workflow folder; the + adapter answers "no workflow" itself. ## Data protection @@ -111,13 +114,12 @@ Everything lives under `/DATA/AppData/ipcr-forge/`, one folder per part: | Folder | Holds | | --- | --- | -| `radicle/` | node identity (key pair), seeded repositories, Caddy config | -| `gitea/` | repositories, SQLite database, `app.ini`, runner registration, the runner's image store | -| `bridge/` | `cache/` (disposable bare repositories), `secrets/gitea-token` (0600) | -| `ipcr/` | Kubo repo (node identity, IPNS keys, pinned images), TLS CA, watcher state, `secrets/registry-auth` | +| `radicle/` | node identity (key pair), seeded repositories (including job COBs), Caddy config | +| `ci/` | `cib`'s database (`state/`), report pages and run logs (`html/`), act's action cache, the daemon's image store (`docker/`), the staging registry (`registry/`, disposable) | +| `ipcr/` | Kubo repo (node identity, IPNS keys, pinned images), TLS CA, watcher state | | `site/`, `web/` | the landing page and its nginx config, re-rendered on every start | No user directory (`/DATA/Documents`, `Media`, …) is mounted. The one file outside `/DATA` is -IPCR's `/etc/docker/certs.d/ipcr.localhost:4767/ca.crt`, as in the standalone app. Identities -that matter on the network (Radicle's node key, Kubo's node key and IPNS keys) survive -uninstall and reinstall, so published repositories and image names stay valid. +IPCR's `/etc/docker/certs.d/ipcr.localhost:4767/ca.crt`, as in the standalone app. Identities that +matter on the network (Radicle's node key, Kubo's node key and IPNS keys) survive uninstall and +reinstall, so published repositories and image names stay valid. diff --git a/Apps/IPCR-Forge/seed/gitea/runner-config/act_runner-run b/Apps/IPCR-Forge/seed/gitea/runner-config/act_runner-run deleted file mode 100644 index ae63df8..0000000 --- a/Apps/IPCR-Forge/seed/gitea/runner-config/act_runner-run +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -# Replaces the image's own /etc/s6/act_runner/run. Two waits, both of which the -# stock script gets wrong for this deployment. -# -# 1. The nested Docker daemon. Upstream waits for it through s6's readiness -# notification. On a cold start with a warm image store dockerd takes longer -# than s6-notifyoncheck's window, the notification never arrives, s6 marks -# the service permanently failed, and act_runner sits blocked in -# `s6-svwait -U` for two to four minutes before s6 gives up and lets it past -# anyway. The runner shows "offline" in the forge for that whole time and -# picks up no jobs. Asking the daemon directly is the same wait without the -# guesswork. -# -# 2. The registration token. It is minted by an init step that runs after the -# stack is up, so on a first install it does not exist yet when this -# container starts — and the stock entrypoint reads the token file exactly -# once, before its retry loop, so a token that arrives late is never seen. - -while [ ! -d /etc/s6/docker/supervise ]; do - sleep 0.1 -done - -while ! docker info >/dev/null 2>&1; do - sleep 1 -done - -while [ ! -s "${GITEA_RUNNER_REGISTRATION_TOKEN_FILE:-/data/token}" ] && [ ! -s /data/.runner ]; do - echo "waiting for the runner registration token" - sleep 2 -done - -exec run.sh diff --git a/Apps/IPCR-Forge/seed/gitea/runner-config/config.yaml b/Apps/IPCR-Forge/seed/gitea/runner-config/config.yaml deleted file mode 100644 index 9da8334..0000000 --- a/Apps/IPCR-Forge/seed/gitea/runner-config/config.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# act_runner configuration. Only the settings that differ from upstream's -# defaults are written out — `act_runner generate-config` prints the full set. - -log: - level: info - -runner: - # The file the runner's own registration is kept in, relative to /data. - file: .runner - # One job at a time. A PCS shares its CPU with everything else installed on - # it, and an image build is the least polite thing on the box. - capacity: 1 - timeout: 1h - fetch_timeout: 5s - fetch_interval: 2s - # Overrides the labels stored at registration (.runner). These are upstream - # act_runner defaults: Gitea's own job images, which carry node, bash, git - # and the docker CLI with buildx, so marketplace actions (actions/checkout, - # docker/build-push-action, ...) run as they do on GitHub. The registration - # labels pointed at node:22-bookworm, which has no docker CLI. - 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" - - "docker:docker://docker:28.5-cli" - -cache: - # The cache server binds an address inside the runner container, which the - # job containers — created by the Docker daemon nested inside that same - # container — cannot route to. Left on, every `actions/cache` step fails - # noisily instead of being skipped. - enabled: false - -container: - # Jobs run on the runner's own nested daemon, on its default bridge: they - # reach the internet, and they reach Gitea the way anything outside would, - # through its public address. - network: bridge - privileged: false - force_pull: false diff --git a/Apps/IPCR-Forge/seed/site/index.html b/Apps/IPCR-Forge/seed/site/index.html index f877ca2..89d480c 100644 --- a/Apps/IPCR-Forge/seed/site/index.html +++ b/Apps/IPCR-Forge/seed/site/index.html @@ -8,8 +8,9 @@