.gitbay/wiki/Architecture/diagrams/diagrams.py

8dcfa45a8ac03a5ff9c36828274d05acadcf846c
gitbay/.gitbay/wiki/Architecture/diagrams/diagrams.py history · blame · raw

425 lines · 21285 bytes

  1#!/usr/bin/env python3
  2"""Emit the architecture package's SVG diagrams.
  3
  4Usage: python3 .gitbay/wiki/Architecture/diagrams/diagrams.py .gitbay/wiki/Architecture/diagrams
  5"""
  6import sys
  7from xml.sax.saxutils import escape as esc
  8
  9OUT = sys.argv[1]
 10
 11STYLE = """<style>
 12text{font-family:'Atkinson Hyperlegible Next',system-ui,-apple-system,'Segoe UI',sans-serif}
 13.bg{fill:#ffffff}
 14.t{fill:#1a1a1a;font-size:13px}
 15.tb1{fill:#1a1a1a;font-size:13px;font-weight:700}
 16.ts{fill:#4d4d4d;font-size:11px}
 17.th{fill:#1a1a1a;font-size:17px;font-weight:700}
 18.m{font-family:'Atkinson Hyperlegible Mono',ui-monospace,Menlo,monospace}
 19.gb{fill:#eaf0fd;stroke:#1f4fd1;stroke-width:1.4}
 20.ext{fill:#f3f3f3;stroke:#6b6b6b;stroke-width:1.2}
 21.act{fill:#ffffff;stroke:#1a1a1a;stroke-width:1.2}
 22.st{fill:#fff4e8;stroke:#9a3412;stroke-width:1.2}
 23.bad{fill:#fdecec;stroke:#b42318;stroke-width:1.2}
 24.ok{fill:#e8f5ec;stroke:#1a7f37;stroke-width:1.2}
 25.dec{fill:#ffffff;stroke:#1f4fd1;stroke-width:1.4;stroke-dasharray:5 3}
 26.host{fill:none;stroke:#6b6b6b;stroke-width:1.2;stroke-dasharray:3 3}
 27.zone{fill:none;stroke:#c2410c;stroke-width:1.6;stroke-dasharray:7 4}
 28.zl{fill:#c2410c;font-size:12px;font-weight:700}
 29.tag{fill:#c2410c;font-size:11px;font-weight:700}
 30.ln{stroke:#1a1a1a;stroke-width:1.2;fill:none}
 31.lnd{stroke:#6b6b6b;stroke-width:1.2;fill:none;stroke-dasharray:4 3}
 32.life{stroke:#9a9a9a;stroke-width:1;stroke-dasharray:3 4}
 33.ah{fill:#1a1a1a}
 34.ahd{fill:#6b6b6b}
 35@media (prefers-color-scheme: dark){
 36.bg{fill:#121212}
 37.t,.tb1,.th{fill:#ececec}
 38.ts{fill:#b0b0b0}
 39.gb{fill:#16233f;stroke:#7aa2ff}
 40.ext{fill:#1e1e1e;stroke:#8a8a8a}
 41.act{fill:#121212;stroke:#ececec}
 42.st{fill:#2a1a0e;stroke:#f0a36b}
 43.bad{fill:#2c1414;stroke:#f28b82}
 44.ok{fill:#122417;stroke:#6fcf8f}
 45.dec{fill:#121212;stroke:#7aa2ff}
 46.host{stroke:#8a8a8a}
 47.zone{stroke:#fb923c}
 48.zl,.tag{fill:#fb923c}
 49.ln{stroke:#ececec}
 50.lnd{stroke:#9a9a9a}
 51.life{stroke:#6a6a6a}
 52.ah{fill:#ececec}
 53.ahd{fill:#9a9a9a}
 54}
 55</style>
 56<defs>
 57<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ah"/></marker>
 58<marker id="ad" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ahd"/></marker>
 59</defs>"""
 60
 61
 62class SVG:
 63    def __init__(self, w, h, title, desc):
 64        self.w, self.h = w, h
 65        self.parts = [
 66            f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" width="{w}" height="{h}" role="img" aria-labelledby="t d">',
 67            f'<title id="t">{esc(title)}</title><desc id="d">{esc(desc)}</desc>',
 68            STYLE,
 69            f'<rect class="bg" x="0" y="0" width="{w}" height="{h}"/>',
 70            f'<text class="th" x="28" y="36">{esc(title)}</text>',
 71        ]
 72
 73    def text(self, x, y, s, cls="t", anchor="start"):
 74        self.parts.append(f'<text class="{cls}" x="{x}" y="{y}" text-anchor="{anchor}">{esc(s)}</text>')
 75
 76    def box(self, x, y, w, h, cls, title=None, lines=(), tcls="tb1", lcls="ts"):
 77        self.parts.append(f'<rect class="{cls}" x="{x}" y="{y}" width="{w}" height="{h}" rx="2"/>')
 78        n = (1 if title else 0) + len(lines)
 79        lh = 16
 80        cy = y + h / 2 - (n - 1) * lh / 2 + 4
 81        if title:
 82            self.text(x + w / 2, cy, title, tcls, "middle")
 83            cy += lh
 84        for ln in lines:
 85            self.text(x + w / 2, cy, ln, lcls, "middle")
 86            cy += lh
 87
 88    def rect(self, x, y, w, h, cls):
 89        self.parts.append(f'<rect class="{cls}" x="{x}" y="{y}" width="{w}" height="{h}" rx="2"/>')
 90
 91    def arrow(self, pts, cls="ln", both=False, label=None, lx=None, ly=None, lcls="ts", anchor="middle"):
 92        d = "M" + " L".join(f"{x},{y}" for x, y in pts)
 93        mk = "ad" if cls == "lnd" else "a"
 94        start = f' marker-start="url(#{mk})"' if both else ""
 95        self.parts.append(f'<path class="{cls}" d="{d}" marker-end="url(#{mk})"{start}/>')
 96        if label:
 97            if lx is None:
 98                (x1, y1), (x2, y2) = pts[0], pts[-1]
 99                lx, ly = (x1 + x2) / 2, (y1 + y2) / 2 - 5
