a project registry — the list of what exists (#25) #30

Merged
dan-claude-bot merged 1 commit from feat/project-registry into main 2026-07-13 20:45:23 +00:00
dan-claude-bot commented 2026-07-13 20:04:31 +00:00 (Migrated from github.com)

Closes #25.

environments.yaml could say where things deploy to, and — since #21 — how a project you have already named is placed once it is there. It could not say which projects exist. "Every project" was a thing the operator remembered, so fleet operations (#26) had nothing to iterate and rebuild-from-state (#27) was an assumption: you cannot restore what you cannot enumerate.

A new optional top-level block, keyed by the full <org>/<repo> slug:

projects:
  heavy-duty/incubator:
    environments: [prod, staging]

The key is the repo — no repo: field, because a second place to write the same string is a second place for it to be wrong. And no bare-<repo> fallback, unlike github_apps and environments.<env>.projects: those carry one because state files in the wild are keyed that way, and this block has none to support. A bare <repo> is unique only within an org, which is exactly why it is not a key (#12, learned twice).

Validated, because a registry only earns its keep if it is true

Both checks defend one failure — a silently skipped project reads exactly like a clean one. They run in loadBindings, so every verb refuses a registry that lies, rather than one command warning about it:

  • an environment no environments: block defines is an error (the project would be registered into an environment no command can visit);
  • every environments.<env>.projects.<slug> binding must be registered for that env, or the two blocks describe two different fleets: a destination_uuid or smoke_target real enough for a direct apply, invisible to every fleet run. Only enforced when projects: is present, so pre-registry state files keep loading unchanged.

projectsIn(bindings, env) returns an environment's slugs, sorted. The --all flag that consumes it is #26's, not this PR's — this ships the schema, the validation, the accessor, tests and docs, and nothing else.

Note for the reviewer

loadBindings now renders zod's issues rather than result.error.message. Zod's default .message is the whole issue array as JSON, which flattened these multi-line refusals into a single line of \n escapes — throwing away the part worth writing.

Merge order

This is the base of the stack. #26 (--all) and #27 (--emit-draft) both build on it. #29 is independent.

Gates: npm run check, npm run build, npm test (213 passing) all green.

🤖 Generated with Claude Code

Closes #25. `environments.yaml` could say where things deploy to, and — since #21 — how a project you have already named is placed once it is there. It could not say **which projects exist**. "Every project" was a thing the operator remembered, so fleet operations (#26) had nothing to iterate and rebuild-from-state (#27) was an assumption: you cannot restore what you cannot enumerate. A new **optional** top-level block, keyed by the full `<org>/<repo>` slug: ```yaml projects: heavy-duty/incubator: environments: [prod, staging] ``` The key *is* the repo — no `repo:` field, because a second place to write the same string is a second place for it to be wrong. And no bare-`<repo>` fallback, unlike `github_apps` and `environments.<env>.projects`: those carry one because state files in the wild are keyed that way, and this block has none to support. A bare `<repo>` is unique only *within* an org, which is exactly why it is not a key (#12, learned twice). ### Validated, because a registry only earns its keep if it is true Both checks defend one failure — **a silently skipped project reads exactly like a clean one**. They run in `loadBindings`, so every verb refuses a registry that lies, rather than one command warning about it: - an environment no `environments:` block defines is an error (the project would be registered into an environment no command can visit); - every `environments.<env>.projects.<slug>` binding must be registered for that env, or the two blocks describe two different fleets: a `destination_uuid` or `smoke_target` real enough for a direct `apply`, invisible to every fleet run. Only enforced when `projects:` is present, so pre-registry state files keep loading unchanged. `projectsIn(bindings, env)` returns an environment's slugs, sorted. The `--all` flag that consumes it is #26's, not this PR's — this ships the schema, the validation, the accessor, tests and docs, and nothing else. ### Note for the reviewer `loadBindings` now renders zod's issues rather than `result.error.message`. Zod's default `.message` is the whole issue array **as JSON**, which flattened these multi-line refusals into a single line of `\n` escapes — throwing away the part worth writing. ### Merge order This is the base of the stack. #26 (`--all`) and #27 (`--emit-draft`) both build on it. #29 is independent. Gates: `npm run check`, `npm run build`, `npm test` (213 passing) all green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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: heavy-duty/cast#30
No description provided.