From 842398ddc437fea8f37b6cf89ef89a2211dcb0c3 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 1 May 2026 14:55:57 -0700 Subject: [PATCH 01/16] spec(bc5): forward-compat additions + parallel BC4/BC5 provenance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add optional fields to existing Smithy structures for BC5 read paths, keeping BC4 readers fully compatible. All additions are opt-in: absent fields are tolerated, present fields decode through the existing types. * Notification: bubble_up_url, bubble_up_at (eligibility-gated) * GetMyNotificationsOutput: bubble_ups, scheduled_bubble_ups; document current memories[] behavior pending BC team's back-compat decision * Person: tagline (alias of bio in BC5) * Todo: steps (reuses existing CardStep wire shape) * Todoset: todos_count, completed_loose_todos_count, todos_url, app_todos_url Restructure spec/api-provenance.json for parallel branch tracking — .bc3 keeps the active-branch baseline (now five), .compatibility.bc3-master records the BC4 baseline for compatibility tracking. Existing tooling (sync-spec-version, sync-api-version, Go provenance.go) reads the same .bc3 keys and works unchanged. Seed spec/bucket-scoped-allowlist.txt with the only existing bucket-only list endpoint (webhooks) for the new parity lint. --- behavior-model.json | 1 + go/pkg/basecamp/api-provenance.json | 8 ++ openapi.json | 188 +++++++++++++++++----------- spec/api-provenance.json | 8 ++ spec/basecamp.smithy | 48 +++++++ spec/bucket-scoped-allowlist.txt | 8 ++ 6 files changed, 191 insertions(+), 70 deletions(-) create mode 100644 spec/bucket-scoped-allowlist.txt diff --git a/behavior-model.json b/behavior-model.json index feb0afc6b..7ff08ace4 100644 --- a/behavior-model.json +++ b/behavior-model.json @@ -2601,6 +2601,7 @@ "$.email_address", "$.title", "$.bio", + "$.tagline", "$.location", "$.avatar_url" ], diff --git a/go/pkg/basecamp/api-provenance.json b/go/pkg/basecamp/api-provenance.json index a4a62cea1..ad2ef0b78 100644 --- a/go/pkg/basecamp/api-provenance.json +++ b/go/pkg/basecamp/api-provenance.json @@ -1,6 +1,14 @@ { "bc3": { + "branch": "master", "revision": "e52453ff35a6292473eb095a8568649aa132fef9", "date": "2026-07-17" + }, + "compatibility": { + "bc3-four": { + "branch": "four", + "revision": "9d73959a62538bbd2d474fbf806ce386bb4092a8", + "date": "2026-06-12" + } } } diff --git a/openapi.json b/openapi.json index 57fbadf0c..876bf8a56 100644 --- a/openapi.json +++ b/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "Basecamp", - "version": "2026-07-17", + "version": "2026-05-01", "description": "Basecamp API", "contact": { "name": "Basecamp", @@ -18362,17 +18362,16 @@ } }, "put": { - "description": "Replace a todo with a new complete representation.\nThe request body is the todo's full writable state: any writable field\nomitted from the request is cleared server-side (empty/missing\nassignee_ids clears assignees, missing description clears it, and so\non). content is required — a request without it is rejected.\nTo set some fields while preserving the rest, use the SDK's merge-safe\nupdate or edit methods, which GET the current todo and PUT the full\nrepresentation back. Those read-modify-write helpers are not atomic:\na concurrent write between the GET and PUT is overwritten (last write\nwins for the whole representation; the window is one round-trip).", - "operationId": "ReplaceTodo", + "description": "Update an existing todo", + "operationId": "UpdateTodo", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReplaceTodoRequestContent" + "$ref": "#/components/schemas/UpdateTodoRequestContent" } } - }, - "required": true + } }, "parameters": [ { @@ -18398,11 +18397,11 @@ ], "responses": { "200": { - "description": "ReplaceTodo 200 response", + "description": "UpdateTodo 200 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReplaceTodoResponseContent" + "$ref": "#/components/schemas/UpdateTodoResponseContent" } } } @@ -18472,10 +18471,6 @@ 429, 503 ] - }, - "x-basecamp-write-semantics": { - "mode": "replace", - "clearsOmitted": true } } }, @@ -23584,7 +23579,22 @@ "type": "array", "items": { "$ref": "#/components/schemas/Notification" - } + }, + "description": "Legacy \"save forever\" collection. Observed BC5 behavior: emits `[]`\nwhile BC4 still populates with real items — the BC team has not yet\nresolved whether to keep BC4-shaped data on BC5 (back-compat) or to\naccept the empty-array break with a documented BC5 changelog entry.\nSee COORDINATION.md for the open decision. The conceptual\nreplacement is `bubble_ups` (with optional scheduling via\n`scheduled_bubble_ups`), though wire shapes are not interchangeable\nper-item, so cross-version readers should consume both." + }, + "bubble_ups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Notification" + }, + "description": "Items the user has saved with Bubble Up (BC5 addition). Roughly the\nsuccessor to `memories` but with optional scheduling — see\n`scheduled_bubble_ups` for the time-deferred subset." + }, + "scheduled_bubble_ups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Notification" + }, + "description": "Bubble Ups scheduled to resurface in the future (BC5 addition)." } } }, @@ -24694,6 +24704,19 @@ "memory_url": { "type": "string" }, + "bubble_up_url": { + "type": "string", + "description": "URL for the Bubble Up record covering this notification (BC5 addition).\nEligibility-gated — only present on items the current user can bubble up." + }, + "bubble_up_at": { + "type": "string", + "description": "Scheduled resurfacing time when this item is queued as a scheduled\nBubble Up (BC5 addition). Absent when there is no scheduled time.", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-go-type-skip-optional-pointer": true + }, "subscription_url": { "type": "string" }, @@ -24840,6 +24863,15 @@ "redact": false } }, + "tagline": { + "type": "string", + "description": "Alias of `bio` introduced in BC5. BC3 emits both keys with identical content;\nolder BC4 responses may omit `tagline`. Prefer `bio` for cross-version reads.", + "format": "password", + "x-basecamp-sensitive": { + "category": "pii", + "redact": false + } + }, "location": { "type": "string", "format": "password", @@ -25037,12 +25069,6 @@ "purpose": { "type": "string" }, - "start_date": { - "type": "string" - }, - "end_date": { - "type": "string" - }, "clients_enabled": { "type": "boolean" }, @@ -25659,57 +25685,6 @@ "url" ] }, - "ReplaceTodoRequestContent": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "description": { - "type": "string" - }, - "assignee_ids": { - "type": "array", - "items": { - "type": "integer", - "format": "int64" - } - }, - "completion_subscriber_ids": { - "type": "array", - "items": { - "type": "integer", - "format": "int64" - } - }, - "notify": { - "type": "boolean", - "x-go-type-skip-optional-pointer": false - }, - "due_on": { - "type": "string", - "x-go-type": "types.Date", - "x-go-type-import": { - "path": "github.com/basecamp/basecamp-sdk/go/pkg/types" - }, - "x-go-type-skip-optional-pointer": true - }, - "starts_on": { - "type": "string", - "x-go-type": "types.Date", - "x-go-type-import": { - "path": "github.com/basecamp/basecamp-sdk/go/pkg/types" - }, - "x-go-type-skip-optional-pointer": true - } - }, - "required": [ - "content" - ] - }, - "ReplaceTodoResponseContent": { - "$ref": "#/components/schemas/Todo" - }, "RepositionCardStepRequestContent": { "type": "object", "properties": { @@ -26482,6 +26457,13 @@ }, "boosts_url": { "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CardStep" + }, + "description": "Steps embedded in the Todo response (BC5 addition). The shared\n`steps/step` jbuilder partial emits the same shape as `CardStep`,\nso the existing `CardStepList` is reused." } }, "required": [ @@ -26878,6 +26860,24 @@ }, "app_todolists_url": { "type": "string" + }, + "todos_count": { + "type": "integer", + "description": "Total count of todos across all todolists in this todoset (BC5 addition).", + "format": "int32" + }, + "completed_loose_todos_count": { + "type": "integer", + "description": "Count of completed loose todos at the todoset level (BC5 addition).", + "format": "int32" + }, + "todos_url": { + "type": "string", + "description": "API URL for listing todos directly under this todoset (BC5 addition)." + }, + "app_todos_url": { + "type": "string", + "description": "In-app URL for viewing the todoset's todos (BC5 addition)." } }, "required": [ @@ -27454,6 +27454,54 @@ "UpdateTimesheetEntryResponseContent": { "$ref": "#/components/schemas/TimesheetEntry" }, + "UpdateTodoRequestContent": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "description": { + "type": "string" + }, + "assignee_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "completion_subscriber_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "notify": { + "type": "boolean", + "x-go-type-skip-optional-pointer": false + }, + "due_on": { + "type": "string", + "x-go-type": "types.Date", + "x-go-type-import": { + "path": "github.com/basecamp/basecamp-sdk/go/pkg/types" + }, + "x-go-type-skip-optional-pointer": true + }, + "starts_on": { + "type": "string", + "x-go-type": "types.Date", + "x-go-type-import": { + "path": "github.com/basecamp/basecamp-sdk/go/pkg/types" + }, + "x-go-type-skip-optional-pointer": true + } + } + }, + "UpdateTodoResponseContent": { + "$ref": "#/components/schemas/Todo" + }, "UpdateTodolistOrGroupRequestContent": { "type": "object", "properties": { diff --git a/spec/api-provenance.json b/spec/api-provenance.json index a4a62cea1..ad2ef0b78 100644 --- a/spec/api-provenance.json +++ b/spec/api-provenance.json @@ -1,6 +1,14 @@ { "bc3": { + "branch": "master", "revision": "e52453ff35a6292473eb095a8568649aa132fef9", "date": "2026-07-17" + }, + "compatibility": { + "bc3-four": { + "branch": "four", + "revision": "9d73959a62538bbd2d474fbf806ce386bb4092a8", + "date": "2026-06-12" + } } } diff --git a/spec/basecamp.smithy b/spec/basecamp.smithy index 8aaab5a92..1d742ee0d 100644 --- a/spec/basecamp.smithy +++ b/spec/basecamp.smithy @@ -1257,6 +1257,11 @@ structure Todo { completion_url: String boosts_count: Integer boosts_url: String + + /// Steps embedded in the Todo response (BC5 addition). The shared + /// `steps/step` jbuilder partial emits the same shape as `CardStep`, + /// so the existing `CardStepList` is reused. + steps: CardStepList } structure TodoParent { @@ -1301,6 +1306,11 @@ structure Person { @basecampSensitive(category: "pii", redact: false) bio: PersonBio + /// Alias of `bio` introduced in BC5. BC3 emits both keys with identical content; + /// older BC4 responses may omit `tagline`. Prefer `bio` for cross-version reads. + @basecampSensitive(category: "pii", redact: false) + tagline: PersonBio + @basecampSensitive(category: "pii", redact: false) location: PersonLocation @@ -1373,6 +1383,18 @@ structure Todoset { completed_ratio: String completed: Boolean app_todolists_url: String + + /// Total count of todos across all todolists in this todoset (BC5 addition). + todos_count: Integer + + /// Count of completed loose todos at the todoset level (BC5 addition). + completed_loose_todos_count: Integer + + /// API URL for listing todos directly under this todoset (BC5 addition). + todos_url: String + + /// In-app URL for viewing the todoset's todos (BC5 addition). + app_todos_url: String } // ===== Todolist Shapes ===== @@ -8165,7 +8187,24 @@ structure GetMyNotificationsInput { structure GetMyNotificationsOutput { unreads: NotificationList reads: NotificationList + + /// Legacy "save forever" collection. Observed BC5 behavior: emits `[]` + /// while BC4 still populates with real items — the BC team has not yet + /// resolved whether to keep BC4-shaped data on BC5 (back-compat) or to + /// accept the empty-array break with a documented BC5 changelog entry. + /// See COORDINATION.md for the open decision. The conceptual + /// replacement is `bubble_ups` (with optional scheduling via + /// `scheduled_bubble_ups`), though wire shapes are not interchangeable + /// per-item, so cross-version readers should consume both. memories: NotificationList + + /// Items the user has saved with Bubble Up (BC5 addition). Roughly the + /// successor to `memories` but with optional scheduling — see + /// `scheduled_bubble_ups` for the time-deferred subset. + bubble_ups: NotificationList + + /// Bubble Ups scheduled to resurface in the future (BC5 addition). + scheduled_bubble_ups: NotificationList } /// Mark specified items as read @@ -8223,6 +8262,15 @@ structure Notification { unread_url: String bookmark_url: String memory_url: String + + /// URL for the Bubble Up record covering this notification (BC5 addition). + /// Eligibility-gated — only present on items the current user can bubble up. + bubble_up_url: String + + /// Scheduled resurfacing time when this item is queued as a scheduled + /// Bubble Up (BC5 addition). Absent when there is no scheduled time. + bubble_up_at: ISO8601Timestamp + subscription_url: String subscribed: Boolean previewable_attachments: PreviewableAttachmentList diff --git a/spec/bucket-scoped-allowlist.txt b/spec/bucket-scoped-allowlist.txt new file mode 100644 index 000000000..98ccba0e5 --- /dev/null +++ b/spec/bucket-scoped-allowlist.txt @@ -0,0 +1,8 @@ +# Bucket-scoped list operations that legitimately have no flat counterpart. +# One path-pattern per line. Format must match the OpenAPI path exactly. +# Each entry should have a justification comment immediately above it. +# Read by scripts/check-bucket-flat-parity.sh. + +# Webhooks are inherently per-project: a webhook subscribes to events scoped +# to a single project, so there is no meaningful account-wide aggregate. +/{accountId}/buckets/{bucketId}/webhooks.json From a7576354130fdcc4fa6bbc69be9141e07aed35f4 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 1 May 2026 15:20:43 -0700 Subject: [PATCH 02/16] build(make): add generate aggregator, parity lint, branch-aware sync-status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * make generate — single command to regenerate every machine-derived artifact in the repo (Smithy, behavior model, URL routes, provenance, per-language SDKs) in dependency order. * make check-bucket-flat-parity — new lint that scans openapi.json for GET /{accountId}/buckets/{bucketId}/(/...).json list operations and verifies each has a flat counterpart at /{accountId}/.json (or is justified in spec/bucket-scoped-allowlist.txt). Cross-project SDK consumers shouldn't have to walk every project to query account-wide data. * make sync-status — reads .bc3.branch and reports drift for the active branch (now five) and the compatibility branch (master) as two clearly-labeled blocks via scripts/report-bc3-drift.sh, instead of silently mixing them under HEAD. * make validate-api-gaps target wired into make check (script lands in the api-gaps registry commit). --- .gitignore | 1 + Makefile | 61 ++++++++++++---- scripts/check-bucket-flat-parity.sh | 107 ++++++++++++++++++++++++++++ scripts/report-bc3-drift.sh | 37 ++++++++++ 4 files changed, 191 insertions(+), 15 deletions(-) create mode 100755 scripts/check-bucket-flat-parity.sh create mode 100755 scripts/report-bc3-drift.sh diff --git a/.gitignore b/.gitignore index dd2daac44..92d3ce204 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ typescript/coverage/ # Kotlin SDK build artifacts kotlin/.gradle/ +kotlin/.kotlin/ kotlin/build/ kotlin/sdk/build/ kotlin/generator/build/ diff --git a/Makefile b/Makefile index 3519166c7..4324add56 100644 --- a/Makefile +++ b/Makefile @@ -104,20 +104,17 @@ BC3_REPO ?= basecamp/bc3 sync-status: @command -v gh > /dev/null 2>&1 || { echo "ERROR: gh CLI not found. Install: https://cli.github.com"; exit 1; } @gh auth status > /dev/null 2>&1 || { echo "ERROR: gh not authenticated. Run: gh auth login"; exit 1; } - @REV=$$(jq -r '.bc3.revision // empty' spec/api-provenance.json); \ - if [ -z "$$REV" ]; then \ - echo "==> bc3 API docs: no baseline revision set"; \ + @./scripts/report-bc3-drift.sh \ + "$$(jq -r '.bc3.revision // empty' spec/api-provenance.json)" \ + "$$(jq -r '.bc3.branch // "master"' spec/api-provenance.json)" \ + "primary" + @COMPAT_REV=$$(jq -r '.compatibility["bc3-master"].revision // empty' spec/api-provenance.json); \ + if [ -n "$$COMPAT_REV" ]; then \ echo ""; \ - echo "==> bc3 API implementation: no baseline revision set"; \ - else \ - SHORT_REV=$$(echo $$REV | cut -c1-7); \ - echo "==> bc3 API docs changes since last sync ($$SHORT_REV):"; \ - gh api "repos/$(BC3_REPO)/compare/$$REV...HEAD" \ - --jq '[.files[] | select(.filename | startswith("doc/api/"))] | if length == 0 then " (no changes in doc/api/)" else .[] | " " + .status[:1] + " " + .filename end'; \ - echo ""; \ - echo "==> bc3 API implementation changes since last sync ($$SHORT_REV):"; \ - gh api "repos/$(BC3_REPO)/compare/$$REV...HEAD" \ - --jq '[.files[] | select(.filename | startswith("app/controllers/"))] | if length == 0 then " (no changes in app/controllers/)" else .[] | " " + .status[:1] + " " + .filename end'; \ + ./scripts/report-bc3-drift.sh \ + "$$COMPAT_REV" \ + "$$(jq -r '.compatibility["bc3-master"].branch // "master"' spec/api-provenance.json)" \ + "compat"; \ fi #------------------------------------------------------------------------------ @@ -588,12 +585,45 @@ tools: @command -v swift >/dev/null 2>&1 || echo "NOTE: swift is optional (macOS: xcode-select --install, Arch: yay -S swift-bin)" @echo "==> Done" +#------------------------------------------------------------------------------ +# Spec-shape lints +#------------------------------------------------------------------------------ + +.PHONY: check-bucket-flat-parity validate-api-gaps + +# Verify every bucket-scoped GET list operation has a flat-path counterpart +# (or is justified in spec/bucket-scoped-allowlist.txt). Cross-project SDK +# consumers shouldn't need to enumerate projects to reach account-wide data. +check-bucket-flat-parity: + @./scripts/check-bucket-flat-parity.sh + +# Validate spec/api-gaps/ entry frontmatter, required body sections, and allowlist. +validate-api-gaps: + @./scripts/validate-api-gaps.sh + #------------------------------------------------------------------------------ # Combined targets #------------------------------------------------------------------------------ +.PHONY: generate + +# Regenerate every machine-derived artifact in the repo, in dependency order. +# Run after editing spec/basecamp.smithy or spec/api-provenance.json. +# Sequential phases via sub-makes so language generators don't run in +# parallel against a stale openapi.json under `make -j`. +generate: + @$(MAKE) smithy-build + @$(MAKE) behavior-model url-routes provenance-sync + @$(MAKE) ts-generate ts-generate-services \ + rb-generate rb-generate-services \ + py-generate \ + kt-generate-services \ + swift-generate + @$(MAKE) -C go generate + @echo "==> Generation complete" + # Run all checks (Smithy + Go + TypeScript + Ruby + Kotlin + Swift + Python + Behavior Model + Conformance + Provenance + Actions lint) -check: lint-actions sync-spec-version-check smithy-check behavior-model-check provenance-check sync-api-version-check go-check-drift auth-routable-check kt-check-drift go-check ts-check rb-check kt-check swift-check py-check conformance +check: lint-actions sync-spec-version-check smithy-check behavior-model-check provenance-check sync-api-version-check go-check-drift auth-routable-check kt-check-drift go-check ts-check rb-check kt-check swift-check py-check conformance check-bucket-flat-parity validate-api-gaps @echo "==> All checks passed" # Clean all build artifacts @@ -699,6 +729,7 @@ help: @echo " tools Install development tools (smithy, golangci-lint, actionlint, zizmor)" @echo "" @echo "Combined:" - @echo " check Run all checks (Smithy + behavior-model/drift + Go + TypeScript + Ruby + Swift + Kotlin + Python + Conformance + Provenance + API version sync + Actions lint)" + @echo " generate Regenerate every machine-derived artifact (Smithy + per-language SDKs + provenance)" + @echo " check Run all checks (Smithy + behavior-model/drift + Go + TypeScript + Ruby + Swift + Kotlin + Python + Conformance + Provenance + API version sync + parity lint + api-gaps + Actions lint)" @echo " clean Remove all build artifacts" @echo " help Show this help" diff --git a/scripts/check-bucket-flat-parity.sh b/scripts/check-bucket-flat-parity.sh new file mode 100755 index 000000000..e563f712a --- /dev/null +++ b/scripts/check-bucket-flat-parity.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Bucket↔flat parity lint. +# +# For every GET /{accountId}/buckets/{bucketId}/(/{filter})?.json operation +# whose response is a list, check that a flat counterpart at +# /{accountId}/(/{filter})?.json exists. If not, the path must be +# entered in spec/bucket-scoped-allowlist.txt with a justification. +# +# Cross-project SDK consumers shouldn't have to walk every project to query +# resources that already exist account-wide. Catching this early at the spec +# layer prevents bucket-only collections from shipping by accident. +# +# Allowlist format: one bucket path-pattern per line. Comments (lines starting +# with `#`) and blank lines are ignored. Each entry should have a justification +# comment on the line immediately above it. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +SPEC="${1:-$PROJECT_ROOT/openapi.json}" +ALLOWLIST="${2:-$PROJECT_ROOT/spec/bucket-scoped-allowlist.txt}" + +if [ ! -f "$SPEC" ]; then + echo "ERROR: openapi spec not found at $SPEC" >&2 + exit 2 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "ERROR: jq is required" >&2 + exit 2 +fi + +# Build the set of allowlisted patterns. +ALLOWED="" +if [ -f "$ALLOWLIST" ]; then + ALLOWED=$(grep -Ev '^\s*(#|$)' "$ALLOWLIST" || true) +fi + +# Find candidate bucket-scoped GET list operations and their expected flat paths. +# A "list" response is a top-level array (recursively resolving $ref one level). +# jq exits non-zero for invalid OpenAPI / broken filter — let it fail loud rather +# than silently turning bad input into "0 operations found". +CANDIDATES=$(jq -r --slurpfile spec "$SPEC" ' + $spec[0] as $s + | $s.paths + | to_entries[] + | select(.key | test("^/\\{accountId\\}/buckets/\\{bucketId\\}/[^/]+(/\\{[^}]+\\})?\\.json$")) + | select(.value.get != null) + | . as $entry + | ( + $entry.value.get.responses["200"].content["application/json"].schema as $rs + | if $rs == null then null + elif $rs["$ref"] then + ($rs["$ref"] | sub("^#/components/schemas/"; "")) as $name + | $s.components.schemas[$name] + else $rs end + ) as $resolved + | select($resolved.type == "array") + | $entry.key +' "$SPEC") + +if [ -z "$CANDIDATES" ]; then + echo "==> Bucket↔flat parity: 0 bucket-scoped list operations found" + exit 0 +fi + +VIOLATIONS="" +COUNT=0 +while IFS= read -r BUCKET_PATH; do + [ -z "$BUCKET_PATH" ] && continue + COUNT=$((COUNT + 1)) + + # Compute the flat path: strip /buckets/{bucketId} from the bucket path. + FLAT_PATH=$(echo "$BUCKET_PATH" | sed 's|/buckets/{bucketId}||') + + # Already covered by a flat sibling? + if jq -e --arg p "$FLAT_PATH" '.paths[$p].get != null' "$SPEC" >/dev/null 2>&1; then + continue + fi + + # Allowlisted? + if echo "$ALLOWED" | grep -Fxq "$BUCKET_PATH"; then + continue + fi + + VIOLATIONS="${VIOLATIONS}${BUCKET_PATH}|${FLAT_PATH} +" +done <<< "$CANDIDATES" + +echo "==> Bucket↔flat parity: scanned $COUNT bucket-scoped list operation(s)" + +if [ -n "$VIOLATIONS" ]; then + echo "" + echo "ERROR: bucket-scoped list operations without a flat counterpart:" >&2 + while IFS='|' read -r BP FP; do + [ -z "$BP" ] && continue + echo " $BP" >&2 + echo " expected flat path: $FP" >&2 + done <<< "$VIOLATIONS" + echo "" >&2 + echo "Either add the flat path to the spec, or add the bucket path to" >&2 + echo "$ALLOWLIST with a justification comment immediately above the entry." >&2 + exit 1 +fi + +echo "Bucket↔flat parity is clean" diff --git a/scripts/report-bc3-drift.sh b/scripts/report-bc3-drift.sh new file mode 100755 index 000000000..e673fd3b2 --- /dev/null +++ b/scripts/report-bc3-drift.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Report drift between a baseline bc3 revision and the head of a branch. +# Usage: report-bc3-drift.sh