IPCR Forge: fixes from the holyhorse run

- forge-runner: 1G (act_runner was OOM-killed mid-job at 256M)
- bridge: log rad's stdout on failure; page links Gitea from its own host
  (html_url is the internal address); description fallback without it
- docs/forge.md: holyhorse verification and findings 11-14
This commit is contained in:
Worph
2026-10-04 18:59:21 +00:00
parent 6fe4c40cbe
commit 5227a77477
7 changed files with 35 additions and 13 deletions
+3 -1
View File
@@ -200,7 +200,9 @@ services:
deploy: deploy:
resources: resources:
limits: limits:
memory: 256M # act_runner holds a job's checkout and actions in memory while it copies them into the
# job container: 256M was OOM-killed mid-job on the first build (holyhorse, 2026-10-04).
memory: 1G
environment: environment:
TZ: $TZ TZ: $TZ
HOME: /data HOME: /data
+24 -7
View File
@@ -148,11 +148,20 @@ New in v3 (tested locally with Gitea 1.27.3 and rad 1.10.1, 2026-10-04):
10. **`rad sync` blocks for its timeout when the node has no peers.** The bridge pushes with 10. **`rad sync` blocks for its timeout when the node has no peers.** The bridge pushes with
`-o no-sync` and announces separately with a 30-second bound, so a push is never held up by the `-o no-sync` and announces separately with a 30-second bound, so a push is never held up by the
network. network.
11. **`act_runner` needs about 1 GB.** It holds a job's checkout and actions in memory while it
copies them into the job container. At 256 MB it was OOM-killed mid-job. The job container
then ran on, orphaned, and Gitea showed the run as "running" until it timed out.
12. **The first `rad init` can fail if the node is still starting.** The bridge's retry adopts the
RID that `rad init` already wrote into the mirror's `rad` remote, so no second repository is
created (seen on holyhorse). `rad` prints its errors on stdout, which the bridge now logs.
13. **Gitea's API returns `html_url` built from the internal address** (`http://gitea:3000/…`) when
called through it. The page builds Gitea links from its own host name instead.
14. **Right after `up`, new domains answer with the gateway's SSO redirect** until Caddy picks up the
labels (about a minute). The runner's registration retries through that window.
## Verified ## Verified
Local integration (Gitea 1.27.3-rootless, `radicle-seed-node` 1.10.1, the bridge image, one Docker **Locally** (Gitea 1.27.3-rootless, `radicle-seed-node` 1.10.1, the bridge image, one Docker network):
network):
- `setup` run twice leaves exactly one system hook. - `setup` run twice leaves exactly one system hook.
- `ipcr-hello` was created and mirrored with the crefs rule in its identity. - `ipcr-hello` was created and mirrored with the crefs rule in its identity.
@@ -160,14 +169,22 @@ network):
- A private repository never appeared. - A private repository never appeared.
- A public repository made private was frozen. - A public repository made private was frozen.
The full chain on a PCS (runner → staging → IPCR → pull from a second server) is in **On holyhorse** (a test PCS, hand install that mimics Maison, 2026-10-04):
[Open items](#open-items) until it is run on a test box.
| Step | Result |
| --- | --- |
| install steps: Gitea admin, runner token, `forge-setup` | token, system hook, `ipcr-hello` created; runner registered |
| `ipcr-hello` mirrored to Radicle | `rad:z4Nc3jTG2q2rfQbio2gfuA7tWouee`, crefs rule in place |
| tag `v1.0.1` created through Gitea's API (as the web UI does) | Actions run started on `push`; tag canonical on Radicle in seconds |
| build (job image pulled cold) | success after about 4 min; `localhost:5000/gitea_admin/ipcr-hello:1.0.1` and `latest` |
| IPCR import | about a minute later: `/ipns/k51qzi5uqu5dg7urd3olssfzho23ze73n0jd9rwlqkivjlz3gspzyajk8cqfmd/gitea_admin/ipcr-hello:1.0.1` |
| `docker pull` of that name, then `docker run` | page served (`Hello from IPFS`) |
| forge page `ipcr-forge-holyhorse.nsl.sh` | 200; `/images.json` and `/repos.json` filled |
## Open items ## Open items
- **Run the full chain on a test PCS.** Check that the runner registers, that jobs see the daemon - **Pull from a second server.** Verified on holyhorse itself only; v2's images pulled across
(`docker_host: ""` mounts its socket into each job), that a `v*` tag on `ipcr-hello` builds and servers the same way.
imports, and that the image pulls from a second server.
- **Publish the images**: `ghcr.io/yundera/ipcr:1.2.0` and `ghcr.io/yundera/ipcr-forge-bridge:0.1.0` - **Publish the images**: `ghcr.io/yundera/ipcr:1.2.0` and `ghcr.io/yundera/ipcr-forge-bridge:0.1.0`
(workflows `image.yml` and `bridge.yml`, tag `bridge-v0.1.0`). (workflows `image.yml` and `bridge.yml`, tag `bridge-v0.1.0`).
- **Per-repository image names:** the bridge as a registry token server (see Trust). - **Per-repository image names:** the bridge as a registry token server (see Trust).
-1
View File
@@ -96,7 +96,6 @@ type repo struct {
Mirror bool `json:"mirror"` Mirror bool `json:"mirror"`
Empty bool `json:"empty"` Empty bool `json:"empty"`
DefaultBranch string `json:"default_branch"` DefaultBranch string `json:"default_branch"`
HTMLURL string `json:"html_url"`
Owner struct { Owner struct {
Login string `json:"login"` Login string `json:"login"`
// public | limited | private. Only a public owner's repositories are public. // public | limited | private. Only a public owner's repositories are public.
+1 -1
View File
@@ -46,7 +46,7 @@ func (m *mirror) sync(id int64) {
return return
} }
cur := prev cur := prev
cur.FullName, cur.HTMLURL = r.FullName, r.HTMLURL cur.FullName = r.FullName
if r.Empty { if r.Empty {
if cur.RID == "" { if cur.RID == "" {
cur.State, cur.Error = stWaiting, "" cur.State, cur.Error = stWaiting, ""
+5 -1
View File
@@ -43,6 +43,10 @@ func (m *mirror) run(timeout time.Duration, dir, name string, args ...string) (s
cmd.Stdout, cmd.Stderr = &stdout, &stderr cmd.Stdout, cmd.Stderr = &stdout, &stderr
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
msg := strings.TrimSpace(stderr.String()) msg := strings.TrimSpace(stderr.String())
if msg == "" {
// rad reports its errors on stdout.
msg = strings.TrimSpace(stdout.String())
}
if len(msg) > 500 { if len(msg) > 500 {
msg = "…" + msg[len(msg)-500:] msg = "…" + msg[len(msg)-500:]
} }
@@ -66,7 +70,7 @@ func (m *mirror) rad(dir string, args ...string) (string, error) {
func (m *mirror) radInit(dir string, r *repo) (string, error) { func (m *mirror) radInit(dir string, r *repo) (string, error) {
desc := r.Description desc := r.Description
if desc == "" { if desc == "" {
desc = "Mirrored from " + r.HTMLURL desc = "Mirrored from Gitea: " + r.FullName
} }
out, err := m.rad(dir, "init", dir, "--name", r.Name, "--description", desc, out, err := m.rad(dir, "init", dir, "--name", r.Name, "--description", desc,
"--default-branch", r.DefaultBranch, "--public", "--no-confirm") "--default-branch", r.DefaultBranch, "--public", "--no-confirm")
-1
View File
@@ -24,7 +24,6 @@ const (
type repoState struct { type repoState struct {
ID int64 `json:"id"` ID int64 `json:"id"`
FullName string `json:"full_name"` FullName string `json:"full_name"`
HTMLURL string `json:"html_url,omitempty"`
RID string `json:"rid,omitempty"` RID string `json:"rid,omitempty"`
DefaultBranch string `json:"default_branch,omitempty"` DefaultBranch string `json:"default_branch,omitempty"`
State string `json:"state"` State string `json:"state"`
+2 -1
View File
@@ -347,7 +347,8 @@ for IPFS peers. Details: <a href="https://github.com/Yundera/ipcr" target="_blan
repos.forEach(function (r) { repos.forEach(function (r) {
var tr = el("tr"); var tr = el("tr");
var td1 = el("td"); var td1 = el("td");
td1.appendChild(r.html_url ? link(r.html_url, r.full_name) : el("span", r.full_name)); // Not r.html_url: the bridge talks to Gitea's internal address, which is what Gitea puts there.
td1.appendChild(base ? link(url("gitea") + "/" + r.full_name, r.full_name) : el("span", r.full_name));
var td2 = el("td"); var td2 = el("td");
if (r.rid) { if (r.rid) {
td2.appendChild(base ? link(url("radicle") + "/nodes/radicle-api-" + base + "/" + r.rid, r.rid) : el("code", r.rid)); td2.appendChild(base ? link(url("radicle") + "/nodes/radicle-api-" + base + "/" + r.rid, r.rid) : el("code", r.rid));