Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ Update the `revision` and `date` fields, then `make provenance-sync`. This is no

The Smithy service version is derived from the shared provenance date. Run `make sync-spec-version` (or `make smithy-build`, which does this automatically) after updating provenance.

**Pin semantics.** The pin is the conformance baseline as of the last sync — it asserts that all upstream drift up to that revision has been *triaged*, not that every contract in it has been *absorbed*. Upstream contracts shipped past the SDK's modeled surface are tracked in `spec/api-gaps/` (status `addressed-in-bc3-pr-N`) until an absorption PR lands. A repin is valid exactly when every drift item in `pin..HEAD` is either absorbed into the spec or registered in `spec/api-gaps/`; it is not blocked on absorption itself. The pin never moves backward. The `compatibility.*` pins mark the last **verified API-surface state** of their branch, not a last-glanced timestamp — refresh one only when re-verifying that branch's API surface, and record verification dates in the PR that did the checking.

### Pre-sync

Use `make sync-status` to see upstream diffs since last sync.
Expand Down
9 changes: 9 additions & 0 deletions COORDINATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ live BC5 by the #11629 tooling. The historical server-side audit lived on the
`five+api` branch under `tmp/api_audit.md` (generated by the BC3 plan's
`script/api/audit`).

The SDK's conformance baseline is the pin in
[`spec/api-provenance.json`](spec/api-provenance.json) — `bc3` `master`
`ba105ba7` as of the 2026-07-22 sync. Contracts documented past the SDK's
modeled surface are registered in [`spec/api-gaps/`](spec/api-gaps/) until
absorbed (see AGENTS.md §Provenance is Mandatory for the pin semantics).
The `compatibility.bc3-four` pin stays at `9d73959a` (2026-06-12): the
`four` branch was re-verified on 2026-07-22 with **zero API drift** since —
no changes under `doc/api/` or the API controllers/views.

## Division of labor

- **BC3 plan owns**: server-side audit, jbuilder + controller + doc
Expand Down
3 changes: 2 additions & 1 deletion behavior-model.json
Original file line number Diff line number Diff line change
Expand Up @@ -2594,7 +2594,8 @@
"$.avatar_url"
],
"OutOfOfficePerson": [
"$.name"
"$.name",
"$.avatar_url"
],
"Person": [
"$.name",
Expand Down
4 changes: 2 additions & 2 deletions go/pkg/basecamp/api-provenance.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"bc3": {
"branch": "master",
"revision": "e52453ff35a6292473eb095a8568649aa132fef9",
"date": "2026-07-17"
"revision": "ba105ba7d7e48bd97afdc98305e9fb8a63a88beb",
"date": "2026-07-22"
},
"compatibility": {
"bc3-four": {
Expand Down
18 changes: 11 additions & 7 deletions go/pkg/basecamp/people.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,18 +534,22 @@ type UpdateMyPreferencesRequest struct {
}

// OutOfOffice represents out-of-office status for a person.
// When out of office is not enabled, Enabled is false and StartDate,
// EndDate, and BackOnDate are omitted from the response.
type OutOfOffice struct {
Enabled bool `json:"enabled,omitempty"`
StartDate string `json:"start_date,omitempty"`
EndDate string `json:"end_date,omitempty"`
Ongoing bool `json:"ongoing,omitempty"`
Person OutOfOfficePerson `json:"person,omitempty"`
Enabled bool `json:"enabled,omitempty"`
StartDate string `json:"start_date,omitempty"`
EndDate string `json:"end_date,omitempty"`
BackOnDate string `json:"back_on_date,omitempty"`
Ongoing bool `json:"ongoing,omitempty"`
Person OutOfOfficePerson `json:"person,omitempty"`
}

// OutOfOfficePerson represents the person associated with an out-of-office status.
type OutOfOfficePerson struct {
ID int64 `json:"id"`
Name string `json:"name,omitempty"`
ID int64 `json:"id"`
Name string `json:"name,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
}

// EnableOutOfOfficeRequest specifies the parameters for enabling out-of-office.
Expand Down
54 changes: 54 additions & 0 deletions go/pkg/basecamp/people_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,3 +440,57 @@ func TestPeopleService_UpdateMyProfileClearsField(t *testing.T) {
t.Errorf("expected bio to be empty string, got %v", bio)
}
}

func TestOutOfOffice_Unmarshal(t *testing.T) {
data := loadPeopleFixture(t, "out-of-office.json")

var ooo OutOfOffice
if err := json.Unmarshal(data, &ooo); err != nil {
t.Fatalf("failed to unmarshal out-of-office.json: %v", err)
}

if !ooo.Enabled {
t.Error("expected enabled to be true")
}
if !ooo.Ongoing {
t.Error("expected ongoing to be true")
}
if ooo.StartDate != "2026-07-20" {
t.Errorf("expected start_date '2026-07-20', got %q", ooo.StartDate)
}
if ooo.EndDate != "2026-07-26" {
t.Errorf("expected end_date '2026-07-26', got %q", ooo.EndDate)
}
if ooo.BackOnDate != "2026-07-27" {
t.Errorf("expected back_on_date '2026-07-27', got %q", ooo.BackOnDate)
}
if ooo.Person.ID != 1049715913 {
t.Errorf("expected person id 1049715913, got %d", ooo.Person.ID)
}
if ooo.Person.Name != "Victor Cooper" {
t.Errorf("expected person name 'Victor Cooper', got %q", ooo.Person.Name)
}
if ooo.Person.AvatarURL == "" {
t.Error("expected non-empty person avatar_url")
}
}

func TestOutOfOffice_UnmarshalDisabled(t *testing.T) {
data := []byte(`{"person":{"id":1049715913,"name":"Victor Cooper"},"enabled":false,"ongoing":false}`)

var ooo OutOfOffice
if err := json.Unmarshal(data, &ooo); err != nil {
t.Fatalf("failed to unmarshal disabled out-of-office: %v", err)
}

if ooo.Enabled {
t.Error("expected enabled to be false")
}
if ooo.Ongoing {
t.Error("expected ongoing to be false")
}
if ooo.StartDate != "" || ooo.EndDate != "" || ooo.BackOnDate != "" {
t.Errorf("expected omitted dates to be empty, got start=%q end=%q back_on=%q",
ooo.StartDate, ooo.EndDate, ooo.BackOnDate)
}
}
2 changes: 1 addition & 1 deletion go/pkg/basecamp/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ package basecamp
const Version = "0.8.0"

// APIVersion is the Basecamp API version this SDK targets.
const APIVersion = "2026-07-17"
const APIVersion = "2026-07-22"
72 changes: 45 additions & 27 deletions go/pkg/generated/client.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ data class BasecampConfig(
) {
companion object {
const val VERSION = "0.8.0"
const val API_VERSION = "2026-07-17"
const val API_VERSION = "2026-07-22"
const val DEFAULT_BASE_URL = "https://3.basecampapi.com"
const val DEFAULT_USER_AGENT = "basecamp-sdk-kotlin/$VERSION (api:$API_VERSION)"
const val DEFAULT_MAX_RETRIES = 3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class ReportsService(client: AccountClient) : BaseService(client) {
}

/**
* Get upcoming schedule entries within a date window
* Get upcoming schedule entries and assignable items within a date window.
* @param options Optional query parameters and pagination control
*/
suspend fun upcoming(options: GetUpcomingScheduleOptions? = null): JsonElement {
Expand Down
26 changes: 19 additions & 7 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "Basecamp",
"version": "2026-07-17",
"version": "2026-07-22",
"description": "Basecamp API",
"contact": {
"name": "Basecamp",
Expand Down Expand Up @@ -8790,7 +8790,7 @@
},
"/{accountId}/my/readings.json": {
"get": {
"description": "Get the current user's notification inbox (the \"Hey!\" menu).\nNotifications are grouped into unreads, reads, and memories.\nReads are paginated (50 per page). Unreads are capped at 100.",
"description": "Get the current user's notification inbox (the \"Hey!\" menu).\nNotifications are grouped into unreads, reads, bubble-ups, and\nscheduled bubble-ups (`memories` remains as an always-empty\nplaceholder on BC5). Reads are paginated (50 per page). Unreads are\ncapped at 100. Bubble-ups are capped per `limit_bubble_ups`.",
"operationId": "GetMyNotifications",
"parameters": [
{
Expand Down Expand Up @@ -14950,7 +14950,7 @@
},
"/{accountId}/reports/schedules/upcoming.json": {
"get": {
"description": "Get upcoming schedule entries within a date window",
"description": "Get upcoming schedule entries and assignable items within a date window.\nThis endpoint is preserved as the canonical API path on BC5;\nthe BC5 `/calendar` web view is HTML-only.",
"operationId": "GetUpcomingSchedule",
"parameters": [
{
Expand Down Expand Up @@ -24654,7 +24654,8 @@
"x-go-type-skip-optional-pointer": true
},
"section": {
"type": "string"
"type": "string",
"description": "The notification category: `inbox`, `chats`, `pings`, `bubbles`,\nor `mentions`."
},
"unread_count": {
"type": "integer",
Expand Down Expand Up @@ -24758,6 +24759,7 @@
},
"OutOfOffice": {
"type": "object",
"description": "When out of office is not enabled, `enabled` is `false` and\n`start_date`, `end_date`, and `back_on_date` are omitted.",
"properties": {
"person": {
"$ref": "#/components/schemas/OutOfOfficePerson"
Expand All @@ -24773,6 +24775,10 @@
},
"end_date": {
"type": "string"
},
"back_on_date": {
"type": "string",
"description": "First working day after the out-of-office window ends.\nOmitted when out of office is not enabled."
}
}
},
Expand Down Expand Up @@ -24804,6 +24810,10 @@
"name": {
"type": "string",
"format": "password"
},
"avatar_url": {
"type": "string",
"format": "password"
}
},
"required": [
Expand Down Expand Up @@ -24976,7 +24986,8 @@
"type": "string"
},
"time_zone_name": {
"type": "string"
"type": "string",
"description": "Returned as a Rails-style name (e.g. \"Central Time (US & Canada)\")."
},
"first_week_day": {
"type": "string"
Expand All @@ -24991,7 +25002,7 @@
"properties": {
"time_zone_name": {
"type": "string",
"description": "Time zone name (e.g. \"America/Chicago\", \"London\", \"UTC\")"
"description": "Time zone name. Accepts any valid Rails time zone name (e.g.\n\"London\", \"UTC\") as well as IANA identifiers (e.g.\n\"America/Chicago\"), which are normalized to the matching\nRails-style name before saving."
},
"first_week_day": {
"type": "string",
Expand Down Expand Up @@ -27880,7 +27891,8 @@
"type": "array",
"items": {
"$ref": "#/components/schemas/WebhookDelivery"
}
},
"description": "Up to the 25 most recent delivery exchanges, most recent first.\nEmpty when the webhook hasn't delivered anything yet."
}
},
"required": [
Expand Down
2 changes: 1 addition & 1 deletion python/src/basecamp/_version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
VERSION = "0.8.0"
API_VERSION = "2026-07-17"
API_VERSION = "2026-07-22"
2 changes: 2 additions & 0 deletions python/src/basecamp/generated/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,7 @@ class Notification(TypedDict):


class OutOfOffice(TypedDict):
back_on_date: NotRequired[str]
enabled: NotRequired[bool]
end_date: NotRequired[str]
ongoing: NotRequired[bool]
Expand All @@ -932,6 +933,7 @@ class OutOfOfficePayload(TypedDict):


class OutOfOfficePerson(TypedDict):
avatar_url: NotRequired[str]
id: int
name: NotRequired[str]

Expand Down
Loading
Loading