---
id: w8/m1/t004
title: Build minutes — record completed build-Job durations as build_seconds rows
worker: worker8
status: done
estimate: 30m
depends_on: [w8/m1/t001]
---

## Objective

Meter builds: each completed BuildKit build Job (w1/m5) contributes one `build_seconds` usage row — Job start→completion duration, attributed to the service that was built — matching Render's "pipeline minutes" meter (stored as seconds; m3 displays minutes).

## Context

- Build Jobs run in `BEX_BUILD_NAMESPACE` (or the App's namespace when unset) and carry the App's labels (docs/ADR004-deployment.md); bex-api already holds a Kubernetes client.
- Simplest robust source: during the hourly tick (t003), list Jobs completed within the window by label selector and read `status.startTime` → `status.completionTime`. No kube-state-metrics dependency.
- The window-keyed upsert makes this idempotent like the other kinds: a Job completed at 14:23 lands in the 14:00 window; re-listing it on re-run updates the same row.
- Synergy note: when w2/m5 (deploy objects) lands, deploys will record build durations first-class — this listing approach can then switch source without touching the schema.

## Steps

1. Add a `build-seconds(window)` collector to `internal/usage` that lists completed build Jobs via the k8s client.
2. Fold it into the t003 tick alongside the Prometheus kinds.
3. Unit-test against a fake clientset with fixture Jobs (completed inside/outside the window, still-running).

## Files

- `lego/backend/internal/usage/`

## Acceptance criteria

- [ ] A build-from-git deploy produces exactly one `build_seconds` row in the window its Job completed, with quantity ≈ the Job's real duration.
- [ ] Still-running and out-of-window Jobs contribute nothing.

## Out of scope

- Switching to w2/m5 deploy objects as the source (follow-up when they exist).
