Rename the bridge to the forge service; this repository is the add-on
- bridge/ → forge/, module and binary ipcr-forge, image ghcr.io/yundera/ipcr-forge
(was ipcr-forge-bridge); releases are plain vX.Y.Z tags (workflow image.yml)
- data folders $AppID/bridge/{state,secrets} → $AppID/forge/..., in-container /forge
- docs link to the engine (Yundera/ipcr) instead of relative paths; README
This commit is contained in:
+8
-8
@@ -1,10 +1,10 @@
|
||||
# ipcr-forge-bridge: the Gitea → Radicle mirror and IPCR Forge's page. Our Go binary, plus git and
|
||||
# ipcr-forge: the Gitea → Radicle mirror and IPCR Forge's page. Our Go binary, plus git and
|
||||
# the stock Radicle CLI (`rad`, `git-remote-rad`) at the version the forge's node runs.
|
||||
# Build context: this folder (bridge/).
|
||||
# Build context: this folder (forge/).
|
||||
FROM golang:1.24-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/ipcr-forge-bridge .
|
||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/ipcr-forge .
|
||||
|
||||
# Radicle's official static (musl) release, checked against its published digest.
|
||||
FROM alpine:3.22 AS radicle
|
||||
@@ -21,17 +21,17 @@ RUN apk add --no-cache xz \
|
||||
|
||||
FROM alpine:3.22
|
||||
ARG VERSION=dev
|
||||
LABEL org.opencontainers.image.title="ipcr-forge-bridge" \
|
||||
LABEL org.opencontainers.image.title="ipcr-forge" \
|
||||
org.opencontainers.image.description="IPCR Forge: mirrors public Gitea repositories to Radicle, serves the forge's page" \
|
||||
org.opencontainers.image.source="https://github.com/Yundera/ipcr" \
|
||||
org.opencontainers.image.source="https://github.com/Yundera/ipcr-forge" \
|
||||
org.opencontainers.image.version="$VERSION"
|
||||
RUN apk add --no-cache ca-certificates git \
|
||||
# Mirrors and the node's storage belong to the service's uid, but a bind mount can make git see
|
||||
# another owner ("dubious ownership"); the storage is the node's own, so trust it.
|
||||
&& git config --system safe.directory '*'
|
||||
COPY --from=radicle /out/ /usr/local/bin/
|
||||
COPY --from=build /out/ipcr-forge-bridge /usr/local/bin/ipcr-forge-bridge
|
||||
COPY --from=build /out/ipcr-forge /usr/local/bin/ipcr-forge
|
||||
ENV RAD_HOME=/radicle-home \
|
||||
HOME=/tmp
|
||||
# No entrypoint: the install step runs `sh -c '… ipcr-forge-bridge setup'` in this image.
|
||||
CMD ["ipcr-forge-bridge", "serve"]
|
||||
# No entrypoint: the install step runs `sh -c '… ipcr-forge setup'` in this image.
|
||||
CMD ["ipcr-forge", "serve"]
|
||||
|
||||
+3
-3
@@ -17,12 +17,12 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// The gate is the staging registry's front door (`ipcr-forge-bridge gate`). CI jobs push to
|
||||
// The gate is the staging registry's front door (`ipcr-forge gate`). CI jobs push to
|
||||
// localhost:5000 and IPCR imports from it; before the gate, anyone who could run a job could push
|
||||
// any image name, and IPCR published what it found. Now:
|
||||
//
|
||||
// - a job logs in as its repository: user <owner>/<repo>, password the IPCR_PUSH_TOKEN secret the
|
||||
// bridge set on that repository (pushcreds.go). It may read and write that repository's
|
||||
// forge service set on that repository (pushcreds.go). It may read and write that repository's
|
||||
// manifests, blobs and tags, and nothing else: no other name, no catalog, no delete, no
|
||||
// cross-repository blob mount;
|
||||
// - IPCR logs in as the importer (the credential it generated itself): read everything, list the
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
// Environment: GATE_LISTEN (default :5000), GATE_UPSTREAM (default
|
||||
// unix:///run/staging/registry.sock), GATE_CREDENTIALS (default /gate/credentials.json).
|
||||
|
||||
// gateCreds is credentials.json, written by the bridge: SHA-256 (hex) of each password.
|
||||
// gateCreds is credentials.json, written by the forge service: SHA-256 (hex) of each password.
|
||||
type gateCreds struct {
|
||||
Push map[string]string `json:"push"` // "<owner>/<repo>" (lowercase) → hash
|
||||
Importer map[string]string `json:"importer"` // user → hash
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
module ipcr-forge-bridge
|
||||
module ipcr-forge
|
||||
|
||||
go 1.24
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ package main
|
||||
// and exports it (`ipfs key export`, libp2p-protobuf-cleartext). The additional data binds name and
|
||||
// id to the ciphertext, so neither can be edited. Byte slices are base64 (encoding/json's default).
|
||||
//
|
||||
// A copy of gateway/keybackup.go (ipcrd), without its CLI: the bridge writes backups (it can read
|
||||
// A copy of gateway/keybackup.go (ipcrd), without its CLI: the forge service writes backups (it can read
|
||||
// Kubo's keystore; ipcrd cannot) and ipcrd reads them, so the cleartext key never crosses the
|
||||
// network. Keep the two identical; both test the same vector.
|
||||
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
// ipcr-forge-bridge — the glue of IPCR Forge: Gitea is where people work, Radicle is where the code
|
||||
// ipcr-forge — the glue of IPCR Forge: Gitea is where people work, Radicle is where the code
|
||||
// is replicated, and this service keeps the second following the first. One way only.
|
||||
//
|
||||
// serve (default) webhook receiver + mirror worker + the forge's page
|
||||
@@ -21,7 +21,7 @@
|
||||
// GITEA_OWNER the admin account (default gitea_admin)
|
||||
// GITEA_PASSWORD setup only: its password, used once to mint the token and the hook
|
||||
// SECRETS_DIR gitea-token and hook-secret (default /secrets)
|
||||
// STATE_DIR repos.json and the bare mirrors (default /bridge)
|
||||
// STATE_DIR repos.json and the bare mirrors (default /forge)
|
||||
// RAD_HOME the node's Radicle home (default /radicle-home)
|
||||
// WEB_LISTEN the page (default :8080)
|
||||
// HOOK_LISTEN the webhook, internal only (default :8081)
|
||||
@@ -42,7 +42,7 @@
|
||||
// IMPORT_PUBLISHER the publisher key's name (default forge)
|
||||
// IPCR_ROOT_ORG organisation published at the root of the forge's name, until changed in
|
||||
// the admin page (default: none)
|
||||
// GATE_CREDENTIALS the gate's credentials.json (default /bridge/gate/credentials.json)
|
||||
// GATE_CREDENTIALS the gate's credentials.json (default /forge/gate/credentials.json)
|
||||
// IPCR_STAGING_AUTH IPCR's own staging credential (default /srv/ipcr-state/staging-auth)
|
||||
package main
|
||||
|
||||
@@ -104,7 +104,7 @@ func serve() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("RECONCILE: %w", err)
|
||||
}
|
||||
stateDir := env("STATE_DIR", "/bridge")
|
||||
stateDir := env("STATE_DIR", "/forge")
|
||||
st, err := loadState(stateDir + "/repos.json")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -166,11 +166,11 @@ func serve() error {
|
||||
log.Printf("allowlist: %v", err)
|
||||
}
|
||||
}
|
||||
log.Printf("bridge: admin pages on, IPCR admin API %s", ipcr)
|
||||
log.Printf("forge: admin pages on, IPCR admin API %s", ipcr)
|
||||
}
|
||||
go reconcileLoop(m, q, every)
|
||||
|
||||
log.Printf("bridge: %s → Radicle (%s), reconcile every %s", m.gitea.base, m.radHome, every)
|
||||
log.Printf("forge: %s → Radicle (%s), reconcile every %s", m.gitea.base, m.radHome, every)
|
||||
return http.ListenAndServe(env("WEB_LISTEN", ":8080"), mux)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import (
|
||||
var exampleFiles embed.FS
|
||||
|
||||
const (
|
||||
tokenName = "ipcr-forge-bridge"
|
||||
tokenName = "ipcr-forge"
|
||||
adminTokenName = "ipcr-forge-secrets"
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ const (
|
||||
stFrozen = "frozen" // no longer public in Gitea (or deleted): no further sync, nothing retracted
|
||||
)
|
||||
|
||||
// repoState is what the bridge remembers per Gitea repository, keyed by Gitea's numeric ID so a
|
||||
// repoState is what the forge service remembers per Gitea repository, keyed by Gitea's numeric ID so a
|
||||
// rename or a transfer keeps its Radicle ID.
|
||||
type repoState struct {
|
||||
ID int64 `json:"id"`
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>IPCR Forge admin</title>
|
||||
<link rel="icon" href="icon.png">
|
||||
<!--
|
||||
The forge's admin page: Gitea site administrators only (the bridge checks the session for the
|
||||
page and for every API call). Everything goes through /admin/api/: the bridge's own endpoints
|
||||
The forge's admin page: Gitea site administrators only (the forge service checks the session for the
|
||||
page and for every API call). Everything goes through /admin/api/: the forge service's own endpoints
|
||||
(repositories, key export) and IPCR's admin API behind /admin/api/ipcr/.
|
||||
-->
|
||||
<style>
|
||||
@@ -233,7 +233,7 @@
|
||||
var t = $("toast"); t.textContent = msg; t.style.display = "block";
|
||||
clearTimeout(toast.t); toast.t = setTimeout(function () { t.style.display = "none"; }, ms || 4000);
|
||||
}
|
||||
// Every call: same origin, with the header the bridge requires on writes.
|
||||
// Every call: same origin, with the header the forge service requires on writes.
|
||||
function api(method, path, body) {
|
||||
var opt = { method: method, headers: { "X-IPCR-Admin": "1" }, credentials: "same-origin", cache: "no-store" };
|
||||
if (body !== undefined) { opt.headers["Content-Type"] = "application/json"; opt.body = JSON.stringify(body); }
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<title>IPCR Forge</title>
|
||||
<link rel="icon" href="icon.png">
|
||||
<!--
|
||||
Served by the forge's bridge (ipcr-forge-bridge, embedded in its binary). Every address is
|
||||
Served by the forge service (ipcr-forge, embedded in its binary). Every address is
|
||||
derived in the browser from this page's own hostname (ipcr-forge-<domain> → gitea-<domain>,
|
||||
radicle-<domain>, radicle-api-<domain>), so the nip.io / sslip.io aliases work too.
|
||||
Live data: /images.json (IPCR's published.json), /repos.json (the bridge's Gitea → Radicle
|
||||
Live data: /images.json (IPCR's published.json), /repos.json (the forge service's Gitea → Radicle
|
||||
mirror state) and the Radicle API's node info.
|
||||
-->
|
||||
<style>
|
||||
@@ -363,7 +363,7 @@ for IPFS peers. Details: <a href="https://github.com/Yundera/ipcr" target="_blan
|
||||
imagesBody.appendChild(el("p", "Nothing published yet. Tag v1.0.0 on the example repository (above), or push a v* tag from a repository with a workflow; its image appears here a few minutes later.", "muted"));
|
||||
});
|
||||
|
||||
// Radicle mirrors: the bridge's state.
|
||||
// Radicle mirrors: the forge service's state.
|
||||
var reposBody = document.getElementById("repos-body");
|
||||
fetch("repos.json", { cache: "no-store" })
|
||||
.then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); })
|
||||
@@ -374,7 +374,7 @@ for IPFS peers. Details: <a href="https://github.com/Yundera/ipcr" target="_blan
|
||||
repos.forEach(function (r) {
|
||||
var tr = el("tr");
|
||||
var td1 = el("td");
|
||||
// Not r.html_url: the bridge talks to Gitea's internal address, which is what Gitea puts there.
|
||||
// Not r.html_url: the forge service 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");
|
||||
if (r.rid) {
|
||||
|
||||
Reference in New Issue
Block a user