100            self.text(lx, ly, label, lcls, anchor)
101
102    def line(self, x1, y1, x2, y2, cls):
103        self.parts.append(f'<line class="{cls}" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}"/>')
104
105    def zone(self, x, y, w, h, label):
106        self.rect(x, y, w, h, "zone")
107        self.text(x + 8, y + 16, label, "zl")
108
109    def legend(self, y, items):
110        x = 28
111        for cls, label in items:
112            self.parts.append(f'<rect class="{cls}" x="{x}" y="{y - 11}" width="18" height="14" rx="2"/>')
113            self.text(x + 24, y, label, "ts")
114            x += 34 + 6.2 * len(label)
115
116    def save(self, name):
117        self.parts.append("</svg>")
118        with open(f"{OUT}/{name}", "w") as f:
119            f.write("\n".join(self.parts) + "\n")
120
121
122LEGEND = [("gb", "gitbay"), ("act", "actor"), ("ext", "external or host"), ("st", "stored data"), ("bad", "untrusted or refused")]
123
124
125def context():
126    s = SVG(970, 590, "1. System context", "Actors and external systems around a gitbay instance.")
127    actors = [
128        ("Anonymous visitor", "HTTPS · git:// if enabled"),
129        ("User: CLI or OpenSSH", "SSH :22 · public key"),
130        ("User: browser", "HTTPS :443 · session cookie"),
131        ("iOS app", "HTTPS API · bearer token"),
132        ("CI runner", "SSH :22 · runner-scoped key"),
133    ]
134    tops = [70, 140, 210, 280, 350]
135    targets = [125, 170, 215, 260, 305]
136    for (t, sub), y, ty in zip(actors, tops, targets):
137        s.box(30, y, 200, 48, "act", t, [sub])
138        s.arrow([(230, y + 24), (425, ty)])
139    s.box(30, 450, 200, 48, "act", "Operator", ["SSH :2222 · root"])
140    s.arrow([(230, 474), (425, 474)])
141
142    s.rect(400, 60, 290, 470, "host")
143    s.text(412, 80, "Host (Linux, systemd)", "ts")
144    s.rect(425, 100, 240, 300, "gb")
145    s.text(545, 132, "gitbayd", "tb1", "middle")
146    for i, ln in enumerate(["SSH · HTTPS · git hooks", "command registry and policy", "workers: mail, push,", "webhooks, mirrors, CI"]):
147        s.text(545, 154 + i * 16, ln, "ts", "middle")
148    s.box(445, 250, 200, 52, "st", "SQLite · repositories · LFS")
149    s.box(445, 322, 200, 52, "ext", "git subprocesses")
150    s.box(425, 450, 240, 48, "ext", "operator sshd :2222", ["keys only · fail2ban"])
151
152    ext = [
153        ("ACME CA", "TLS certificates"),
154        ("SMTP relay", "mail · STARTTLS if offered"),
155        ("Apple Push (APNs)", "iOS notifications"),
156        ("Webhook endpoints", "HMAC-signed POSTs"),
157        ("Mirror remotes", "push and pull mirrors"),
158        ("Package registries", "dependency checks, opt-in"),
159    ]
160    etops = [70, 135, 200, 265, 330, 395]
161    sources = [120, 160, 200, 240, 280, 320]
162    for (t, sub), y, sy in zip(ext, etops, sources):
163        s.box(750, y, 200, 48, "ext", t, [sub])
164        s.arrow([(665, sy), (750, y + 24)], both=(t == "Mirror remotes"))
165    s.box(750, 470, 200, 48, "ext", "Offsite object storage", ["restic · append-only key"])
166    s.arrow([(690, 494), (750, 494)], cls="lnd")
167    s.legend(570, LEGEND)
168    s.save("01-context.svg")
169
170
171def components():
172    s = SVG(970, 680, "2. Components inside gitbayd", "Packages of the gitbayd daemon and how requests move between them.")
173    s.box(40, 70, 250, 64, "gb", "internal/sshd", ["SSH :22 · key auth · exec · git transport"])
174    s.box(310, 70, 330, 64, "gb", "internal/httpd", ["web · JSON API · smart HTTP (fetch) · LFS"])
175    s.box(660, 70, 270, 64, "gb", "internal/gitd", ["git:// · upload-pack · off by default"])
176    s.box(40, 190, 600, 80, "gb", "internal/control: the command registry",
177          ["Dispatch: scope · read-only · disabled · admin · pending · write budget", "stdin gating · handler · audit of successful writes"])
178    s.box(660, 190, 270, 80, "gb", "internal/policy", ["CanRead / CanWrite / CanAdmin", "key scopes · CheckPush · CODEOWNERS"])
179    s.box(40, 320, 180, 70, "gb", "internal/hookd", ["pre- and post-receive", "decisions"])
180    s.box(240, 320, 190, 70, "gb", "internal/gitutil", ["git as a subprocess", "argv only, no shell"])
181    s.box(450, 320, 190, 70, "gb", "internal/sig", ["OpenPGP and SSHSIG", "verification only"])
182    s.box(660, 320, 270, 70, "gb", "internal/store", ["SQLite · hand-written SQL", "59 migrations"])
183    s.box(40, 440, 600, 70, "gb", "background workers",
184          ["webhook delivery · mail · APNs · mirrors", "CI scheduler and stale-build reaper · dependency checks · retention sweep"])
185    s.box(800, 440, 130, 70, "gb", "internal/lfs", ["content-addressed", "HMAC tokens"])
186    s.box(40, 570, 180, 50, "st", "hook.sock", ["unix socket"])
187    s.box(240, 570, 190, 50, "st", "repos/*.git", ["bare repositories"])
188    s.box(660, 570, 120, 50, "st", "gitbay.db", ["SQLite, 0640"])
189    s.box(800, 570, 130, 50, "st", "lfs/", ["objects"])
190
191    s.arrow([(165, 134), (165, 190)])
192    s.arrow([(475, 134), (475, 190)])
193    s.arrow([(700, 134), (610, 190)])
194    s.arrow([(640, 230), (660, 230)])
195    s.arrow([(335, 270), (335, 320)], label="git transport", lx=342, ly=300, anchor="start")
196    s.arrow([(545, 270), (545, 320)])
197    s.arrow([(600, 270), (700, 320)])
198    s.arrow([(130, 320), (130, 270)], label="decision request", lx=137, ly=300, anchor="start")
199    s.arrow([(560, 440), (700, 390)])
200    s.arrow([(335, 390), (335, 570)])
201    s.arrow([(240, 595), (220, 595)])
202    s.text(230, 560, "hooks", "ts", "middle")
203    s.arrow([(120, 570), (120, 390)])
204    s.arrow([(720, 390), (720, 570)])
205    s.arrow([(865, 510), (865, 570)])
206    s.legend(660, [("gb", "gitbayd package"), ("st", "on-disk state")])
207    s.save("02-components.svg")
208
209
210def deployment():
211    s = SVG(970, 640, "3. Deployment (reference host)", "Processes, users, ports and files on the single gitbay host.")
212    s.rect(20, 80, 180, 470, "ext")
213    s.text(110, 110, "Internet", "tb1", "middle")
214    for i, ln in enumerate(["clients: SSH, HTTPS", "ACME CA", "SMTP relay", "APNs", "webhook endpoints", "mirror remotes", "package registries", "offsite object storage"]):
215        s.text(110, 140 + i * 22, ln, "ts", "middle")
216
217    s.rect(240, 60, 710, 520, "host")
218    s.text(252, 80, "Host: Ubuntu 24.04 · ufw inbound 22, 80, 443, 2222 · outbound open", "ts")
219    s.box(280, 100, 360, 200, "gb", "gitbayd.service (user gitbay)",
220          [":22 SSH · :443 HTTPS · :80 ACME and redirect", "hook.sock (unix)", "ProtectSystem=strict · NoNewPrivileges", "CAP_NET_BIND_SERVICE only · SystemCallFilter", "MemoryDenyWriteExecute · PrivateTmp"])
221    s.box(680, 100, 250, 200, "st", "/var/lib/gitbay (0750)",
222          ["gitbay.db (0640)", "repos/ · lfs/ · hooks/", "ssh/host_ed25519 (0600)", "acme/", "/etc/gitbay/config.toml (0640)", "/var/backups/gitbay (0750)"])
223    s.box(280, 340, 360, 100, "gb", "gitbay-runner.service (user ci-runner)",
224          ["polls git@127.0.0.1 over SSH with a runner key", "MemoryMax 6G · CPUQuota 300% · Delegate=yes"])
225    s.box(300, 470, 320, 80, "bad", "CI containers (rootless podman)",
226          ["untrusted steps · --pull=never", "build home per repository, read-write"])
227    s.box(680, 340, 250, 100, "ext", "timers (user gitbay)",
228          ["backup nightly · database hourly", "git gc weekly · monitor hourly", "restic to offsite storage"])
229    s.box(680, 470, 250, 80, "ext", "operator sshd :2222", ["keys only · fail2ban"])
230
231    s.arrow([(200, 170), (280, 170)], label=":22 :443 :80", lx=240, ly=163)
232    s.arrow([(280, 260), (200, 260)], label="outbound", lx=240, ly=276)
233    s.arrow([(640, 200), (680, 200)])
234    s.arrow([(460, 340), (460, 300)], label="SSH", lx=468, ly=324, anchor="start")
235    s.arrow([(460, 440), (460, 470)])
236    s.arrow([(805, 340), (805, 300)])
237    s.arrow([(300, 520), (200, 500)], cls="lnd", label="egress open", lx=250, ly=530)
238    s.arrow([(200, 540), (255, 566), (805, 566), (805, 550)], label="SSH :2222", lx=530, ly=560)
239    s.legend(620, [("gb", "gitbay unit"), ("st", "files"), ("ext", "host service"), ("bad", "untrusted code")])
240    s.save("03-deployment.svg")
241
242
243def trust():
244    s = SVG(970, 650, "4. Trust boundaries", "Zones Z0 to Z6 and the boundaries TB1 to TB10 that data crosses between them.")
245    s.zone(20, 60, 190, 560, "Z0 Internet (untrusted)")
246    s.zone(250, 60, 380, 310, "Z1 gitbayd")
247    s.zone(660, 60, 290, 270, "Z2 Local state")
248    s.zone(250, 400, 380, 110, "Z3 git and hooks")
249    s.zone(660, 400, 290, 220, "Z4 Runner")
250    s.zone(675, 500, 260, 110, "Z5 Containers")
251    s.zone(250, 540, 380, 80, "Z6 Operator")
252
253    ents = [("Visitor", 90), ("User over SSH", 170), ("Browser", 250), ("API client, iOS", 330)]
254    for name, y in ents:
255        s.box(35, y, 160, 50, "act", name)
256    s.box(35, 500, 160, 60, "ext", "Webhook and", ["mirror endpoints"])
257
258    s.box(270, 95, 150, 55, "gb", "sshd", ["key auth"])
259    s.box(270, 200, 150, 60, "gb", "httpd", ["cookie · token · CSP"])
260    s.box(270, 300, 150, 50, "gb", "workers")
261    s.box(450, 130, 160, 110, "gb", "Dispatch", ["handler", "resolveRepo", "policy"])
262
263    s.box(680, 95, 250, 55, "st", "SQLite", ["token hashes · secrets in clear"])
264    s.box(680, 170, 250, 55, "st", "repositories · LFS")
265    s.box(680, 245, 250, 55, "st", "host key · ACME · config")
266
267    s.box(270, 430, 150, 60, "ext", "git receive-pack", ["upload-pack"])
268    s.box(450, 430, 160, 60, "ext", "gitbayd hook", ["to hook.sock"])
269    s.box(680, 430, 250, 50, "gb", "gitbay-runner")
270    s.box(690, 530, 230, 55, "bad", "build steps", ["repository and fork code"])
271    s.box(270, 565, 340, 40, "ext", "root shell: outside every in-app control")
272
273    s.arrow([(195, 195), (270, 122)]); s.text(232, 150, "TB1", "tag", "middle")
274    s.arrow([(195, 115), (270, 215)]); s.arrow([(195, 275), (270, 232)], both=True); s.arrow([(195, 355), (270, 250)])
275    s.text(232, 300, "TB2 · TB9", "tag", "middle")
276    s.arrow([(420, 122), (450, 160)]); s.arrow([(420, 230), (450, 215)]); s.text(435, 190, "TB3", "tag", "middle")
277    s.arrow([(610, 160), (680, 122)]); s.arrow([(610, 200), (680, 197)])
278    s.arrow([(480, 240), (400, 430)]); s.text(455, 330, "TB4", "tag", "end")
279    s.arrow([(420, 460), (450, 460)])
280    s.arrow([(560, 430), (560, 240)]); s.text(566, 330, "TB5", "tag")
281    s.arrow([(680, 450), (610, 240)], both=True); s.text(648, 320, "TB6", "tag")
282    s.arrow([(805, 480), (805, 530)]); s.text(812, 510, "TB7", "tag")
283    s.arrow([(270, 325), (195, 520)]); s.text(226, 460, "TB8", "tag", "middle")
284    s.arrow([(690, 545), (645, 525), (195, 525)], cls="lnd", label="egress open", lx=420, ly=520)
285    s.text(620, 596, "TB10", "tag", "end")
286    s.legend(640, [("act", "external actor"), ("gb", "gitbay process"), ("st", "stored data"), ("bad", "untrusted code")])
287    s.save("04-trust-boundaries.svg")
288
289
290def authz():
291    s = SVG(970, 860, "5. Authorization decision", "How a request is authorised: Dispatch gates, then repository resolution with a policy predicate, and the git transport path.")
292    x, w = 40, 380
293    dx, dw = 470, 190
294    s.box(x, 60, w, 44, "act", "Credential", ["SSH key · API token · session cookie"])
295    steps = [
296        (124, "gb", "Resolve account and scope", None),
297        (188, "dec", "Scope allows this command?", ("no", "denied (exit 4)")),
298        (252, "dec", "Account disabled?", ("yes", "denied (exit 4)")),
299        (316, "dec", "admin command and not an admin?", ("yes", "denied (exit 4)")),
300        (380, "dec", "Pending account, command not allowed?", ("yes", "denied (exit 4)")),
301        (444, "dec", "Write budget exhausted?", ("yes", "refused, try later")),
302        (508, "gb", "Handler: resolveRepo(path, predicate)", None),
303        (572, "dec", "Repository exists?", ("no", "not found (exit 3)")),
304        (636, "dec", "Predicate passes? (CanRead / CanWrite / CanAdmin)", None),
305        (716, "dec", "Caller can read it?", None),
306    ]
307    prev = 104
308    for y, cls, title, deny in steps:
309        s.arrow([(x + w / 2, prev), (x + w / 2, y)], label=("no" if y == 716 else None), lx=x + w / 2 + 8, ly=y - 14, anchor="start")
310        s.box(x, y, w, 44, cls, title)
311        if deny:
312            s.arrow([(x + w, y + 22), (dx, y + 22)], label=deny[0], lx=x + w + 22, ly=y + 16)
313            s.box(dx, y + 4, dw, 36, "bad", deny[1])
314        prev = y + 44
315    s.arrow([(x + w, 658), (dx, 658)], label="yes", lx=x + w + 22, ly=652)
316    s.box(dx, 638, dw, 40, "ok", "run · audit if it wrote")
317    s.arrow([(x + w, 728), (dx, 716)], label="no", lx=x + w + 20, ly=716)
318    s.box(dx, 698, dw, 34, "bad", "not found (exit 3)")
319    s.arrow([(x + w, 748), (dx, 760)], label="yes", lx=x + w + 20, ly=768)
320    s.box(dx, 744, dw, 34, "bad", "permission denied (exit 4)")
321
322    gx, gw = 690, 250
323    s.text(gx, 76, "git transport (runGit)", "tb1")
324    gsteps = [
325        ("dec", "Deploy key?", "yes: only its repository and mode"),
326        ("dec", "CanRead?", "no: not found"),
327        ("dec", "Key scope allows git?", "runner: read only · else denied"),
328        ("dec", "Push: CanWrite?", "no: denied"),
329        ("dec", "Archived, pull mirror, quota", "refused"),
330        ("gb", "pre-receive: CheckPush", "protected · require-mr · tags"),
331        ("gb", "require-signed", "verify each incoming commit"),
332        ("ok", "git applies the ref updates", None),
333    ]
334    y = 92
335    for i, (cls, title, note) in enumerate(gsteps):
336        if i:
337            s.arrow([(gx + gw / 2, y - 18), (gx + gw / 2, y)])
338        s.box(gx, y, gw, 52, cls, title, [note] if note else [])
339        y += 70
340    s.text(gx, 680, "Merges by the server skip the hooks:", "ts")
341    s.text(gx, 696, "MergeGates decides them, and require-signed", "ts")
342    s.text(gx, 712, "allows only fast-forward merges, so the", "ts")
343    s.text(gx, 728, "server never writes an unsigned commit.", "ts")
344    s.legend(835, [("dec", "check"), ("gb", "step"), ("bad", "refusal"), ("ok", "allowed")])
345    s.save("05-authorization.svg")
346
347
348def sequence(name, title, desc, parts, msgs, h):
349    s = SVG(970, h, title, desc)
350    xs = [p[0] for p in parts]
351    for x, label, cls in parts:
352        s.box(x - 75, 56, 150, 40, cls, label)
353        s.line(x, 96, x, h - 30, "life")
354    y = 130
355    for m in msgs:
356        a, b, text = m[0], m[1], m[2]
357        style = m[3] if len(m) > 3 else "ln"
358        if a == b:
359            x = xs[a]
360            s.arrow([(x, y - 8), (x + 30, y - 8), (x + 30, y + 6), (x + 4, y + 6)], cls=style)
361            tw = 5.9 * len(text) + 8
362            s.parts.append(f'<rect class="bg" x="{x + 33}" y="{y - 10}" width="{tw}" height="15"/>')
363            s.text(x + 36, y + 2, text, "ts")
364        else:
365            x1, x2 = xs[a], xs[b]
366            s.arrow([(x1, y), (x2 - 2 if x2 > x1 else x2 + 2, y)], cls=style)
367            tw = 5.9 * len(text) + 8
368            cx = (x1 + x2) / 2
369            s.parts.append(f'<rect class="bg" x="{cx - tw / 2}" y="{y - 19}" width="{tw}" height="15"/>')
370            s.text(cx, y - 7, text, "ts", "middle")
371        y += 38
372    s.save(name)
373
374
375def push():
376    parts = [(90, "Client", "act"), (250, "sshd · runGit", "gb"), (410, "git receive-pack", "ext"),
377             (570, "gitbayd hook", "ext"), (730, "hookd", "gb"), (885, "policy · sig · store", "gb")]
378    msgs = [
379        (0, 1, "exec git-receive-pack 'owner/repo'"),
380        (1, 5, "resolve repository · access · scope · quota"),
381        (1, 2, "spawn with hook socket, repo, user, scope"),
382        (0, 2, "pack data"),
383        (2, 3, "pre-receive: ref updates on stdin"),
384        (3, 4, "request over hook.sock"),
385        (4, 5, "CheckPush: protected · require-mr · tags"),
386        (4, 3, "need commits (if require-signed)", "lnd"),
387        (3, 4, "raw commit objects", "lnd"),
388        (4, 5, "VerifyCommit for each", "lnd"),
389        (4, 3, "allow, or refuse with a message"),
390        (3, 2, "exit 0 or 1"),
391        (2, 3, "post-receive"),
392        (3, 4, "post-receive request"),
393        (4, 5, "events · CI queue · mirrors · MR heads · Closes #N"),
394        (2, 0, "result"),
395    ]
396    sequence("06-push-flow.svg", "6. git push over SSH", "Sequence of a push: SSH checks, pre-receive decision by the daemon, optional signature verification, post-receive side effects.", parts, msgs, 780)
397
398
399def ci():
400    parts = [(90, "Pusher", "act"), (280, "gitbayd", "gb"), (470, "store", "gb"), (660, "gitbay-runner", "gb"), (860, "container", "bad")]
401    msgs = [
402        (0, 1, "push (post-receive)"),
403        (1, 2, "queueJobs: ci/<job> pending, skipped, or reused (same tree)"),
404        (3, 1, "runner next [--untrusted] · runner key, attached repositories"),
405        (1, 2, "ClaimBuild: trusted builds unless --untrusted"),
406        (1, 3, "claim: steps, image, secrets only if trusted"),
407        (3, 1, "clone over SSH (read only)"),
408        (3, 4, "podman run --pull=never · env file 0600 · build home rw"),
409        (3, 4, "podman exec sh -c <step>, for each step"),
410        (3, 1, "runner log <id>: streamed output"),
411        (1, 2, "append log · a cancel ends the stream"),
412        (3, 1, "runner done <id> success|failure"),
413        (1, 2, "status ci/<job> · event · failure mail"),
414        (2, 2, "scheduler reaps: log closed > 2 min, or started > 90 min"),
415    ]
416    sequence("07-ci-flow.svg", "7. CI build", "Sequence of a CI build from push to result, including the claim and where secrets travel.", parts, msgs, 660)
417
418
419context()
420components()
421deployment()
422trust()
423authz()
424push()
425ci()