Skip to content

feat(cli): v0.2 CRUD surface — registry-driven create/update/delete for 14 resource types + apply -f - #1

Merged
pawel-cygal merged 16 commits into
mainfrom
impl/v0.2-crud
Aug 8, 2026
Merged

feat(cli): v0.2 CRUD surface — registry-driven create/update/delete for 14 resource types + apply -f#1
pawel-cygal merged 16 commits into
mainfrom
impl/v0.2-crud

Conversation

@pawel-cygal

Copy link
Copy Markdown
Member

Phase 1 of the full-coverage CLI (v0.2 → v0.3 generate → v0.4 export). Spec/plan in docs/superpowers/.

Adds a registry-driven imperative surface on syschecks-go v0.3.3. A single Resource (SDK method closures + field descriptors + serialization) is registered per type; a generic factory turns each into list/get/create/update/delete (a verb's subcommand exists only where the SDK supports it). Three features share the registry so they can't diverge (CRUD now; export/generate in later phases).

Surface

  • Generic CRUD for 14 resource types: organization, project, check, notification-channel, team, service, oncall-schedule, escalation-policy, maintenance-window, playbook, status-page, lifecycle-watch (create=upsert, no update), contact-method (no get), integration-key (no get/update).
  • apply -f <file|-> — multi-doc, dispatch by kind, update-if-id-else-create; reads stdin on -.
  • Hybrid input — flat scalar fields as flags + -f file.yaml (flag overrides file); get -o yaml round-trips through apply.
  • Actions: incident get/acknowledge/resolve, agent token/delete. v0.1 (check run/pause/resume, probe, verify, auth, config) unchanged.

Quality

Built task-by-task with TDD (15 commits), a per-task spec+quality review and a final whole-branch review — all clean. Full go build/vet/test ./... green; v0.1 behaviour not regressed.

Known backlog (non-blocking, filed for v0.2.x)

  • team update --slug (and some contact-method fields) are silently dropped when the SDK Update model lacks them — warn or stop advertising those flags.
  • get -o yaml emits no kind, so a literal get -o yaml | apply -f - needs a manual kind: (emit it from get).
  • apply -f skips client-side Required validation (fails server-side instead).
  • check create --help exposes ~87 flags (whole CheckCreate model) — curate a per-check-type common subset.
  • integration-key list wrapper key/columns are unverified guesses (backend schema is empty) — confirm against a live response.

Pawel Cygal added 15 commits August 5, 2026 21:40
TestFactoryBuildsCrudAndRoutesFlags previously only checked cmd.Use and
subcommand names, never running a leaf's RunE. Extend it to actually
execute `create --name widget-x` and assert on the body createFn
received and the rendered output, and add a required-field-missing case
asserting exit code 2.
- bodyFromFlagsAndFile replaces Task 1's bodyFromFlags: loads an optional
  -f YAML/JSON document via sigs.k8s.io/yaml (converts to JSON first so
  ints/bools match the SDK models), then overlays any changed flags on
  top (flags win); required-field enforcement now also accepts a value
  supplied only via -f.
- newCreateCmd/newUpdateCmd gain a --file/-f string flag and call the
  new helper.
- newApplyCmd (internal/cli/apply.go) reads -f, splits the file into
  YAML documents on a lone "---" line, and for each document reads
  kind, looks up registry[kind] (clierr.Config on unknown kind), and
  dispatches to that resource's updateFn (when the doc carries an id)
  or createFn otherwise; org is resolved per the resource's OrgMode via
  the existing resolveResourceOrg helper. Registered on root.

Tested against synthetic in-memory resources registered/deregistered
inside apply_test.go (real check/notification-channel resources land in
Task 3), covering multi-doc dispatch, create-vs-update routing, unknown
kind, missing -f, and the flag-overrides-file merge.
…CRUD

Migrate the four existing typed resources onto the Task 1/2 resource
registry (list/get/create/update/delete via newResourceCmd), adding full
create/update/delete for all of them without regressing v0.1 behavior:
org keeps its slug-aware get, check keeps its name-aware get plus
run/pause/resume/test-alert, notification keeps test. Adds
mapToBody[T] (the inverse of toMap) so create/update bodies are built
straight from the SDK's Create models instead of hand field mapping.
DeleteOrganizationParams.Confirm must equal the organization's name,
but deleteFn always sent it empty, so a real backend rejected every
org delete. Fetch the org first to read its name and pass it as
confirm — the CLI's own --yes gate already got the user's consent.
Register team and service as OrgArg registry resources (Teams.*/
Services.* SDK calls), wired directly into root.go via
newResourceCmd(registry["team"|"service"]).
Resource gains an optional ListCols: list renders it when set, else falls
back to Cols (unchanged for every other resource). service's list endpoint
(ServiceListItemResponse) returns health_status/checks_count that its
get/create/update responses (ServiceDetailResponse/ServiceResponse) don't
have, so those three verbs were rendering blank cells for them.
Register maintenance-window (Org=OrgParam, org rides ListMaintenanceWindowsParams.OrganizationId)
and playbook (Org=OrgArg, org is a required path arg) on the resource registry,
wired into root.go alongside team/service/oncall-schedule/escalation-policy.
lifecycle-watch update discarded its id arg and, since required-field
enforcement only applies on create, could silently upsert an empty-keyed
watch. Remove updateFn (list/get/create/delete only), promote
vendor/resource-type/resource-id to Required on create, and document the
upsert semantics on newLifecycleWatchCmd's create Long text.
Incidents are addressed by the (check_id, log_id) pair the API uses, not a
single id, and agents are created by self-registration against a minted
token rather than a create call — neither fits the generic CRUD verb
factory, so both are hand-written cobra leaves added onto the existing
incident/agent parent commands, same pattern as check's run/pause/resume.
output.Render's --quiet path indexed t.Cols[0] unconditionally, which
panicked for any zero-column table — exactly what renderRawObject produces
for a bare {} ack/resolve confirmation. Guard it at the root, and have
renderRawObject print a short confirmation instead of a columnless table in
the default format.

incident acknowledge also decided whether to send --note by comparing it
against "", which conflated an omitted flag with an explicit empty one; use
cmd.Flags().Changed("note") instead, matching bodyFromFlagsAndFile's
established pattern.
Add apply -f - stdin support (get -o yaml | apply -f -) and a round-trip
test proving get -o yaml output feeds back into apply as the same update
body, for check (bespoke get, OrgParam) and team (generic get, OrgArg).
Update README with the full v0.2 CRUD surface: generic list/get/create/
update/delete per resource, apply -f, hybrid flags+-f input, and the
incident/agent actions.
@pawel-cygal

Copy link
Copy Markdown
Member Author

Live E2E (throwaway org, org-scoped PAT, cleaned up) confirmed the CRUD surface really works — check/team full lifecycle (create→get→update-verified→delete→404), apply -f multi-doc, org-scope isolation, client-side Required enforcement, clean error surfacing. Two runtime findings for the v0.2.x backlog (not caught by code review, only by real E2E):

  1. get/create -o json returns a 1-element array [{...}], not a bare object — single-resource output should be an object so scripts can use .id/.name (currently need .[0].id), kubectl-style. Highest-value polish.
  2. check -f interval: 300 did not persist (backend uses a different field name for the interval on create) — a field-name/schema note, the CLI faithfully forwards the -f body.

createFn ignored the resolved orgID (bound to _), so `notification
create --org X` always posted without organization_id and the API
created a personal channel instead of an org-scoped one. Thread
orgID through and set body["organization_id"] when unset, letting an
explicit -f/--organization-id value still take precedence.
@pawel-cygal
pawel-cygal merged commit 5b35cd7 into main Aug 8, 2026
@pawel-cygal
pawel-cygal deleted the impl/v0.2-crud branch August 8, 2026 12:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant