Skip to content

CODER-003 — (Optional) Aggregate builds endpoint across all repos (one call) #79

Closed

Description

@serge-ivo

Type: Backend

Description

The panel lists builds for all repos in an instance. Rather than the frontend making one /deployments call per repo (N calls, N GitHub round-trips), provide a single aggregate endpoint.

Acceptance Criteria

  • GET /v1/instances/:id/coding/builds returns { builds: Array<{ repoId, repoName, available, run: Run | null }> } — the latest run per repo (drill-down to full history stays on the per-repo /deployments).
  • Fans out to GitHub with bounded concurrency; a single repo failing degrades to available:false for that repo, never fails the whole response.
  • Owner-scoped; only repos with a githubRepo are queried.

Technical Notes

  • File: workers/api/src/routes/coding.ts. Reuse the per-repo GitHub fetch helper from CODER-001; Promise.allSettled over the instance's repos.
  • Trade-off: simplest frontend (1 call) vs. an extra endpoint. If CODER-004 ships against per-repo calls first, this is a pure latency optimization — keep it optional.

Dependencies

CODER-001.

Activity

  1. serge-ivo commented on Aug 1, 2026

    @serge-ivo
    ContributorAuthor

    Architectural clarification (CODER-003)

    Multi-repo data model (concrete).

    1. KV repo registry per instance: repo-registry:{instanceId} → [{ repoId, githubRepo }], written on repo add/remove (D1 remains source of truth; KV is the fan-out index).
    2. Fan-out with per-repo conditional requests. /builds iterates the registry and fetches each repo's latest run with If-None-Match/If-Modified-Since using the per-repo ETag stored in KV. Unchanged repos return 304 → 0 rate-limit cost, served from the KV cache. Bounded concurrency (~6) via Promise.allSettled; one repo failing → available:false for that repo only.
    3. Aggregated response { builds: [{ repoId, repoName, available, run }] }.

    Rate-limit tiers. Only active-tier repos are fetched live each cycle; idle-tier repos are served straight from KV (or refreshed on the 5–10 min idle cadence). So N repos ≠ N live GitHub calls per refresh — this is what keeps us well under 5,000/hr at scale.

    Auth. GitHub App installation token per owner (installationTokenForOwner), server-side only, encrypted cache, never browser-exposed.

  2. serge-ivo commented on Aug 1, 2026

    @serge-ivo
    ContributorAuthor

    Decision recorded: Repo registry strategy (Option D — Hybrid)

    Decision: Option D — Hybrid auto-registry with auto-archive.

    • Auto-add on first open — a repo joins the registry the first time it's opened in the Coder agent; no manual registration step.
    • Auto-archive after inactivity — repos with no activity for X days are auto-archived. Default suggestion 30 days, but treat X as a configurable value (TBD) — not hard-coded.
    • Archived = visible but not polled — archived repos still appear in the build-history UI, but drop out of the fan-out/poll set → zero rate-limit cost.
    • Manual reactivation — an archived repo can be un-archived / reactivated when reopened in the agent.

    Chosen over:

    • A (registry grows forever) — unbounded fan-out / rate-limit cost.
    • B (drop from registry on session end) — loses build history.
    • C (manual add/remove) — too much friction.

    Implementation note: this maps onto the KV repo registry from the ticket body — add an archived flag (+ lastActivityAt) per registry entry; the aggregate /builds fan-out iterates only non-archived entries, while the UI still reads archived entries from KV/cached history.

  3. serge-ivo commented on Aug 1, 2026

    @serge-ivo
    ContributorAuthor

    Decision recorded: Active threshold triggers (dual-signal)

    Decision: Dual-trigger "active" mode — BOTH signals count.

    What makes a repo "active" (either signal is sufficient):

    1. Active conversation — the user is currently having a conversation with the Coder agent for that repo.
    2. Active agent work — an agent is currently executing a task on that repo.

    What makes a repo go fully idle / halt updates:

    • No active conversation AND no agent work AND no UI open for that repo.
    • When ALL three conditions are inactive, polling stops completely — no updates at all (not even slow polling).
    • This is stronger than "slow polling" for idle — it's a complete halt.

    Polling tiers that result:

    • Active (conversation running OR agent running): high-frequency polling (e.g. every 30s) — exact value TBD.
    • Passive (UI open/visible, but no conversation and no agent): low-frequency polling (e.g. every 5 min) — exact value TBD.
    • Halted (UI not open, no conversation, no agent): zero polling — completely stopped.

    Implementation note. The frontend tracks three signals:

    • Conversation activity (new message sent / response being received)
    • Agent run state (from existing runState in repoStatuses)
    • UI visibility (Page Visibility API or tab-focus detection)

    All three feed a single tier computation that drives the polling interval. When the tab/UI is hidden AND no agent is running AND no conversation is active, the interval is set to null / cleared entirely — not just slowed.

    Applies to: CODER-001 (build history), CODER-003 (multi-repo registry), CODER-006 (aggregate status endpoint), and CODER-007 (WebSocket push — the server-side push cadence should respect the same tiers).

  4. serge-ivo commented on Aug 2, 2026

    @serge-ivo
    ContributorAuthor

    Shipped in 6015ba7 — closing

    GET /v1/instances/:id/coding/builds (workers/api/src/routes/coding.ts) — one call for the whole Build Status panel instead of N per-repo /deployments requests.

    • Returns { builds: Array<{ repoId, repoName, available, run: Run | null }> } — the latest run per repo (full history stays on per-repo /deployments).
    • Bounded concurrency (6) fan-out to keep well under GitHub's rate limit even with many repos; Promise.allSettled so a single repo failing degrades to available:false for that repo only, never failing the whole response.
    • Owner-scoped (requireOwned); only GitHub-connected repos are queried. Non-GitHub/local repos are still listed as available:false (not hidden) so the panel can show them; the instance's repo order is preserved.
    • Refactor/DRY: extracted githubHeaders() + mapWorkflowRun() + latestRunFor() and reused them in both /builds and /deployments; the singular /deployment route is left untouched.

    Note on the decision comments above (KV repo-registry, per-repo ETag fan-out, Option D auto-archive, active/passive/halted polling tiers): those are refinements that pay off with the frontend polling loop and at higher repo counts. The endpoint here delivers the acceptance criteria (single aggregate call, bounded concurrency, per-repo graceful degradation); the registry/archive/tier layer is best landed with CODER-004's polling loop (which decides cadence) rather than speculatively now — flagging so it isn't lost.

    tsc --noEmit clean. Next: CODER-004 (#80) — the Builds panel UI that consumes this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend / Worker / API workenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions