Skip to content

SDK v0.13.0 bump readiness: the worklist, audited against the tree #626

Description

@jeremy

Read-only audit of the CLI against basecamp-sdk main (1d8ddb1c0), ahead of the v0.13.0 tag. No bump has happened and none should until the tag exists — there is nothing to pin to. This is the worklist the bump executes against.

Source: the SDK's MIGRATING.md at that SHA, which reports 58 merged PRs, 15 breaking, and enumerates 12 class A and 4 class B breaks for Go. basecamp-sdk is public, so quoting it here is fine.

Every entry below was checked against the tree at 3d4e235d, not inferred. Confirmed non-issues are recorded too, so nobody re-derives them.


The one that bites silently

TimelineEvent.CreatedAt became *time.Time (#658)

internal/commands/timeline.go:393

if !e.CreatedAt.IsZero() {
    timeStr = e.CreatedAt.Local().Format("15:04")
}

Go auto-dereferences a pointer for a value-receiver method call, so this compiles untouched and panics when the server omits the field. It is the only class-B site in the CLI, and it is in a display path that runs on every basecamp timeline.

Nine other wrapper timestamps pointerize in the same sweep (#615 + #658). I checked all ten:

Field CLI site Verdict
TimelineEvent.CreatedAt timeline.go:393 panics — method call
SearchResult.CreatedAt search.go:379 relativeTime(r.CreatedAt) compile error — a function argument gets no auto-deref, so this one is caught
QuestionReminder.RemindAt checkins.go:131 output shape only — see below
HillChart.UpdatedAt not read
Notification.ReadAt / .UnreadAt not read
SearchResult.UpdatedAt not read
ClientApprovalResponse.CreatedAt / .UpdatedAt type unused in the CLI
WebhookDelivery.CreatedAt type unused in the CLI

The compile/panic split is the whole point: search.go will stop the build, timeline.go will not.


Compile breaks — the bump will not build until these are handled

Site What moved Note
internal/tui/resolve/comment_thread.go:141 Recordings().Get removed (#619) Delete it — see below
internal/commands/schedule.go:642 UpdateScheduleEntryRequest fields → pointers (#632) read the ParticipantIDs trap below
internal/commands/gauges.go:61 Gauges().List gained options + result struct (#617)
internal/commands/gauges.go:104 Gauges().ListNeedles same (#617)
internal/commands/gauges.go:274 UpdateGaugeNeedleRequest.Description*string (#560)
internal/commands/cards.go:2938, assign.go:647, assign.go:749 UpdateStepRequest.DueOn*string (#647)
internal/commands/reports.go:298, reports.go:306 UpcomingSchedule returns reduced types, Assignables gone (#648)

Recordings().Get is dead code calling a route that never existed

The plan going in assumed this needed a redesign. It does not. FetchCommentThread — the only caller of Recordings().Gethas no callers of its own, in internal/, in tests, or in e2e/. Its only occurrence in the repo is its own definition.

It also never worked. Per the SDK's route corrections, bc3 draws resources :recordings, only: [], so the flat show does not exist, and app/views/api/recordings/ holds only partials, so the bucket-scoped show cannot render on the API host either. GetRecording was a 404 in every shape. Anyone who had reached this function would have got an error.

Action: delete FetchCommentThread. No replacement, no redesign, nothing to preserve. Check whether NewCommentThread and the rest of comment_thread.go still have live callers while you are in there.

ParticipantIDs is the sharp edge in schedule.go

*[]int64, where nil and empty are different instructions:

ParticipantIDs: nil,                        // leave participants alone
ParticipantIDs: basecamp.Ptr([]int64{}),    // remove EVERY participant

A slice built by filtering is basecamp.Ptr(ids) either way, so a filter matching nothing clears the list instead of leaving it. Guard on len(ids) > 0 when porting schedule.go:642.


Silent behaviour changes

files update on a document: three requests, and the explicit clear may inverse

The highest-risk item here, because it is the one place where the CLI and the SDK now both try to solve the same problem in opposite directions.

internal/commands/files.go:1770 buildDocumentUpdateRequest already does its own read-modify-write, and its comment states the mechanism it depends on:

BC3 rebuilds documents from permitted params on PUT, so omitted title/content fields are replaced with empty values. […] Explicit clears via --title "" or --content "" work by composition: the SDK strips empty strings to absent JSON fields, and the controller then nulls those absent fields during rebuild.

#601 makes Documents().Update a read-modify-write with preserve-on-omission. That inverts the second half: an absent field is now preserved rather than nulled. So --title "" — which the CLI implements by producing an absent field — plausibly stops clearing and starts preserving.

Two consequences at files.go:1652 and files.go:1690:

  1. Three HTTP requests where there was one — the CLI's Documents().Get, then the SDK's internal GET and PUT.
  2. --title "" / --content "" may silently stop clearing.

TestFilesUpdateDocumentEmptyTitleClearsWhilePreservingContent pins the wire shape and should catch it — confirm that it does rather than assuming, since it asserts on a request the SDK now wraps. Likely resolution: delete the CLI's copy and let the SDK own preservation, but that is a behaviour decision about what --title "" means, not a mechanical deletion.

Other read-modify-write sites

Todolists().Update (#574) at todolists.go:435 and tui/workspace/data/hub.go:1136; Schedules().UpdateEntry (#632) at schedule.go:724. Same shape: two round-trips, non-atomic last-write-wins, omission no longer clears.

Cards().Update dropped its preservation GetCard (#647)

cards.go:996, cards.go:1140, assign.go:601, assign.go:714, hub.go:1050. Signature and request type unchanged, so every call site compiles. What moves is the request count, the hook sequence, and the encoding of a due-date clear. Cross-check against the #604/#620 work, which was specifically about not echoing back fields the caller never set.

Schedules().CreateEntry stopped validating timestamps (#664)

schedule.go:560 and hub.go:1155. The SDK's local ErrUsage guard on RFC3339 StartsAt/EndsAt is gone; the value now goes on the wire verbatim. A bare date creates an all-day entry where v0.12.0 refused it locally, and a malformed value reaches bc3 instead of failing with CodeUsage. If the CLI was relying on that error as its input validation, it now has none — check whether schedule.go validates these itself before deciding whether to add a guard.

QuestionReminder.RemindAt*time.Time + omitempty

checkins.go:131 puts it straight into a map[string]any, so it compiles and cannot panic. The JSON output changes: a nil now marshals as null (or is omitted) where it previously emitted 0001-01-01T00:00:00Z. Agent-facing output shape — worth a deliberate choice, not a shrug.


--page is not aligned — the CLI is now over-restrictive

The plan expected the CLI's --page handling to already match. It does not, in an interesting direction.

Five commands hard-reject anything past the first page:

internal/commands/schedule.go:166   only --page 1 is supported; use --all to fetch everything
internal/commands/forwards.go:103   (same)
internal/commands/forwards.go:459   (same)
internal/commands/events.go:51      (same)
internal/commands/projects.go:85    (same)

That guard is correct today, because v0.12.0's Page disabled pagination and returned page 1 regardless of the number. #561/#617 make Page an actual page selector: Page: 3 now issues ?page=3 and populates Meta.Truncated.

So after the bump the restriction blocks a capability that works. The bump should lift these five guards, not preserve them. cards.go:288-308 and accountwide.go already do range and mutual-exclusion checking that stays valid — and --page > MaxInt32 now matches the SDK's own ErrUsage("page is out of range").

skills/basecamp/SKILL.md:156 (--page 1 # First page only, no auto-pagination) becomes incomplete rather than wrong; update it in the same change.


Test fixtures keyed on paths that moved

internal/commands/forwards_test.go:27

forwardsInboxPath = "/99999/inboxes/555/forwards.json"

#586 repoints this to /inbox_forwards.json. The old path 404'd against bc3, so no working call is lost — but the test double registered on the old path stops matching. The other #586 change (todolists/{groupId}/position.jsontodolists/groups/{groupId}/position.json) has no CLI call site; todolists_test.go:330 refers to the todoset position route, which is untouched.

The nine #619 bucket-scoping rewrites also change URLs, but they change signatures too, so the compiler surfaces them. None of the nine are used here — see below.


Reporting only — not gating

internal/observability/collector.go:118 (RecordOperationFromSDK) and hooks.go:74,101 key metrics and trace logs on op.Service / op.Operation straight from the SDK's OperationInfo. Where an update became read-modify-write, hooks now observe {Documents, Get} + {Documents, Replace} instead of {Documents, Update} — likewise for Todolists and schedule entries. Metric and trace names shift; dashboards and any operation allowlist need repointing. Nothing breaks.

Also expect error text to change (#541/#549): 400/422 now fold field detail into the message, and a top-level message key is honoured at every status. Error.FieldErrors is the structured replacement. Any string match on a message is dead — worth a grep during the bump.


Confirmed non-issues — do not re-check these

  • WithMaxRetries appears nowhere in non-test code, so the Disable keyring in installer canary #571 max_retries 0-or-1 token-refresh trap does not apply. The tests that pass WithMaxRetries(1) use static token providers.
  • No file imports basecamp-sdk/go/pkg/generated. The andon cord holds. That takes out the two largest class-B entries wholesale: the ~107 pointerized generated fields (Render GFM tables in message and comment bodies (#405) #560) and Question.Schedule.Hour/.Minute.
  • Todos().Trash is not called anywhere. It was the one removal that took away a working call, and it archived despite its name. Nothing to decide, and nothing to sed.
  • None of the nine Drive the circuit breaker's clock from tests, not sleep() #619 bucket-scoped operations are used: the five campfire chatbot calls, ClientApprovals().List, ClientCorrespondences().List, ClientReplies().List, ClientReplies().Get.
  • Forwards().CreateReply is not called. Forwards().List, Get, GetInbox, ListReplies, GetReply are, and all survive.
  • TodolistGroups().Update is not called, so its removal in favour of Replace (Installer canary failure #574) costs nothing.

Sequence when the tag lands

  1. make bump-sdk — never edit go.mod by hand, and never a local replace.
  2. go build ./... — the seven compile-break rows above, plus search.go:379.
  3. Fix timeline.go:393 before trusting any manual testing. It is the only site that will not announce itself.
  4. Decide the files update document question deliberately; it is a behaviour change, not a port.
  5. Lift the five --page guards and update SKILL.md.
  6. Repoint forwards_test.go:27.
  7. make test, then bin/ci.
  8. API-COVERAGE.md — and note that Add basecamp files versions — HELD, blocked on the SDK #622 stays blocked regardless: sdk#649 is open with no fix PR and is not in v0.13.0, so basecamp files versions does not unblock at this tag.

Tracking the tag itself: sdk#667.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions