feat(forge-labels): label + milestone bootstrap parity (MF-7) #311

Merged
code-lead merged 1 commit from feat/298-mf7-label-bootstrap-parity into main 2026-04-24 09:52:13 +00:00
Collaborator

Summary

  • Add ForgePort.createLabel(repo, { name, color, description }) — color is a 6-hex string without #; each adapter adds/strips the prefix its forge expects (Forgejo #rrggbb, GitHub rrggbb, GitLab #rrggbb). Implement on all three adapters.
  • Migrate labels.ts::reconcileLabels off raw HTTP onto the adapter factory — it now calls createForgeAdapterForRepo(repo, token) and exercises listRepoLabels + createLabel through the port. labels.ts stays forge-agnostic and speaks domain colors throughout.
  • ReconcileOptions swaps fetchImpl + forgejoUrl for an injectable adapter used by tests; main.ts bootstrap call simplified (no more URL override needed — the factory reads the forge binding out of config/agents.json).

Design notes

  • Previously reconcileLabels hit the Forgejo REST API directly. A { forge: "github" } repo declared in config/agents.json silently dropped every routing label, which meant every webhook delivered to that repo would no-op through addIssueLabels. MF-7 closes that hole so adding a GitHub/GitLab repo to the fleet gets the canonical area:* / type:* set installed end-to-end on first boot.
  • Label-ID vs name on mutation — Forgejo uses numeric IDs, GitHub + GitLab accept names. createLabel keeps the signature name-based on the port; existing ForgejoAdapter.addLabels / .removeLabel already resolve IDs internally via resolveLabelId, no change there.
  • GitLab's listRepoLabels already strips #, so reconcile round-trips cleanly against an already-populated GitLab project (covered by a dedicated test).

Test plan

  • bun test apps/server/src/infrastructure/forge/labels.test.ts — 16 pass (10 mock-adapter behaviour + 6 per-forge end-to-end reconciles × Forgejo/GitHub/GitLab × {empty repo, fully populated}).
  • bun test apps/server — 1266 pass / 4 fail, matches pre-existing baseline (session JSONL pruning × 3, foreman session CRUD × 1 — all unrelated to this change).
  • bun x turbo run typecheck clean across all workspaces.
  • bun x biome check clean on touched files.

Out of scope

  • Milestone bootstrap. The MF-7 spec mentions adding listMilestones + createMilestone to the port, but config/agents.json has no milestones: stanza today — there is nothing to reconcile against yet. Track as a follow-up once operators grow a config surface for canonical milestones. No port methods added on this PR.
  • Colour palette drift between forges that render label hexes differently — explicitly out of scope per the spec's own "Out of scope" clause for MF-7.
  • GitLab Premium vs Free label semantics — labels are identical on both tiers, so no probe needed (unlike the dependency surface).

Closes #298

## Summary - Add `ForgePort.createLabel(repo, { name, color, description })` — color is a 6-hex string without `#`; each adapter adds/strips the prefix its forge expects (Forgejo `#rrggbb`, GitHub `rrggbb`, GitLab `#rrggbb`). Implement on all three adapters. - Migrate `labels.ts::reconcileLabels` off raw HTTP onto the adapter factory — it now calls `createForgeAdapterForRepo(repo, token)` and exercises `listRepoLabels` + `createLabel` through the port. `labels.ts` stays forge-agnostic and speaks domain colors throughout. - `ReconcileOptions` swaps `fetchImpl` + `forgejoUrl` for an injectable `adapter` used by tests; `main.ts` bootstrap call simplified (no more URL override needed — the factory reads the forge binding out of `config/agents.json`). ## Design notes - Previously `reconcileLabels` hit the Forgejo REST API directly. A `{ forge: "github" }` repo declared in `config/agents.json` silently dropped every routing label, which meant every webhook delivered to that repo would no-op through `addIssueLabels`. MF-7 closes that hole so adding a GitHub/GitLab repo to the fleet gets the canonical `area:*` / `type:*` set installed end-to-end on first boot. - Label-ID vs name on mutation — Forgejo uses numeric IDs, GitHub + GitLab accept names. `createLabel` keeps the signature name-based on the port; existing `ForgejoAdapter.addLabels` / `.removeLabel` already resolve IDs internally via `resolveLabelId`, no change there. - GitLab's `listRepoLabels` already strips `#`, so reconcile round-trips cleanly against an already-populated GitLab project (covered by a dedicated test). ## Test plan - [x] `bun test apps/server/src/infrastructure/forge/labels.test.ts` — 16 pass (10 mock-adapter behaviour + 6 per-forge end-to-end reconciles × Forgejo/GitHub/GitLab × {empty repo, fully populated}). - [x] `bun test apps/server` — 1266 pass / 4 fail, matches pre-existing baseline (session JSONL pruning × 3, foreman session CRUD × 1 — all unrelated to this change). - [x] `bun x turbo run typecheck` clean across all workspaces. - [x] `bun x biome check` clean on touched files. ## Out of scope - **Milestone bootstrap.** The MF-7 spec mentions adding `listMilestones` + `createMilestone` to the port, but `config/agents.json` has no `milestones:` stanza today — there is nothing to reconcile against yet. Track as a follow-up once operators grow a config surface for canonical milestones. No port methods added on this PR. - **Colour palette drift between forges** that render label hexes differently — explicitly out of scope per the spec's own "Out of scope" clause for MF-7. - **GitLab Premium vs Free label semantics** — labels are identical on both tiers, so no probe needed (unlike the dependency surface). Closes #298
feat(forge-labels): label + milestone bootstrap parity (MF-7)
All checks were successful
qa / qa (pull_request) Successful in 3m59s
qa / dockerfile (pull_request) Successful in 9s
fcb9ee54ec
Wire `labels.ts` reconcile through `ForgePort` so the canonical
`area:*` / `type:*` set bootstraps against Forgejo, GitHub, and
GitLab from a single code path. Previously `reconcileLabels` hit
the Forgejo REST API directly, which meant a `{ forge: "github" }`
repo in `config/agents.json` silently dropped every webhook until
an operator hand-created the labels in the forge UI.

Port surface
- Add `ForgePort.createLabel(repo, { name, color, description })`
  — color is a 6-hex string without `#`; each adapter adds/strips
  the prefix its forge expects (Forgejo `#rrggbb`, GitHub `rrggbb`,
  GitLab `#rrggbb`).
- Implement on all three adapters; GitLab's existing `listRepoLabels`
  already strips `#`, so reconcile now round-trips cleanly.

Labels module
- `reconcileLabels` now resolves the forge binding via
  `createForgeAdapterForRepo` and calls `adapter.listRepoLabels` +
  `adapter.createLabel`. `labels.ts` stays forge-agnostic — it speaks
  domain colors throughout.
- Test override switched from `fetchImpl` + `forgejoUrl` to an
  injectable `adapter` on `ReconcileOptions`.
- `main.ts` bootstrap call simplified (no more `forgejoUrl` override).

Milestones are deliberately out of scope — `config/agents.json` has
no `milestones:` stanza yet, so there is nothing to reconcile. Track
under a follow-up.

Tests
- `labels.test.ts` driven by an in-memory mock adapter for the
  behavioural cases plus three per-forge end-to-end reconciles
  (Forgejo, GitHub, GitLab) against a fetch spy. Each e2e pair
  asserts (1) canonical set created on an empty repo, and (2) no-op
  on a fully-populated repo — the MF-7 parity guarantee.

Closes #298

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
code-lead deleted branch feat/298-mf7-label-bootstrap-parity 2026-04-24 09:52:13 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!311
No description provided.