Skip to content

Fix --version over-propagation in check-cli-surface.sh - #499

Merged
robzolkos merged 1 commit into
basecamp:mainfrom
sawirricardo:fix-version-over-propagation
Jun 29, 2026
Merged

Fix --version over-propagation in check-cli-surface.sh#499
robzolkos merged 1 commit into
basecamp:mainfrom
sawirricardo:fix-version-over-propagation

Conversation

@sawirricardo

@sawirricardosawirricardo commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

scripts/check-cli-surface.sh merged ALL root .flags (22 entries) into every subcommand, including local-only flags like --version that Cobra does not propagate to children. This created a ~612-entry divergence vs the Go surface walker (github.com/basecamp/cli/surface).

Fix

Derive $ROOT_FLAGS from a child command's inherited_flags instead of the root's .flags. This captures only the 5 persistent flags Cobra actually propagates (account, json, md, project, quiet), excluding 17 local-only flags (version, agent, verbose, cache-dir, etc.).

Before → After

MetricBeforeAfter
Root flags merged into subcommands225
--version in subcommand surface✓ (correctly omitted)
Surface lines~10,900~7,750

Testing

# Verify root still has all its flags
./bin/basecamp --help --agent | jq ".flags | length"# 22# Verify children only inherit persistent flags
./bin/basecamp todos --help --agent | jq ".inherited_flags | length"# 5# Regenerate surface and confirm no --version in subcommands
bash scripts/check-cli-surface.sh ./bin/basecamp /tmp/surface.txt
grep "FLAG basecamp account --version" /tmp/surface.txt # no match
grep "FLAG basecamp --version" /tmp/surface.txt # match (root only)

Closes#368


Summary by cubic

Fixes incorrect flag propagation in scripts/check-cli-surface.sh by only inheriting persistent flags for subcommands. This removes --version and other local-only flags from subcommand surfaces and restores parity with the Go walker github.com/basecamp/cli/surface.

  • Bug Fixes
    • Derive ROOT_FLAGS from a subcommand’s inherited_flags instead of the root’s .flags.
    • Subcommands now include only 5 persistent flags; --version is root-only; surface size drops from ~10,900 to ~7,750 lines.

Written for commit 9e45417. Summary will update on new commits.

Review in cubic

The script merged ALL root flags (including local-only ones like
--version) into every subcommand's flag list. Cobra adds --version as
a local flag via InitDefaultVersionFlag, not a persistent one, so it
does not inherit to subcommands.
Fix: derive ROOT_FLAGS from a child command's inherited_flags instead
of the root's .flags. This captures only the persistent subset that
Cobra actually propagates to children.
Closesbasecamp#368
CopilotAI review requested due to automatic review settings June 12, 2026 08:32
@github-actionsgithub-actionsBot added the bug Something isn't working label Jun 12, 2026

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates the CLI surface-check script to detect only persistent (inherited) flags, avoiding root-only flags that don’t propagate to subcommands.

Changes:

  • Switches ROOT_FLAGS extraction from .flags on the root command to .inherited_flags on a child subcommand.
  • Adds logic to pick the first subcommand name from the root --help --agent JSON output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadscripts/check-cli-surface.sh
Comment threadscripts/check-cli-surface.sh

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Re-trigger cubic

@robzolkos
robzolkos merged commit 539999b into basecamp:mainJun 29, 2026
26 checks passed
@robzolkos

Copy link
Copy Markdown
Collaborator

@sawirricardo thanks!

@sawirricardo
sawirricardo deleted the fix-version-over-propagation branch July 2, 2026 20:48
jeremy added a commit that referenced this pull request Jul 18, 2026
* Fix todos update silently clearing completion subscribers (#538)
The BC3 todos PUT has replace semantics: any field omitted from the body
is cleared. Neither branch of todos update included
completion_subscriber_ids, so every update wiped the todo's "When done,
notify" list. The #412/#413 read-modify-write merge couldn't preserve it
because the SDK's Todo model drops completion_subscribers in
todoFromGenerated (basecamp/basecamp-sdk#355) — the merge had nothing to
carry forward.
Preservation now works via a raw GET of the flat /todos/{id}.json route
(which serves the field the SDK model drops), feeding the current
subscriber ids into both the typed-merge and raw-clear PUT paths. The
read fails closed: an HTTP error, malformed JSON, or a response missing
the completion_subscribers key aborts the command before any PUT rather
than risking a silent clear. The helper is commented as temporary and
goes away once the SDK round-trips the field.
Alongside the fix, subscribers become directly editable:
- todos create/update --notify-on-completion <names or ids> sets them
(comma-separated, with people-name tab completion)
- todos update --no-notify-on-completion clears them (by omission —
explicit intent skips the preservation read entirely)
Unit tests cover preservation in both branches, the flat-route
assertion, explicit set/clear bypassing the read, the fail-closed
matrix (missing key / malformed JSON / HTTP 500 in each branch), flag
conflicts, and the create body. A live smoke test exercises the full
sequence: create with subscriber, title-only update preserves, --no-due
preserves, --no-notify-on-completion clears.
.surface gains the three new flag records by hand (verified identical
to fresh generation); a full regeneration is deferred because the
snapshot has unrelated drift since #499 changed the generator script.
* Use completion-subscriber wording in --notify-on-completion errors
--notify-on-completion resolved people through resolveAssigneeIDs, so
failures surfaced as assignee errors ("No valid assignees provided",
"Assignee ID must be a positive number") — misleading for a flag that
sets completion subscribers.
Parameterize the resolver with a role label: resolvePersonRoleID(s)
carry the wording, and resolveAssigneeID(s) stay as assignee-labeled
wrappers so existing call sites and messages are unchanged. The
subscriber call sites now go through resolveCompletionSubscriberIDs.
Tests cover both subscriber-worded messages and assert no PUT occurs
on resolution failure.
* Test the name-resolution failure path for --notify-on-completion
The subscriber-wording tests covered the numeric-validation and
empty-list errors but not the ResolvePerson miss, so the "failed to
resolve completion subscriber" formatting was only exercised indirectly.
The update mock now matches the flat preservation route exactly
(/todos/999.json) instead of any .json GET, and serves /people routes an
empty directory so name resolution deterministically misses. New subtest
asserts the subscriber-worded resolve error and that no PUT occurs. The
explicit set/clear tests tighten their no-preservation-read assertions
to the exact route.
* Address review: SDK error conversion + exact mock route match
The preservation read's GET error now runs through convertSDKError
before wrapping, so structured codes/hints (rate limit, circuit
breaker) survive with the subscriber-specific context — matching every
other raw Account() call site.
The update mock's preservation branch now matches the account-scoped
flat route exactly (/99999/todos/999.json) instead of by suffix, so a
bucket-scoped GET can never satisfy it.
* Reject missing or invalid subscriber ids in the preservation read
A completion_subscribers element without a positive id would have put a
zero into completion_subscriber_ids and reached the PUT, slipping past
the fail-closed contract. The preservation read now errors on any
non-positive id, and the fail-closed test matrix covers the case.
jeremy added a commit that referenced this pull request Jul 19, 2026
…#542)
* Fix agent-help hidden-flag leak; make check-surface a real drift gate
The committed .surface snapshot is generated and enforced by the Go
surface package (surface.SnapshotString) via TestSurfaceSnapshot, not by
scripts/check-cli-surface.sh. The two generators diverge: the Go one
walks the command tree (full inherited globals, excludes hidden flags,
no --version leak), while the shell script walks --help --agent (curated
salient globals post-#499, and — the bug here — emits hidden flags).
Rather than churn .surface to the script's shape (which would break
TestSurfaceSnapshot and enshrine the leak), fix the underlying issues:
- emitAgentHelp leaked hidden flags because pflag's VisitAll visits them,
so `basecamp recordings --help --agent` surfaced the MarkHidden'd,
"Not supported" --assignee flag. Skip hidden flags at all three
emission sites to match text --help and the tree walker. Add a
regression test.
- check-surface previously regenerated to /tmp and echoed a line count,
catching nothing. Repoint it at TestSurfaceSnapshot (the authority) so
it actually fails on drift, and add an update-surface target for
regeneration. check-surface-compat / the cli-surface CI job are
unchanged (they diff generated-vs-generated across versions).
- TestSurfaceSnapshot's -update-surface wrote .surface without a trailing
newline; add one so update-surface is idempotent and POSIX-clean.
Closes#539.
* Write .surface on acknowledged removal-only updates
TestSurfaceSnapshot's -update-surface only wrote .surface when there were
additions, so a removal-only change (removed lines acknowledged in
.surface-breaking) left the removed CMD/FLAG lines in .surface — and
consumers that read it as the current surface (check-skill-drift,
check-smoke-coverage) would keep validating removed commands. Write
whenever there are no unacknowledged removals instead.
* Only rewrite .surface when the snapshot actually changed
Refine the -update-surface write condition into a testable predicate,
shouldWriteBaseline: write when update mode is on, there are no
unacknowledged removals, and the baseline bytes differ from the freshly
generated surface. The byte-difference guard keeps removal-only updates
(acknowledged in .surface-breaking) writing while making an unchanged
surface a true no-op. Add a TestShouldWriteBaseline regression test
covering removal-only, addition, no-change, unacknowledged-removal, and
non-update cases.
Also refresh 'make help': describe check-surface as the drift gate and
list the new update-surface target.
* Harden surface drift gate and hidden-flag test from review
- check-surface now fails when the committed .surface differs from the
generated surface even if the only delta is an acknowledged removal
that was never regenerated (baselineNeedsRegen). Previously such a
baseline passed the gate while still feeding stale removed lines to
check-skill-drift and check-smoke-coverage.
- TestAgentHelpOmitsHiddenFlags now asserts Execute() succeeds and checks
both flags and inherited_flags, matching the guarantee that no hidden
flag appears anywhere in agent help.
- check-surface's failure hint is now conditional (TestSurfaceSnapshot
can fail for reasons other than drift).
Add TestBaselineNeedsRegen covering the acknowledged-removal, no-change,
addition, and unacknowledged-removal cases.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugSomething isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix --version over-propagation in check-cli-surface.sh

3 participants

@sawirricardo@robzolkos