3 Commits
Author SHA1 Message Date
Worph bef30d8e20 Docs: the forge is the AIO listing only; the split listing is retired (IPCR 1.7.0 is the registry beside a stock Gitea)
ipcr / image (push) Successful in 2m26s
2026-10-12 16:21:23 +00:00
Worph 17df335e3d Gitea workflow: build the forge image on the forge itself (runs-on: ipcr → staging → IPFS)
ipcr / image (push) Successful in 15m0s
2026-10-05 17:59:48 +00:00
Worph b508bd267b Admin page: the publisher key is created when IPCR starts (ipcr 1.6.0)
image / vet (push) Successful in 1m43s
image / image (push) Failing after 35s
2026-10-05 17:43:04 +00:00
4 changed files with 66 additions and 44 deletions
+42
View File
@@ -0,0 +1,42 @@
# IPCR Forge: builds the forge service's image (forge/Dockerfile) for every v* tag pushed to this repository's Gitea copy, and pushes it
# to the forge's staging registry; IPCR imports it from there within a minute and publishes it on
# IPFS under the forge's name (<forge>/ipcr/ipcr-forge, and ipcr-forge at the root).
#
# GitHub ignores .gitea/ (it runs .github/workflows/image.yml → ghcr.io), and Gitea ignores
# .github/ as soon as this folder exists: one repository, one workflow on each side.
#
# runs-on: ipcr the forge's own runner, the only one that reaches the staging registry
# localhost:5000 that registry, logged in to as this repository (IPCR_PUSH_TOKEN, a
# secret the forge sets on every repository allowed to publish)
# network=host so buildx's builder can reach it too
name: ipcr
on:
push:
tags: ['v*']
jobs:
image:
runs-on: ipcr
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: localhost:5000
username: ${{ github.repository }}
password: ${{ secrets.IPCR_PUSH_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: localhost:5000/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host
- uses: docker/build-push-action@v6
with:
context: forge
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+6 -8
View File
@@ -15,12 +15,10 @@ registry gateway, IPFS import, IPNS naming and admin API), used here as the imag
`ghcr.io/yundera/ipcr`. `ghcr.io/yundera/ipcr`.
- **[docs/forge.md](docs/forge.md)**: how it works, the trust model, findings, what was verified. - **[docs/forge.md](docs/forge.md)**: how it works, the trust model, findings, what was verified.
- **[Apps/IPCR-Forge](https://github.com/Yundera/ipcr/tree/main/Apps/IPCR-Forge)** and - **[Apps/IPCR-Forge-AIO](https://github.com/Yundera/ipcr/tree/main/Apps/IPCR-Forge-AIO)** (in the IPCR repository,
**[Apps/IPCR-Forge-AIO](https://github.com/Yundera/ipcr/tree/main/Apps/IPCR-Forge-AIO)** (in the IPCR repository, with the rest of the IPCR app store): the Yundera AppStore listing (compose, rationale, seed). It
with the rest of the IPCR app store): the Yundera AppStore listings (compose, rationale, seed). The bundles Gitea, Radicle and IPCR, and pins this repository's image and the engine's. See
first uses the store's Gitea app (and its Radicle app, if installed); the second bundles both. [docs/forge.md](docs/forge.md#one-listing-all-in-one).
Both pin this repository's image and the engine's. See
[docs/forge.md](docs/forge.md#two-listings-split-and-all-in-one).
- **[forge/](forge/)**: the forge service, `ghcr.io/yundera/ipcr-forge` (Go, standard library only): - **[forge/](forge/)**: the forge service, `ghcr.io/yundera/ipcr-forge` (Go, standard library only):
the Gitea → Radicle mirror, the forge's page and admin pages (Gitea login), the staging registry's the Gitea → Radicle mirror, the forge's page and admin pages (Gitea login), the staging registry's
gate and per-repository push credentials, and the install step. gate and per-repository push credentials, and the install step.
@@ -36,5 +34,5 @@ docker build -t ghcr.io/yundera/ipcr-forge:dev forge
Releases: a `vX.Y.Z` tag publishes `ghcr.io/yundera/ipcr-forge:X.Y.Z` Releases: a `vX.Y.Z` tag publishes `ghcr.io/yundera/ipcr-forge:X.Y.Z`
([.github/workflows/image.yml](.github/workflows/image.yml)). Then bump the version in the ([.github/workflows/image.yml](.github/workflows/image.yml)). Then bump the version in the
listings, `Apps/IPCR-Forge/docker-compose.yml` and `Apps/IPCR-Forge-AIO/docker-compose.yml` in listing, `Apps/IPCR-Forge-AIO/docker-compose.yml` in the IPCR repository (the service, `forge-gate`
the IPCR repository (the service, `forge-gate` and the `forge-setup` step, in each). and the `forge-setup` step).
+17 -35
View File
@@ -6,8 +6,7 @@ naming, the admin API). The engine knows nothing about Gitea, Radicle or this fo
the engine's image (`ghcr.io/yundera/ipcr`) and the contract listed in its the engine's image (`ghcr.io/yundera/ipcr`) and the contract listed in its
[design notes](https://github.com/Yundera/ipcr/blob/main/docs/design.md#contract-with-add-ons). [design notes](https://github.com/Yundera/ipcr/blob/main/docs/design.md#contract-with-add-ons).
How the forge (store listings: [Apps/IPCR-Forge](https://github.com/Yundera/ipcr/tree/main/Apps/IPCR-Forge) How the forge (store listing: [Apps/IPCR-Forge-AIO](https://github.com/Yundera/ipcr/tree/main/Apps/IPCR-Forge-AIO), in the IPCR repository)
and [Apps/IPCR-Forge-AIO](https://github.com/Yundera/ipcr/tree/main/Apps/IPCR-Forge-AIO), in the IPCR repository)
goes from `git push` to `docker pull`, why it is shaped this way, and what was learned building it. goes from `git push` to `docker pull`, why it is shaped this way, and what was learned building it.
The listings' own security argument is in The listings' own security argument is in
[rationale.md](https://github.com/Yundera/ipcr/blob/main/Apps/IPCR-Forge-AIO/rationale.md). [rationale.md](https://github.com/Yundera/ipcr/blob/main/Apps/IPCR-Forge-AIO/rationale.md).
@@ -62,39 +61,22 @@ anyone: docker pull ipcr.localhost:4767/ipns/<forge k51… | example.eth>/<owner
The two legs do not depend on each other: a repository can be built without being mirrored The two legs do not depend on each other: a repository can be built without being mirrored
(private repositories), and mirrored without being built (no workflow). (private repositories), and mirrored without being built (no workflow).
## Two listings: split and all in one ## One listing: all in one
The same image runs in both; only the compose differs. The forge ships as one listing, **IPCR-Forge-AIO**: Gitea, Radicle and IPCR bundled, with the forge's
own runner (labels `ipcr` and `ubuntu-latest`/`-24.04`/`-22.04`). It cannot sit beside the store's
Gitea, Radicle or IPCR apps (same container names, domains, ports).
| | **IPCR-Forge** (split) | **IPCR-Forge-AIO** (all in one) | A split listing (`Apps/IPCR-Forge`, the forge on top of the store's Gitea app and, optionally, its
| --- | --- | --- | Radicle app) existed until IPCR 1.7.0. It was retired: beside a stock Gitea, what is wanted is a
| Gitea | the store's **Gitea** app, required, reached at `gitea:3000` on `pcs` | bundled | registry, not a forge, and the IPCR app is now that registry, with its own push tokens, dashboard and
| Radicle | the store's **Radicle** app, optional, its home mounted from `/DATA/AppData/radicle/home` | bundled | public pulls, and no Gitea or Radicle knowledge
| Runner labels | `ipcr` only | `ipcr` and `ubuntu-latest`/`-24.04`/`-22.04` | ([push.md](https://github.com/Yundera/ipcr/blob/main/docs/push.md)). What it cost, for the record: the
| Runner registration token | the setup step, through Gitea's API (`RUNNER_DIR`) | an init step on Gitea's volume | Gitea admin password at install, a webhook allowance in the Gitea listing, a read-write mount of the
| Beside the Gitea/Radicle apps | yes, that is the point | no: same container names, domains, ports | Radicle app's home, and `rad` versions kept in step across two listings. Its code paths
(`RUNNER_DIR`, the missing-Gitea and missing-Radicle states) are still here, unused by the AIO
listing, until a cleanup.
Neither sits beside the IPCR app (the engine is bundled in both: its configuration is the forge's).
What the split one relies on, and how each part fails when missing:
- **Gitea**: the forge service calls `http://gitea:3000` and Gitea calls the forge's webhook at
`http://ipcr-forge:8081`, which the Gitea listing allows (`webhook.ALLOWED_HOST_LIST:
external,ipcr-forge`). The setup step signs in once as `FORGE_GITEA_ADMIN` / `FORGE_GITEA_PASSWORD`
(default `gitea_admin` and the server's default password). Gitea mints tokens only for a password,
never for another token, so these are what to change if the password was changed. Setup leaves
`secrets/setup-status` (`ok`, `unreachable`, `bad-credentials`), and the page shows a banner
for the last two. Setup exits cleanly in either case, so the install never hangs on a missing Gitea.
- **The runner**: the Gitea app has its own runner, `gitea-runner`, with the usual labels. Its jobs
run in its own daemon, where `localhost:5000` is nothing. So the forge's runner takes `ipcr` only,
and publishing workflows say `runs-on: ipcr` (the example does; it works in both listings).
- **Radicle**: the forge service needs the node's home itself (its key, its storage, its control
socket), not an API: Radicle's HTTP API is read-only. A repository is mirrored only when
`keys/radicle` and `node/control.sock` are there; otherwise it shows *waiting* and the page hides
the Radicle parts. The reconcile loop looks again every pass, so installing Radicle later starts
the mirror. `RADICLE=off` turns it off for good. The split listing creates
`/DATA/AppData/radicle/home` itself, owned by `$PUID`: left to Docker it would be created as
root, and the Radicle app's `rad auth` (as `$PUID`) could not write its key there later.
- **Versions**: the forge image carries the `rad` CLI at the version the Radicle listing pins - **Versions**: the forge image carries the `rad` CLI at the version the Radicle listing pins
(`RADICLE_VERSION` in [forge/Dockerfile](../forge/Dockerfile)). Bump both together. (`RADICLE_VERSION` in [forge/Dockerfile](../forge/Dockerfile)). Bump both together.
@@ -287,7 +269,7 @@ A repository opts in by committing a workflow under `.gitea/workflows/`. The tem
registry, the only one its token may push. `docker/metadata-action` lowercases it. registry, the only one its token may push. `docker/metadata-action` lowercases it.
- `context: .` and `driver-opts: network=host`: see findings 2 and 3. - `context: .` and `driver-opts: network=host`: see findings 2 and 3.
- `runs-on: ipcr`: the forge's own runner, the only one whose jobs reach the staging registry (see - `runs-on: ipcr`: the forge's own runner, the only one whose jobs reach the staging registry (see
[Two listings](#two-listings-split-and-all-in-one)). [One listing](#one-listing-all-in-one)).
- The image is published as `/ipns/<forge>/<owner>/<repo>:<tag>`. - The image is published as `/ipns/<forge>/<owner>/<repo>:<tag>`.
## Findings ## Findings
@@ -448,5 +430,5 @@ New in v3 (tested locally with Gitea 1.27.3 and rad 1.10.1, 2026-10-04):
- **act_runner** is pinned at 0.6.1, proven with Gitea 1.27.3. Upstream has moved on. - **act_runner** is pinned at 0.6.1, proven with Gitea 1.27.3. Upstream has moved on.
- **Radicle → Gitea:** patches opened on Radicle could become pull requests. Not built. - **Radicle → Gitea:** patches opened on Radicle could become pull requests. Not built.
- **Store install:** test through the store, not only as a hand install. - **Store install:** test through the store, not only as a hand install.
- **Split listing:** the store has no dependencies between apps; removing the Gitea app leaves the - **Split-listing code:** `RUNNER_DIR` and the missing-Gitea / missing-Radicle states served only the
forge reporting `unreachable` on its page, nothing more. retired split listing; remove them in a cleanup.
+1 -1
View File
@@ -332,7 +332,7 @@
st8.textContent = ""; st8.textContent = "";
publisher = s.key.id || ""; publisher = s.key.id || "";
$("key-name").textContent = s.key.name || "—"; $("key-name").textContent = s.key.name || "—";
$("key-id").textContent = publisher || "not created yet (the first import creates it)"; $("key-id").textContent = publisher || "not created yet (IPCR creates it when it starts: reload in a minute)";
$("ens-value").textContent = "ipns://" + (publisher || "k51…"); $("ens-value").textContent = "ipns://" + (publisher || "k51…");
$("dns-value").textContent = "dnslink=/ipns/" + (publisher || "k51…"); $("dns-value").textContent = "dnslink=/ipns/" + (publisher || "k51…");
pubName = s.name && s.name.ok ? s.name.name : ""; pubName = s.name && s.name.ok ? s.name.name : "";