neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
Bring PROGRESS and ROADMAP current: slices 60–71
2782d61 · Gabriel Morell · 2026-09-11 16:40
Message
{commit_body(@commit)}
Files changed
modified
PROGRESS.md
+180
−0
@@ -12,6 +12,150 @@ intermediate "I was here when I stopped" state) lives in the
| 12 | 12 | |
| 13 | 13 | --- |
| 14 | 14 | |
| 15 | +## Slice 71 — Org dashboard | |
| 16 | + | |
| 17 | +`/orgs/:handle/dashboard` — the signed-in dashboard narrowed to one organization. | |
| 18 | + | |
| 19 | +- **Reframed, not just filtered.** The landing dashboard is about *you*, so it only shows work you opened. An org dashboard is about the org, so open PRs and issues from everyone appear alongside a separate "Your work here" panel. Without that it would be the landing page with fewer rows on it, which isn't worth a route. | |
| 20 | +- **Scoped by `Repositories.visible_repo_ids_for_org/2`**, so every panel inherits the org's visibility rules. The review queue intersects "repos you maintain" with "repos in this org" rather than leaking a maintainer queue from elsewhere. | |
| 21 | +- **`GitGudWeb.DashboardComponents`** — panels, work/CI/repo rows and the display helpers, extracted from `PageLive.Home` and shared. The two dashboards differ in what they query; they should never differ in how a row of work looks. | |
| 22 | +- **`PullRequests.list_open_in_repos/2` + `Issues.list_open_in_repos/2`** do no visibility widening of their own, unlike `list_open_authored_by/3` — the caller has already decided what the viewer may see, which is what makes it safe to show other people's work. `list_open_review_requests/2` gained `:repo_ids` for the same reason. | |
| 23 | +- Two test-driven corrections in the page rather than the test: an unreachable "org has no repositories" empty state (every org is provisioned a public profile repo on creation), and a "New repository" assertion that could never fail because the app layout carries that text for every signed-in visitor. | |
| 24 | + | |
| 25 | +--- | |
| 26 | + | |
| 27 | +## Slice 70 — Private package registries (PyPI, Hex, Cargo, apt) | |
| 28 | + | |
| 29 | +Four package registries served by the forge, plus the OCI one already in zot. Planned in `docs/package-registries.md` and landed in the five phases it set out. | |
| 30 | + | |
| 31 | +### The substrate (phase 0) | |
| 32 | + | |
| 33 | +- **Not four features — one substrate with four format adapters.** Everything they share already existed: blob storage (`GitGud.Storage`), namespacing, auth tokens, a UI. What differs per format is the *index shape* and the *upload framing*, which is what `GitGud.Packages.Format` captures (`index/2`, `store/3`, `content_type/1`, optional `normalize/1` and `namespace_index/3`). | |
| 34 | +- **`packages.format` + `packages.normalized_name`**, with uniqueness moving to `(repository, format, normalized_name)` — the form every lookup actually goes through. Existing rows backfill to `oci`. | |
| 35 | +- **`package_files`** is one row per artifact, since a release is rarely one file. Bytes go to `GitGud.Storage` under a content-addressed key, and a published filename can never later point at different bytes — a supply-chain property, not a convenience. | |
| 36 | +- **One dispatcher** at `/pkg/:format/:owner/:name/*rest` doing the four things that don't vary: resolve, authenticate, authorize, render. | |
| 37 | + | |
| 38 | +### The adapters (phases 1–4) | |
| 39 | + | |
| 40 | +- **PyPI** — PEP 503 HTML and PEP 691 JSON off one data structure, negotiated on `Accept` with a real q-value parser (`q=0` is a refusal, not a weak preference). PEP 503 normalisation applied before *any* lookup; trailing slashes are load-bearing and Plug drops them, so the raw fact is passed through. twine's multipart upload, with its `sha256_digest` verified. | |
| 41 | +- **Hex** — registry v2 protobufs encoded per request out of Postgres rather than rebuilt by `mix hex.registry build`, so publishing is an insert instead of O(everything). Wire format transcribed from hex_core's own definitions and checked by decoding our output with hex_core's decoder. The signature is RSA over the SHA-512 digest of the payload; the gzip is *content*, not a transfer encoding. `metadata.config` is parsed with `:erl_parse.parse_term/1`, never `:erl_eval` — there's a test that an `os:cmd/1` in there is a parse error. | |
| 42 | +- **Cargo** — sparse HTTP index only; the git-index form was declined despite this being a git server, because sparse has been the default since 1.68 and the git form means a second write path. Prefix shard checked against the name it belongs to, `ETag`/`304` keyed on the body's own hash, `features2`/`v: 2` splitting, and the `explicit_name_in_toml` → `package` rename translation (backwards, it resolves the wrong crate silently). | |
| 43 | +- **apt** — signed `InRelease`, `Packages` and `Packages.xz`, pool. Release carries no MD5Sum or SHA1 block at all, since clients are required to reject those as security. `Description-md5` is mandatory as of APT 2.7.7. `ar` is parsed in Elixir rather than shelled out to, to keep `binutils` out of the runtime image. | |
| 44 | + | |
| 45 | +### `native/gitgud_apt` | |
| 46 | + | |
| 47 | +- **The one place in the registries that needed a NIF.** apt requires an OpenPGP signature on `Release` and OTP has none — `:public_key` speaks PKCS and X.509. Rustler binds `sequoia-openpgp`. | |
| 48 | +- **Crypto backend was the phase's only real unknown, and all three were built and compared.** Nettle (the default) would add `libnettle`/`libgmp` to the runtime image; the pure-Rust backend is gated behind `allow-variable-time-crypto`, which is wrong for code holding a private key; OpenSSL links libssl/libcrypto, which the builder already installs for gix and the runner already installs for the Erlang runtime. **No new package in either image.** The aptly fallback the plan kept in reserve went unused. | |
| 49 | +- The crate also carries xz and zstd (pure Rust), because a `.deb`'s control member is `control.tar.xz` on anything since squeeze and `:erl_tar` knows only gzip. The same encoder produces `Packages.xz`. | |
| 50 | + | |
| 51 | +### Signing keys, CI, and namespace repositories | |
| 52 | + | |
| 53 | +- **Keys belong to the namespace, not the repository.** Per-repo keys meant a machine installing from five repositories in one org needed five entries in `/etc/apt/keyrings` for what its sources list calls one publisher. `(organization | user, format)` with exactly one owner enforced by a check constraint; private halves wrapped by the `GitGud.Secrets` master key and `key_id`, so a rotation re-wrap reaches them. | |
| 54 | +- **`GET /pkg/_keys/:format/:owner`** is the URL to pin. Unauthenticated and generating on demand — pinning the key is the *first* line of the setup, before apt has fetched any `Release`, so 404ing until something was signed made step one fail. | |
| 55 | +- **`_all`** is a namespace-wide apt repository: one `deb` line and one keyring file for everything an org or user publishes, filtered to what the caller may read and signed per request. It sits in the repository slot because a repository name must start with an alphanumeric, so it can never collide — and the callback is optional, so other formats can opt in without new routing. | |
| 56 | +- **CI publishes with no stored secret.** `${{ github.token }}` is a `RunnerJwt` and now authenticates as `{:job, repository_id}`, writing its own repository only — the same deal the OCI token endpoint already offered, which is what lets a publish step be pasted into a workflow. | |
| 57 | +- The packages page groups by format and carries install *and* publish instructions for both a terminal and a workflow; snippets live in `GitGud.Packages.Usage` as pure functions so their real rules can be asserted. | |
| 58 | + | |
| 59 | +### Verification | |
| 60 | + | |
| 61 | +Each adapter was driven with its actual client, not just its tests: `pip install` and a twine-framed upload; `mix hex.repo add` → `deps.get` → compile → run; `cargo publish`, resolve, build, `yank`, and a 304; and a `debian:trixie-slim` container doing `apt-get update` + `install` against both a single repository and an `_all` namespace, with GnuPG independently confirming `Release.gpg`. | |
| 62 | + | |
| 63 | +--- | |
| 64 | + | |
| 65 | +## Slice 69 — Topic tags + listing pagination | |
| 66 | + | |
| 67 | +- **Up to seven tags per repository**, browsable at `/tags` and joining Syms in the Explore menu. Their own table rather than an array column: the interesting queries are "everything tagged elixir" and "every tag with a count". Tags normalise to lowercase, so a repo can't hold both `Elixir` and `elixir`. | |
| 68 | +- **One tag is derived**, not typed — the repo's primary language, kept current by the language-stats worker, and marked by source so a human edit isn't clobbered. | |
| 69 | +- **Pagination** on `/repositories` (15) and `/orgs` (7), deep-linkable. `/repositories/2` puts the page in the path; orgs use `/orgs/page/2` and not `/orgs/2`, because an org handle may start with a digit and that URL already belongs to the org actually named "2". | |
| 70 | + | |
| 71 | +--- | |
| 72 | + | |
| 73 | +## Slice 68 — Scoped moderation for owners | |
| 74 | + | |
| 75 | +Two halves of one problem: an owner couldn't see reports about their own content, and had no way to stop a problem user. | |
| 76 | + | |
| 77 | +- **Reports gain denormalised `repository_id` / `organization_id`**, filled in at report time and backfilled. Resolving the owner from `(target_type, target_id)` needs a different join per type, which is not something to run on every settings page load. Federation targets belong to neither and stay instance admins' business. | |
| 78 | +- **Scoped bans** — a repo admin can stop someone acting in their repo, an org admin across every repo the org owns. Neither reaches further; suspending an account instance-wide stays an instance admin's power. | |
| 79 | +- **Banning folds away existing content at render time**, not by rewriting rows. Lifting a ban restores everything for free, `original_body` (which belongs to the separate mod-message flow) is untouched, and there's no partial write to recover from. An expiring ban un-hides on its own, with no sweeper to run. | |
| 80 | +- Org and repo moderation pages at `/orgs/:handle/settings/moderation` and `/r/:owner/:name/settings/moderation`. | |
| 81 | + | |
| 82 | +--- | |
| 83 | + | |
| 84 | +## Slice 67 — Cross-reference backlinks | |
| 85 | + | |
| 86 | +- Writing `#42` linked one way only; the referenced item now gets a `cross_referenced` entry in its history naming the source, so the relationship reads from both ends. | |
| 87 | +- Recorded when a description is **written** as well as edited — a reference typed while opening an issue is as much a link as one added later. | |
| 88 | +- **Only genuinely new references count**, diffed against what was already there, so an edit that leaves `#42` in place doesn't record it twice. A `#42` inside backticks is text, not a link. | |
| 89 | + | |
| 90 | +--- | |
| 91 | + | |
| 92 | +## Slice 66 — Org membership by invitation | |
| 93 | + | |
| 94 | +- Joining an org becomes something you accept rather than something done to you: an admin invites, membership is inserted only on acceptance, from a panel on the invitee's dashboard naming who asked. | |
| 95 | +- **Answered invitations are kept, not deleted**, so the members page can show one was withdrawn or declined rather than silently losing it. The uniqueness constraint covers only pending rows, so inviting twice returns the existing invitation. | |
| 96 | +- Last-admin guard, so an org can never end up admin-less. | |
| 97 | + | |
| 98 | +--- | |
| 99 | + | |
| 100 | +## Slice 65 — Sudo mode across the credential pages | |
| 101 | + | |
| 102 | +- **`/users/settings/two-factor` was reachable with only a session** — a hijacked cookie could enrol its own authenticator or remove the account's existing second factor without ever proving it knew the password. Same gap on `/users/settings/tokens` (which mints long-lived PATs) and `/users/settings/invites`. | |
| 103 | +- Sudo is declared per-LiveView via `on_mount` rather than in the router, so a settings page is ungated unless someone remembers the line — the audit is the fix, and the pattern makes the next omission just as easy. | |
| 104 | +- **Second-factor re-auth** (`users.second_factor_reauth_enabled`, off by default): answer a sudo prompt with a TOTP code or a security key instead of a password. Requires a factor to actually be registered, so removing every factor degrades to password-only rather than locking the account out. | |
| 105 | + | |
| 106 | +--- | |
| 107 | + | |
| 108 | +## Slice 64 — Code review | |
| 109 | + | |
| 110 | +The largest arc in this stretch: pull requests gain reviewers, line-level discussion, suggested changes, and per-repo merge rules. | |
| 111 | + | |
| 112 | +- **Assigned reviewers**, requested by handle or from chips suggesting who actually reviews in this repo. Self-requests are refused. | |
| 113 | +- **One standing verdict per reviewer.** Reviews stay append-only — that log is the history — but the section leads with each reviewer's latest verdict and folds the log behind a disclosure that only appears once someone has more than one. The form starts from your standing verdict; changing your mind is the same action as leaving a first one. A re-submission that doesn't change the verdict records nothing. | |
| 114 | +- **Line comments** anchored to the new-side line where there is one (that's the line a reader is looking at) and the old side for a deletion, pinned to the head sha they were written against so a later push doesn't silently re-anchor them. | |
| 115 | +- **Suggested changes** — a ```` ```suggestion ```` block renders as the proposed replacement with an Apply button that commits it. It refuses rather than guesses: `:stale` if the branch moved (including because an earlier suggestion was just applied), `:forbidden` without write access to the *source* repo, `:out_of_range`, `:closed`. The ref update is a compare-and-swap, so a concurrent push loses the race instead of being overwritten. | |
| 116 | +- **A dedicated `/pulls/:n/files` screen.** Reviewing code and reading discussion want different things from the screen; a long diff pushed the comment box far below the fold. Conversation, files and history get tabs with counts. | |
| 117 | +- **Per-repo merge rules**, generalised into `merge_blockers/2`, which returns *every* rule a PR currently fails rather than a boolean — a PR can be short on approvals and have a standing objection, and saying only one sends people round twice. Block-on-changes-requested, require N approvals, and block self-approval (the only one on by default, and only for new repos: switching a live repo's review rules out from under it is the owner's call, not a migration's). Repo admins get an override, recorded as one. | |
| 118 | + | |
| 119 | +--- | |
| 120 | + | |
| 121 | +## Slice 63 — Editable issues and PRs, and history subpages | |
| 122 | + | |
| 123 | +- **Titles, descriptions and comments are editable in place** by the author or a repo admin. Comments are soft-deleted, so the history can still render what was edited before it went. | |
| 124 | +- **An append-only `events` table**, polymorphic over `(target_type, target_id)` the way `labelings` already is, rather than one table per target — the history pages read them identically. | |
| 125 | +- **A `/history` subpage** on every issue and PR: pushes (with sha pair and commit count), renames, description edits, labels, state changes, reviews, comment edits and moderation, interleaved in one timeline. Body edits fold their before/after into a collapsed diff rendered from the recorded payload, so it survives the comment being deleted. | |
| 126 | +- Recording happens at the call sites that know who acted, which keeps the contexts from having to guess. | |
| 127 | +- Closed PRs can be reopened by the author or a maintainer. | |
| 128 | + | |
| 129 | +--- | |
| 130 | + | |
| 131 | +## Slice 62 — Discovery: cards, syms, Explore, and a signed-in dashboard | |
| 132 | + | |
| 133 | +- **`/` becomes two pages behind one route.** Signed out, the marketing landing is unchanged — moved verbatim into `LandingComponents` with a test pinning the grid intact. Signed in, it's the viewer's own work: PRs and issues they opened, PRs waiting on them, CI, pins, recently-pushed repos, orgs. | |
| 134 | +- **"Needs your review" is inferred, not requested** (at this point PRs carried an author but no review request): open PRs in repos you maintain that someone else opened. Maintain means own outright or org-admin — plain org membership doesn't inherit the queue, and your own PRs never appear in it. Both tested. | |
| 135 | +- **`/repositories` and `/orgs` rebuilt as cards** carrying the metadata already on the row — avatar, visibility/archived/imported/CI badges, fork parent, top language, size, relative push time — with a live filter, an All/Yours scope toggle and sort. Filtering runs over the already-loaded list; re-querying per keystroke would only add round-trips. | |
| 136 | +- **Syms** — stars, but with a configurable symbol set (`config :git_gud, syms:`). Several at once, since each reads as its own signal. An empty list disables the feature. Dropping a symbol only hides it: rows stay and reappear if it's added back, so an edit never destroys marks. | |
| 137 | +- **Explore dropdown** for the instance-wide facets (`/syms`, later `/tags`). | |
| 138 | + | |
| 139 | +--- | |
| 140 | + | |
| 141 | +## Slice 61 — Cross-repo pull requests, one picker | |
| 142 | + | |
| 143 | +- **Both sides of a PR are pickable from one pool**: every repo sharing this one's lineage that the viewer can read. Previously the source was limited to the current repo, its parent and its linked forks, and the target was always whatever repo you were standing on. | |
| 144 | +- **Lineage is "shares a root commit"**, recorded in `repository_roots` — a fingerprint that survives an import that never set `parent_repository_id`, and a link that was later cut. Indexed on fork/clone creation and on every push; `mix gitgud.backfill_roots` covers repos predating the table. | |
| 145 | +- **The federated path stopped being a separate mode.** Both sides read `[instance][owner/repo][branch]`, and a base instance differing from ours is what makes it an `Offer` — no toggle, no raw AP actor URL pasted into a box. A federated target is unproven until it answers, so the rest of the form sits behind a "Test connection" probe that resolves the composed actor URL and checks it is a Repository. | |
| 146 | +- **Pulls joins the repo header** with an open count (aggregated, not loaded), and opening a no-op PR is refused — same tips, or a merge-base equal to the head. | |
| 147 | + | |
| 148 | +--- | |
| 149 | + | |
| 150 | +## Slice 60 — Per-file history and the blob version pager | |
| 151 | + | |
| 152 | +- **`/commits/:ref/*path`** scopes the commit walk to one file or directory. No git-layer work was needed: `Cli.log` already honoured `:paths`, and `Git.dispatch` falls back to the CLI when the gix NIF returns `:unsupported`, which it does for path filtering. | |
| 153 | +- **The blob view pages through a file's versions.** The pager needs a branch to read history against — a commit SHA alone tells us what came *before* a version, never what came after — so links carry `?from=<ref>`, defaulting to the repo's default branch. A SHA that can't be placed in that history simply gets no pager. Viewing a version where the file doesn't exist is expected (the commit that deleted it is part of its own history) and renders a note rather than bouncing out. | |
| 154 | +- **Jump-to-newest**, plus `?at=<sha>` on the History button so clicking a hash into a version and back out lands you where you were. | |
| 155 | +- **Diff rows stopped rendering three lines tall.** Cells are `whitespace-pre-wrap`, so two sources of stray whitespace rendered literally: the HEEx template's own source indentation, and the trailing newline inside the per-line `<div class="line">` the highlighter emits. | |
| 156 | + | |
| 157 | +--- | |
| 158 | + | |
| 15 | 159 | ## Slice 59 — Repository import (GitHub / GitLab / any git URL) |
| 16 | 160 | |
| 17 | 161 | Bulk-imports repositories from another forge, plus one-off imports of an arbitrary git URL. Git data only: branches, tags and notes come across via `git clone --mirror`; issues and pull requests do not. |
@@ -1201,9 +1345,45 @@ Closed slices 14, 15, 16, 17 in this session. Final task graph:
| 1201 | 1345 | | 124 | done | Canonical `.forgejo/workflows/build.yml` (ports `.gitlab-ci.yml` to Actions + buildx) | |
| 1202 | 1346 | | 125 | done | zot deployment manifest `zot.yml` (PVC-backed, ggr.neiam.co, fsGroup init) | |
| 1203 | 1347 | | 126 | done | `mix gitgud.registry.keygen` task + Packages.Token / ZOT_WEBHOOK_SECRET runtime env wiring | |
| 1348 | +| 127 | done | Per-file commit history + blob version pager, jump-to-newest (slice 60) | | |
| 1349 | +| 128 | done | Diff row whitespace fix — three-line-tall rows, content pushed right (slice 60) | | |
| 1350 | +| 129 | done | Cross-repo PRs over shared lineage + `repository_roots` + backfill task (slice 61) | | |
| 1351 | +| 130 | done | Unified PR picker: instance/owner-repo/branch on both sides, federated probe (slice 61) | | |
| 1352 | +| 131 | done | Pulls in the repo header with an open count; no-op PRs refused (slice 61) | | |
| 1353 | +| 132 | done | `/repositories` + `/orgs` as cards with filter, scope and sort (slice 62) | | |
| 1354 | +| 133 | done | Syms + instance-wide `/syms` ranking + Explore dropdown (slice 62) | | |
| 1355 | +| 134 | done | Signed-in dashboard at `/` replacing the ad grid (slice 62) | | |
| 1356 | +| 135 | done | Editable titles/descriptions/comments + `events` table + `/history` subpages (slice 63) | | |
| 1357 | +| 136 | done | Reopen a closed pull request (slice 63) | | |
| 1358 | +| 137 | done | Assigned reviewers + standing verdict per reviewer (slice 64) | | |
| 1359 | +| 138 | done | Line comments on the PR diff, pinned to the head they were written against (slice 64) | | |
| 1360 | +| 139 | done | Suggested changes with compare-and-swap apply (slice 64) | | |
| 1361 | +| 140 | done | Dedicated `/pulls/:n/files` review screen + reviewer tab (slice 64) | | |
| 1362 | +| 141 | done | Merge rules: changes-requested, N approvals, self-approval, via `merge_blockers/2` (slice 64) | | |
| 1363 | +| 142 | done | Sudo mode on two-factor / tokens / invites; second-factor re-auth (slice 65) | | |
| 1364 | +| 143 | done | Org membership by invitation, with withdrawn/declined kept (slice 66) | | |
| 1365 | +| 144 | done | Cross-reference backlinks in issue + PR history (slice 67) | | |
| 1366 | +| 145 | done | Repo- and org-scoped bans; reports scoped to their owner (slice 68) | | |
| 1367 | +| 146 | done | Banned users' existing content folded away at render time (slice 68) | | |
| 1368 | +| 147 | done | Repo topic tags + `/tags` + language-derived tag (slice 69) | | |
| 1369 | +| 148 | done | Pagination for the repository and organization listings (slice 69) | | |
| 1370 | +| 149 | done | Two flaky tests fixed — full-suite runs were unreliable | | |
| 1371 | +| 150 | done | README + landing tiles brought up to what the forge does | | |
| 1372 | +| 151 | done | Plan: private PyPI, Hex, Cargo and apt registries (`docs/package-registries.md`) | | |
| 1373 | +| 152 | done | Package registries phase 0 — `Packages.Format` substrate, `package_files`, `/pkg` dispatcher (slice 70) | | |
| 1374 | +| 153 | done | Package registries phase 1 — PyPI (PEP 503 + 691), verified with real pip + twine framing (slice 70) | | |
| 1375 | +| 154 | done | Package registries phase 2 — Hex registry v2, verified with `mix hex.repo add` + `deps.get` (slice 70) | | |
| 1376 | +| 155 | done | Package registries phase 3 — Cargo sparse index, verified with real `cargo publish`/`yank` (slice 70) | | |
| 1377 | +| 156 | done | Package registries phase 4 — apt + `native/gitgud_apt` (sequoia-openpgp, OpenSSL backend) (slice 70) | | |
| 1378 | +| 157 | done | Per-namespace signing keys + `/pkg/_keys/:format/:owner`; CI publishes with `github.token` (slice 70) | | |
| 1379 | +| 158 | done | Namespace-wide apt repositories at `_all`, verified with real apt in a debian container (slice 70) | | |
| 1380 | +| 159 | done | README: the package registries | | |
| 1381 | +| 160 | done | Org dashboard at `/orgs/:handle/dashboard` + shared `DashboardComponents` (slice 71) | | |
| 1382 | +| 161 | done | PROGRESS + ROADMAP brought current from slice 59 to slice 71 | | |
| 1204 | 1383 | |
| 1205 | 1384 | End-of-session: 501 tests pass on the NIF backend with the new Multilevel cache. Next pickups: in-browser PR conflict resolution, or filling in the remaining gix NIF ops (diff_stats / merge_base / merge_tree / commit_tree / update_ref) once the gix API shakes out. |
| 1206 | 1385 | Embedded SSH daemon verified end-to-end against `git push --set-upstream origin master` from a real client (after fixing two bugs: spawning git with the dashed subcommand name failed; `:stderr_to_stdout` on the Port corrupted the pkt-line stream). |
| 1207 | 1386 | |
| 1208 | 1387 | End-of-session (registry full-cycle slice): runner-register UI now matches Forgejo's binary + docker install docs (`generate-config` step, `--config` flag on daemon) and gained Podman Quadlet + Kubernetes deployment styles. Ported the legacy `.gitlab-ci.yml` to a canonical Actions workflow at `.forgejo/workflows/build.yml` (buildx + metadata-action + registry build cache). Wrote `zot.yml` in the same heavily-commented Traefik/cert-manager style as `app.yml`, backed by a 100Gi `zot-data` PVC on `openebs-zfspv` (S3/Garage driver kept available in a comment). Added `mix gitgud.registry.keygen` — emits PKCS#1 private + SubjectPublicKeyInfo public PEMs (verified against `openssl rsa -pubout`) plus kubectl one-liners for splitting them across the `elixir-args` and `zot` Secrets. `runtime.exs` now reads `GITGUD_REGISTRY_PRIVATE_KEY` → `Packages.Token` and `ZOT_WEBHOOK_SECRET` → `PackagesWebhookController`, and `app.yml` exposes both env vars (both `optional: true` so pods survive before/after key provisioning). End-to-end push → workflow → runner → registry path is now wired; what remains is operator-side: keygen, `kubectl apply -f zot.yml`, DNS for `ggr.neiam.co`, and providing the new secrets to `elixir-args`. |
| 1209 | 1388 | |
| 1389 | +End-of-session (package registries + org dashboard): closed slices 60–71, which had accumulated unrecorded since slice 59. The bulk is slice 70 — four package registries (PyPI, Hex, Cargo, apt) on one `GitGud.Packages.Format` substrate, each driven with its real client rather than only its tests, plus `native/gitgud_apt` for the OpenPGP signatures OTP can't produce. Its crypto backend was picked by building all three sequoia backends and comparing: OpenSSL adds no package to either container image, where Nettle would add libnettle/libgmp and the pure-Rust backend is gated behind `allow-variable-time-crypto`. Signing keys are per namespace, not per repository, and apt gained a namespace-wide `_all` repository so one keyring entry and one `deb` line cover an org. 1189 tests pass. `mix precommit` is still red on pre-existing type warnings in `pr_live/show.ex` and `pull_requests.ex` — see ROADMAP maintenance. |
modified
ROADMAP.md
+16
−7
@@ -9,7 +9,9 @@ See [PROGRESS.md](./PROGRESS.md) for what's done.
| 9 | 9 | |
| 10 | 10 | ## Now — in flight |
| 11 | 11 | |
| 12 | −Nothing in flight. Slice 59 (repository import) closed most recently. | |
| 12 | +Nothing in flight. Slice 71 (org dashboard) closed most recently; | |
| 13 | +slice 70 (private package registries) is the substantial one before it. | |
| 14 | + | |
| 13 | 15 | Federation feature set is closed through slice 42 (reviews over AP |
| 14 | 16 | shipped in slice 38, suggested-blocks in slice 38, actionable reports |
| 15 | 17 | queue in slice 42). Remaining federation items are discretionary |
@@ -17,9 +19,11 @@ queue in slice 42). Remaining federation items are discretionary
| 17 | 19 | |
| 18 | 20 | ## Next — small, high impact |
| 19 | 21 | |
| 20 | −Nothing in flight here right now — slice 42 (actionable reports) was | |
| 21 | −the last "small, high impact" item on the list. Pick the next leverage | |
| 22 | −item from "Soon" or "Maintenance" below. | |
| 22 | +| Item | Why | Size | | |
| 23 | +|---|---|---| | |
| 24 | +| `${{ gitgud.token }}` in CI + docs | From **G Notes** below. The per-job token is ours, not GitHub's, and every registry and the OCI endpoint now accept it — the name should say so. Keep `${{ github.token }}` working: it is what `forgejo-runner` injects as `GITHUB_TOKEN`, and every existing workflow uses it | small | | |
| 25 | +| Package retention / GC | Nothing prunes old package versions. Yanking deliberately keeps the bytes so existing pins keep resolving, which means storage only grows | medium | | |
| 26 | +| Namespace-wide indexes beyond apt | `namespace_index/3` is optional on `GitGud.Packages.Format` and only `Deb` implements it. A namespace-wide PyPI index is the same shape and needs no new routing | small-medium | | |
| 23 | 27 | |
| 24 | 28 | ## Soon — medium investments |
| 25 | 29 |
@@ -28,7 +32,8 @@ item from "Soon" or "Maintenance" below.
| 28 | 32 | | Import issues / PRs / wikis alongside git data | Slice 59 imports git data only. Issues need author mapping for users who don't exist locally; PRs need ref rewriting | large | |
| 29 | 33 | | Scheduled mirror sync | Re-sync is manual today. A cron would need retained credentials and a divergence policy | medium | |
| 30 | 34 | | Resolve PR conflicts in-browser | Today a conflict means "fix it locally and push" — a 3-way merge editor would close the loop | medium-large | |
| 31 | −| gix NIF: fill in log, commit, tree, diff_stats, merge_base, merge_tree | 10-100× perf for repo-page hot paths; Cli fallback works today | medium | | |
| 35 | +| gix NIF: fill in diff_stats, merge_base, merge_tree, commit_tree, update_ref | 10-100× perf for repo-page hot paths; Cli fallback works today. log / commit / tree landed in slice 56 | medium | | |
| 36 | +| Package signing key rotation | Keys mint themselves on first use and are re-wrapped by the `GitGud.Secrets` master-key rotation, but there is no "roll the keypair" flow — because rolling it breaks every consumer that pinned the old one. Needs a publish-both-then-retire story before it is safe | medium | | |
| 32 | 37 | |
| 33 | 38 | ## Eventually — bigger / discretionary |
| 34 | 39 |
@@ -52,10 +57,14 @@ These are documented in the auto-memory under `project-git-gud-moderation-deferr
| 52 | 57 | |
| 53 | 58 | | Item | Notes | |
| 54 | 59 | |---|---| |
| 55 | −| Wire NIF properly for log/commit/tree | Per-op fallback works today; fill in to actually get the perf win | | |
| 60 | +| Wire NIF for the remaining git ops | log / commit / tree landed in slice 56; `diff_stats`, `merge_base`, `merge_tree`, `commit_tree` and `update_ref` still fall back to the CLI per-op | | |
| 56 | 61 | | Test fixture cleanup | `tmp/test/repos/` wiped on suite start; consider per-test isolation if parallelism grows | |
| 57 | 62 | | Documentation as code grows | This file + PROGRESS.md; append on each slice close | |
| 58 | −| `mix precommit` enforces warnings-as-errors | Stay green; warnings sneak in fast | | |
| 63 | +| `mix precommit` enforces warnings-as-errors | **Currently red**, and has been for a while — 17 sites across 14 files, in three families: unreachable clauses after a type got narrowed (`duration/2`, `format_deny/1`, `actor_name/1`, …), one unused `require Logger`, and genuine type mismatches where `attach_label/2` / `detach_label/2` / `check_review_block/3` are handed a map where a struct is expected. None is a runtime bug today, but the gate is worthless while it is red, and each new warning hides behind the existing noise. Worth one pass with `mix compile --force --warnings-as-errors` | | |
| 64 | +| `native/gitgud_gix/target/` + `priv/native/gitgud_gix.so` are tracked | ~5 MB of build output that churns on every compile. `.gitignore` now covers the NIF crates, but these predate it and stay tracked until someone `git rm --cached`s them | | |
| 59 | 65 | | Operator setup doc | A first-deploy README covering compose, env vars, hooks, runner registration, allowlist seeding | |
| 60 | 66 | | SSH deploy keys over embedded daemon | v1 omits this; deploy keys still work via external SSH wrapper. Wire deploy-key auth into `KeyApi` | |
| 61 | 67 | | SSH stderr → channel stderr | Currently git's stderr lands on BEAM's stderr (iex console). Forward via a wrapper script so the client gets `remote:` messages | |
| 68 | + | |
| 69 | +## G Notes | |
| 70 | +- ${{ gitgud.token }} for the ci/docs (and still provide ${{ github.token }} for backcompat) |
Parents: 8041c34