feat(cube-cli): run and follow a dbt sync from the CLI - #11562
feat(cube-cli): run and follow a dbt sync from the CLI#11562MikeNitsenko wants to merge 33 commits into
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
Claude finished @MikeNitsenko's task in 3m 53s —— View job Review: solid, well-reasoned CLI addition — 0 high, 2 medium, 5 low (6 inline comments posted)Full review
GeneralThis is unusually careful work. The reasoning is written down where it matters — why 404 needs a typed error rather than message matching, why progress goes to stderr, why unknown statuses mean "still working" rather than "done", why No security concerns. Findings
Plus one question inline on TestingThe five new tests are good ones —
I couldn't run |
|
Claude finished @MikeNitsenko's task in 2m 29s —— View job Follow-up review of
|
| # | Prior issue | Status in ffc5837 |
|---|---|---|
| 1 | Grace windows counted in polls | Fixed — MISSING_GRACE/IDLE_GRACE are Duration::from_secs(60), tracked via Cell<Option<Instant>>, independent of --poll |
| 2 | One transient error ends a 30-minute wait | Fixed, and better than suggested — five consecutive failures absorbed, classified by a whitelist (5xx, 408, 429, transport) rather than a blacklist |
| 3 | value * multiplier overflow |
Fixed — checked_mul, plus two assertions including u64::MAX |
| 4 | cancel ignored --json |
Fixed |
| 5 | wait_for_sync re-created the Client |
Fixed — takes api: &Client, matching wait_for_build |
| 6 | Timeout message reported the configured timeout | Fixed — reports started.elapsed(), and the sleep is clamped to remaining so a long --poll still gets a final look |
| 7 | {@link NotFound} JSDoc + confusing message on a tenant that 404s the routes |
Fixed — intra-doc links, and the message now says "this tenant may not serve the dbt-sync endpoints yet" |
The whitelist decision in client::is_transient is the part worth calling out. A blacklist would have been the obvious implementation and would have quietly retried the wait closures' own verdicts — an unknown sync id, a branch nothing is building — until the timeout, turning a clear diagnosis into a silent 30-minute hang. The doc comment says exactly that, and never_retries_a_verdict_the_attempt_reached pins it. Replacing the single-purpose NotFound with ApiError { status, .. } + TransportError is the right generalisation, and Display is genuinely unchanged per status.
wait::poll now has the tests it was missing: terminal-value, absorb-then-succeed, give-up-after-N, never-retry-a-verdict, and timeout-reports-elapsed. Counts are asserted (MAX_TRANSIENT_FAILURES + 1), not just outcomes.
New findings
| # | Sev | Where | Issue |
|---|---|---|---|
| 1 | Low | dbt.rs:83-84 |
Is a cancelled sync's status terminal? If cancel produces CANCELLED, it lands in the permissive bucket and --wait sits out the full 30m on a sync that is over — and cancel ships in this same PR |
| 2 | Low | dbt.rs:233-241 |
The /result fetch happens after wait::poll returns, so it has no transient tolerance — a 502 there still fails the gate for a COMPLETED sync. Same blip, moved to the last request |
| 3 | Low | wait.rs:57-66 |
429 is retried at the same fixed cadence with Retry-After ignored — the one status where the cadence itself is the problem. Plus: a timeout that happens mid-retry reports timed out after Ns with the cause only in stderr |
None of these block. 1 is a question about server-side behaviour I can't verify from here; 2 and 3 are narrow tails of a path that is now much sturdier than it was.
On the 60-second windows
The commit message flags 60s as a guess wanting confirmation against a real cold start — agreed, and that's the right thing to have written down. One interaction to keep in mind: with --timeout 30s, neither diagnosis message can ever fire, since the grace outlives the wait; the user gets the generic timeout instead. Fine as long as it's known.
I could not run cargo test / clippy in this environment (toolchain invocation not permitted), so this remains static review — your checked test-plan boxes are the evidence there. The two unchecked boxes (live deployment, build-status --wait) are still the ones that matter most, and the 60s windows and the cancelled-status question above are both things only that run can settle.
|
Claude finished @MikeNitsenko's task in 1m 58s —— View job Follow-up review of
|
| # | Prior issue | Status in 6686e6e |
|---|---|---|
| 1 | Is a cancelled sync's status terminal? | Answered: cancelling makes the workflow report FAILED, which is already terminal, so --wait exits promptly and non-zero. A CANCELLED constant would be dead code — agreed, and better than adding a speculative one |
| 2 | /result fetch had no transient tolerance |
Fixed — routed through wait::poll with a 30s budget (RESULT_FETCH_TIMEOUT), first answer wins. See the one caveat below |
| 3 | 429 retried at the failing cadence; timeout hid the cause | Fixed — backoff() doubles per consecutive failure capped at 60s, resets on progress, checked_mul so Duration::MAX can't wrap into a tiny sleep; the timeout message now carries the last transient error |
backoff is a careful little function: the cap keeps a long --poll from overshooting the wait it belongs to, the min(remaining) at the call site still guarantees a final look at the deadline, and both edges are asserted (3600s × 16 → cap, Duration::MAX → cap). Skipping Retry-After rather than plumbing headers through the error type is the right trade for the size of the win.
New findings
| # | Sev | Where | Issue |
|---|---|---|---|
| 1 | Low | wait.rs:88 |
last_transient is never cleared on progress, so a blip at minute 1 gets blamed for a timeout at minute 30 — and the alternating-closure test can't distinguish the two cases |
| 2 | Low | dbt.rs:249 |
A COMPLETED sync whose result 404s yields "result": null and exit 0; the 30s budget is already there if a missing result should instead be waited on and then failed |
Neither blocks. 2 hinges on whether the API guarantees a result exists the moment COMPLETED is reported — if it does, a comment saying so closes it.
One nit not worth an inline: backoff_doubles_while_failing_and_resets_on_progress doesn't test the reset half — that lives in poll, not backoff. The name promises more than the body checks.
Testing
Seven wait tests now, and they cover the paths that matter: terminal value, absorb-then-succeed, give-up-after-N with the attempt count asserted, verdict-never-retried, backoff edges, timeout-reports-elapsed, and timeout-carries-the-cause. The gap flagged in finding 1 is the one assertion I'd add.
I could not run cargo test / clippy in this environment (toolchain invocation not permitted), so this remains static review — your checked test-plan boxes are the evidence there. The two unchecked boxes (live deployment, build-status --wait) are still the ones that matter most: the 60s grace windows and the COMPLETED-with-no-result question above can only be settled against a real tenant.
|
Claude finished @MikeNitsenko's task in 2m 13s —— View job Follow-up review of
|
| # | Prior issue | Status in b62cec4 |
|---|---|---|
| 1 | last_transient never cleared, so a recovered blip is blamed for a later timeout |
Fixed, and better than suggested — the count and the message are now a single Option<(u32, String)> streak cleared on progress, so they cannot drift apart again. The old alternating-closure test is replaced by two deterministic ones: fail-throughout reports the cause, recover-then-timeout does not mention it |
| 2 | COMPLETED sync with a missing result → "result": null, exit 0 |
Fixed — None is now Progress::Waiting("result not available yet"), so a slow result is caught and an absent one fails at the 30s budget with a message naming cube dbt result as the way to read it separately. The --json document is still emitted on that failure, which is the right call: the branch name is what a caller needs to carry on with |
Making the two variables one is the part worth calling out. "Remember to clear B when you clear A" is exactly the invariant that rots on the next edit; Option<(count, message)> removes the option of getting it wrong, and None between streaks states the meaning directly.
The docs' CI recipe still works under the new behaviour — it reads only .branchName, and the failure path exits non-zero before jq ever sees the document.
New findings
| # | Sev | Where | Issue |
|---|---|---|---|
| 1 | Low | dbt.rs:267 |
The result fetch reuses wait::poll, whose timeout message advises "raise --timeout" and "it may still be running" — neither is true on a fixed 30s budget for a sync that already reported COMPLETED |
| 2 | Low | wait.rs:259 |
a_recovered_blip_is_not_blamed_for_a_later_timeout needs a waiting attempt to land inside 30ms; a scheduler stall on a loaded runner flips the assertion |
Neither blocks; 2 is a test-robustness nit on an assertion worth keeping reliable.
Testing
Eight wait tests, and the two new ones are deterministic in the way the replaced one was not — the commit message is right that "which of the error or the progress landed last" was a race. The counts are asserted, not just the outcomes.
I could not run cargo test / clippy in this environment (toolchain invocation not permitted), so this remains static review — your checked test-plan boxes are the evidence there. The two unchecked boxes (live deployment, build-status --wait) are still the ones that matter most: the 60s grace windows and how quickly a real /result becomes readable after COMPLETED can only be settled against a real tenant, and the second of those is what the new 30s budget is sized against.
|
Claude finished @MikeNitsenko's task in 1m 12s —— View job Follow-up review of
|
| # | Prior issue | Status in 1641684 |
|---|---|---|
| 1 | poll's timeout advice hardcoded "raise --timeout", untrue on the fixed RESULT_FETCH_TIMEOUT budget |
Fixed — advice is a Wait::on_timeout field defaulting to today's wording, replaced via .advising(…) at the result fetch. See the one wrinkle below |
| 2 | a_recovered_blip_is_not_blamed_for_a_later_timeout needed a waiting attempt inside 30ms |
Fixed — 300ms budget, with the reason written above the call |
The Wait struct is a bigger change than the finding asked for, and the commit message's second justification is the one that convinces me: poll(what, timeout, interval, …) took two adjacent Durations in an order nothing enforced, so a wait that polls every 30 minutes for 5 seconds was one transposition away and would have presented as a server problem. Named fields make that unwritable, and the call sites read better for it. advising() as a consuming builder over a defaulted field keeps the common case a one-liner.
New finding
| # | Sev | Where | Issue |
|---|---|---|---|
| 1 | Low | dbt.rs:272-275 |
The new advice restates the outer .context(…); main.rs:303 renders the chain with {err:#}, so the user sees the same sentence and cube dbt result 1 42 twice in one line |
Doesn't block — the message is correct, just twice as long as it needs to be. Shortening either half settles it.
On the rest of the diff
The test refactor is mechanical and the short_wait helper keeps the three that care about a specific interval (a_timeout_while_absorbing_failures_reports_the_last_one, the interval-longer-than-timeout case) writing it out explicitly, which is right — those two are the ones where the durations are the test. Eight wait tests still, all preserved through the signature change.
I could not run cargo test / clippy in this environment (toolchain invocation not permitted), so this remains static review — your checked test-plan boxes are the evidence there. The two unchecked boxes (live deployment, build-status --wait) are still the ones that matter most, and they're the only way to settle the 60s grace windows and how quickly a real /result becomes readable after COMPLETED.
|
Claude finished @MikeNitsenko's task in 59s —— View job Follow-up review of
|
| # | Prior issue | Status in 9d9e29b |
|---|---|---|
| 1 | advising(…) restated the outer .context(…), so {err:#} printed the same instruction and cube dbt result 1 42 twice in one line |
Fixed — the context now stops at what failed and the tail is the only place the recovery is spelled out |
You picked the opposite half from the one I sketched, and the justification in the commit message is the right one: the tail lands last, which is where a reader looks for what to do next, so it's the better place for the instruction than the context that precedes it. The comment at both sites saying "the other one deliberately doesn't" is what keeps this from being re-broken by whoever edits one of them next.
New finding
| # | Sev | Where | Issue |
|---|---|---|---|
| 1 | Low | dbt.rs:305 |
on_timeout is only appended on the deadline branch, so moving the recovery entirely into the tail leaves every non-timeout failure of this fetch (exhausted 502 retries, a 401, a 500) with no pointer at all — and those had one before this commit |
Doesn't block; it's a clause on the context away, and the tail only ever fires on timeout so restoring it there can't reintroduce the doubling.
On the rest
Nothing else changed — the diff is 13 lines across two string literals and two comments. The --wait --json document, the exit codes, and the docs' CI recipe are untouched by it.
I could not run cargo test / clippy in this environment (toolchain invocation not permitted), so this remains static review; the change is two string literals, so the risk of a build break is nil, but your checked test-plan boxes are still the evidence for the suite. The two unchecked boxes (live deployment, build-status --wait) remain the ones that matter most — the 60s grace windows and how quickly a real /result becomes readable after COMPLETED can only be settled against a real tenant.
|
Claude finished @MikeNitsenko's task in 1m 4s —— View job Follow-up review of
|
| # | Prior issue | Status in 9ca285d |
|---|---|---|
| 1 | Recovery lived only in on_timeout, so exhausted 502 retries and an immediate 401/500 printed no pointer |
Fixed — the context carries it for every outcome and the wait adds nothing, via Wait::advising_nothing() |
You went further than the suggestion block, and the extra piece is the part that makes it work: an empty on_timeout also suppresses the ". " separator in poll, so the timeout message ends at waiting for dbt sync result rather than trailing a dangling period-space. Without that, silencing the tail would have traded a duplicated sentence for a malformed one. timeout_advice_is_the_default_unless_replaced pins both halves — that the default still fires for ordinary callers, and that the silent one ends cleanly — and ends_with is the right assertion for it, since contains would pass on the dangling separator too.
The Last error: suffix still appends after a silent tail, which is correct: that half is the cause, not advice, and a timeout mid-streak should still say what was failing.
New finding
| # | Sev | Where | Issue |
|---|---|---|---|
| 1 | Low | dbt.rs:333 |
None => println!("(no result reported)") is unreachable — result is None only when failed, and that branch bail!s first. Reachable before b62cec4; dead since |
Doesn't block, and it isn't from this commit — it's the leftover of the earlier fix, and it only misleads a reader about whether a successful sync can report no result.
One nit not worth an inline: on_timeout: String with "" as the sentinel for "say nothing" is the kind of distinction Option<String> states outright. The doc comment covers it and advising_nothing() keeps the sentinel from spreading to call sites, so this is preference rather than a defect.
On the rest
The diff is 2 files, ~55 lines, and confined to error text plus the one poll formatting branch. Exit codes, the --wait --json document, and the docs' CI recipe are untouched.
I could not run cargo test / clippy in this environment (toolchain invocation not permitted), so this remains static review — your checked test-plan boxes are the evidence there. The two unchecked boxes (live deployment, build-status --wait) are still the ones that matter most: the 60s grace windows and how quickly a real /result becomes readable after COMPLETED can only be settled against a real tenant.
…s stale rationale
`contains("ref")` matched more than the name family it meant to: `prefix`, `refresh`,
`preferred`. No false positives in the tree today, but review's point was about the
next edit and about which direction of error is worse. A missing guard is a gap; a
false positive fails confidently on an ordinary `--prefix`, and the natural way to
satisfy the test is to add a parser whose message describes nothing about that flag.
So `branch` stays a substring match (`base_branch`, `source_branch`, `branch_name` all
mean a branch) and the three-letter one gets an edge: `ref`, `*_ref`, `ref_*`.
Checked both directions rather than reasoning about them:
- `--prefix` with no parser is ignored — 25 pass;
- `--base-ref` with no parser is caught: "cube agents skills --base_ref accepts an
empty value", "Got instead:" empty.
The comment justifying the exact count still described the filter it replaced, using
`base_branch` as its example — which the widened filter now catches, so the rationale
argued for a filter that no longer existed. The count still earns its place for a
narrower reason: a branchy flag renamed out of the family entirely (`branch` → `name`,
as `create-branch <name>` already is) or removed outright.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude finished @MikeNitsenko's task in 2m 58s —— View job Follow-up review of
|
| # | Prior issue | Status in 2a1a8ce |
|---|---|---|
| 1 | The comment justifying the exact count described the filter it replaced, using base_branch — the case the widened filter now handles |
Fixed — the count is now justified by the narrower reason it still holds: a rename out of the name family (branch → name, as create-branch <name> already is) or an outright removal |
| 2 | contains("ref") also matches prefix, refresh, preferred; a false positive fails confidently on a flag that has nothing to do with branches |
Fixed — ref / *_ref / ref_* for the three-letter half, contains kept for branch |
The asymmetry is the right one and the comment now argues for it rather than for the filter that used to be there: branch is safe as a substring because every name containing it means a branch, while ref isn't, and the two error directions aren't equally costly — a gap is a gap, but a false positive on --prefix fails loudly with advice that describes nothing about the flag, and the cheapest way to satisfy it is to attach a guard whose message is wrong for that value. Verifying both directions (--prefix ignored, --base-ref caught) rather than reasoning about them is what makes the trade checkable.
Re-counted the declarations by hand: 21 value_parser = util::nonempty* sites across data_model.rs (16), dbt.rs (2), deployments.rs, deploy.rs, github.rs, agents.rs. Their ids are 20 × branch (five of them positional branch: String) plus ref and name — so the word-boundary filter reaches exactly the same 20 as the contains version, assert_eq!(…, 20) is unchanged, and nothing in the tree newly matches or newly drops out. The two branch-ish bools (merge --delete-branch, merge-to-default --keep-branch) are still excluded by takes_a_value's ArgAction check from the previous commit.
New finding
| # | Sev | Where | Issue |
|---|---|---|---|
| 1 | Low | util.rs:288 |
The failure label is built from the snake_case arg id, so the multi-word ids this commit exists to match are reported as flags that don't exist (--base_ref) — visible in the commit's own verification output, and get_long() is already used two lines up in argv |
Doesn't block. It was invisible before this commit, since branch/ref render identically either way.
Two nits not worth an inline:
- The gap the word boundary deliberately accepts is slightly wider than the two names it was narrowed against:
refspec,reference,revisionare all outside the family, so a new unguarded flag under one of those passes with the count still at 20. That's the documented trade rather than an oversight — worth knowing it's the shape of the next escape, if there is one. err.contains("an empty value")doesn't check which argument the error is about. Not reachable today (every other required arg is filled with1, which both parsers accept), so a foreign error would fail the test loudly rather than pass it; asserting the flag appears in the error would close it if the argv builder ever grows a case where a placeholder is rejected.
On the rest
One file, +17/−9, entirely inside the test module plus its two comments. dbt.rs, wait.rs, client.rs, deployments.rs, data_model.rs and both docs pages are byte-identical to the last round, so every prior verdict stands and the --wait --json document, exit codes, stderr progress and the CI recipe's jq paths are unaffected.
I could not run cargo fmt / test / clippy in this environment (toolchain invocation not permitted) — the same limitation as every prior round, and it matters here for the same reason as last time: the change is a filter inside a clap-introspection test, and the only way to know it finds the same 20 is to run it. My count above agrees with the assertion but was done by reading declarations; the commit message's two-direction verification (25 pass with --prefix, --base-ref caught) is the evidence.
The one unresolved thread — delete-branch against a live dev-… branch, and running it immediately after a continue-on-error release — still applies and is still the right one to leave open.
…s are named
The failure message is the whole interface of this test, and it was printing flags that
don't exist. Arg ids are snake_case, so `--{id}` renders a `--base-ref` as
`--base_ref` — visible in the previous commit's own verification output, which I quoted
without noticing the flag had no such spelling. Review caught it; `get_long()` was
already being used two lines up in `argv` for exactly this.
The same bug was worse for positionals, which review's note didn't reach: four of the
twenty branch arguments are positional (`dev-mode`, `delete-branch`, `enable-branch`,
`disable-branch`), and every one was reported as `--branch`. They now render `<BRANCH>`,
matching what clap itself prints:
cube data-model dev-mode <BRANCH>
cube data-model delete-branch <BRANCH>
cube deployments build-status --branch
cube dbt sync --ref
Verified the multi-word case reports as typed by declaring the flag as `--base-ref`
without a parser: "cube agents skills --base-ref accepts an empty value".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude finished @MikeNitsenko's task in 2m 22s —— View job Follow-up review of
|
| # | Prior issue | Status in d9d61d3 |
|---|---|---|
| 1 | The failure label was built from the snake_case arg id, so a --base-ref was reported as --base_ref |
Fixed with get_long() — and the commit found the half my note didn't reach |
The positional case is the better half of the diff and it wasn't in the finding: four of the twenty guarded arguments are positional (dev-mode, delete-branch, enable-branch, disable-branch all take branch: String as a positional), and every one of them was being reported as cube data-model dev-mode --branch — a flag that doesn't exist on a command where the value isn't a flag at all. That's a worse version of the bug than the multi-word one I raised: --base_ref at least greps to nothing, while --branch on dev-mode reads as real and sends the reader looking for a declaration that was never written that way. Rendering <BRANCH> matches what clap prints in --help, so the message now works as a lookup key for either kind.
Checked the mechanics of both branches: no arg in the tree is short-only, so get_long() never falls back (and the same unwrap_or two lines up in argv stays inert); is_positional() is available on an unbuilt tree since it keys on index rather than on anything Command::build resolves; and the uppercased id agrees with clap's own placeholder for all four positionals today.
New finding
| # | Sev | Where | Issue |
|---|---|---|---|
| 1 | Low | util.rs:294 |
The option half asks clap what it prints; the positional half still derives the placeholder itself. Agrees for all four today, but an explicit value_name on a branchy positional reports a placeholder that appears nowhere in --help — the same mismatch this commit removes, one field short |
Doesn't block, and it isn't reachable in the current tree.
One nit not worth an inline: argv places every required positional in declaration order and skips optional ones, so a future optional positional declared before a branchy positional would slide the empty value into the wrong slot and the assertion would then be about a different argument. Clap debug-asserts against that declaration order, so it's guarded from outside rather than here — noting it only because the label is what a reader would trust if it ever happened.
On the rest
One file, +10/−1, entirely inside the test module. dbt.rs, wait.rs, client.rs, deployments.rs, data_model.rs and both docs pages are byte-identical to the last round, so every prior verdict stands and the --wait --json document, exit codes, stderr progress and the CI recipe's jq paths are unaffected. The filter and the exact count of 20 are untouched, so the two mechanisms from the last two commits (name match holds additions, exact count holds departures) still hold.
I could not run cargo fmt / test / clippy in this environment (toolchain invocation not permitted) — the same limitation as every prior round. It matters least on this one: the change is a format-string branch, and the commit message quotes the four rendered labels plus a --base-ref verification, which is the evidence static review can't produce.
The one unresolved thread — delete-branch against a live dev-… branch, and running it immediately after a continue-on-error release — still applies and is still the right one to leave open.
Half-done in the last commit: the option label asks clap what it prints via
`get_long()`, then the positional label derived `<BRANCH>` by uppercasing the field
name. It agrees with clap for all four positionals today, so this is about the next
declaration — an explicit `#[arg(value_name = "…")]` on a branchy positional would make
the failure quote a placeholder that appears nowhere in --help, which is the exact
mismatch the previous commit existed to remove.
`get_value_names()` now supplies it, with the uppercased id as a fallback so today's
four render identically. Verified by putting `value_name = "BRANCH_NAME"` on an
unguarded branchy positional:
cube data-model enable-branch <BRANCH_NAME> accepts an empty value
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude finished @MikeNitsenko's task in 1m 57s —— View job Follow-up review of
|
| # | Prior issue | Status in 2c3853f |
|---|---|---|
| 1 | The option label asked clap what it prints; the positional label derived <BRANCH> by uppercasing the field name, so an explicit value_name would quote a placeholder absent from --help |
Fixed — get_value_names().and_then(first) with the uppercased id as fallback |
Checked the mechanics rather than assuming: clap's derive doesn't emit value_name for a plain positional (Command::_build_self fills it from the id), so get_value_names() is None for all four of today's branch: String positionals and the fallback keeps them rendering as <BRANCH> — which is what the commit says it verified. Some(&[]) is handled by .first(), and a multi-value arg's first name is the right one to show. The value_name = "BRANCH_NAME" verification in the commit message is the half reading can't produce.
Re-counted the tree while I was in it: 21 value_parser = util::nonempty* declarations (16 data_model.rs, 2 dbt.rs, one each in dbt/deployments/deploy/github/agents), of which create-branch <name> carries the id name — 20 with a branchy id, matching assert_eq!(…, 20) unchanged. The word-boundary ref filter and the ArgAction-based takes_a_value are untouched, so both mechanisms from the last three commits still hold: name match catches additions, exact count catches departures.
On the rest
One file, +9/−1, entirely inside the test module. dbt.rs, wait.rs, client.rs, deployments.rs, data_model.rs and both docs pages are byte-identical to the last round, so every prior verdict stands and the --wait --json document, exit codes, stderr progress and the CI recipe's jq paths are unaffected.
One nit not worth an inline comment, since it isn't reachable and I'd be posting a narrower version of a finding you just fixed: clap prints an optional positional as [BRANCH] and a multi-value one as <BRANCH>..., while this always prints <{name}>. All four branchy positionals are required single-value Strings, so it agrees today; is_required_set() is already in scope in argv if a future declaration ever makes it disagree.
I could not run cargo fmt / test / clippy in this environment (toolchain invocation not permitted) — the same limitation as every prior round. For a nine-line change to a format branch the build risk is the get_value_names() signature (Option<&[Str]>, handled), and the commit message quotes the rendered label from a real run.
The one unresolved thread — delete-branch against a live dev-… branch, and running it immediately after a continue-on-error release — still applies and is still the right one to leave open. Nothing new from my side.
…ample that cannot work
The deployment owner authorized checks against a live tenant and the delete-branch
endpoint landed there, so the thread I left open on speculation is now measured. Both
answers are the permissive ones:
- `delete-branch` works on a personal `dev-…` branch, not only the shared branches the
earlier verification covered;
- it works while that branch's dev-mode session is still active, and ends the session —
`build-status` drops to master/production_build afterwards.
So the recipe's order (release, then prune) needs no reordering, and the prune is a
backstop rather than a hostage: a `continue-on-error` release that genuinely failed
can't strand the session, because deleting the fork releases it. That's in the Warning.
Verifying it turned up a documented flow that cannot work. The Git-workflow example
wrote to the branch name passed to `create-branch --dev-mode`:
cube data-model create-branch ID my-branch --dev-mode
cube data-model put ID model/cubes/orders.yml --file orders.yml --branch my-branch
The put fails 400: "Branch \"my-branch\" is not a dev-mode branch." `--dev-mode` points
the session at the new branch without forking the personal `dev-…` branch that writes
require, and `build-status` reports the branch as `dev_mode` regardless, so it looks
ready. The example now forks with `dev-mode` and writes to that name; the <Info> says
plainly that `--dev-mode` is not a shortcut for it, and `--dev-mode`'s own help says so
too.
The example was also missing the step that makes its last line meaningful: measured
that `commit --branch <dev-…>` pushes to the shared branch the fork came from (wrote a
file on a fork, committed, found it on the parent), which is what `merge-to-default
--branch my-branch` then merges.
Every branch created for these checks was deleted afterwards; the tenant is back to the
branches it started with.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude finished @MikeNitsenko's task in 2m 37s —— View job Follow-up review of
|
| Prior issue | Status in 1973186 |
|---|---|
delete-branch on a personal dev-… branch was unverified (the live checks covered a shared branch, an unknown one, and the deploy branch) |
Measured — it works |
The prune runs right after a continue-on-error release, so a failed release leaves the session active on the branch being deleted |
Measured — deleting the fork succeeds and ends the session; build-status drops to master/production_build |
That's the answer that makes the recipe's existing order correct without a reorder, and it turns the prune from a step that depends on the release into one that covers for it — which is now the <Warning>'s second paragraph. I'd offered the reorder as the option that needed no live check; getting the measurement instead is strictly better, since it also settles what happens to a session whose branch disappears, which the reorder would have left unknown.
Noting the authorization and the cleanup ("every branch created for these checks was deleted afterwards") is the right thing to have in the commit message for work against someone else's tenant.
The bug the verification turned up
This is the second time in this PR that pointing the binary at a real deployment found something no amount of diff-reading would: the documented Git workflow could not work. create-branch --dev-mode re-points the session at the new branch without forking, so the very next line — put --branch my-branch — fails 400 "Branch "my-branch" is not a dev-mode branch". And build-status reports that branch as dev_mode, so every signal available to a reader says it should have worked.
Two details make the fix better than a minimal correction:
- the
<Info>states the trap directly (--dev-mode"is not a shortcut for this") rather than just describing the working path, so a reader who already has the broken invocation in a script finds their own case; - the example gained the
commitstep, verified by writing a file on a fork and finding it on the parent — without it the finalmerge-to-default --branch my-branchmerged a branch that had nothing on it, so the example was two bugs deep, not one.
The --dev-mode help text now carries the same caveat, which is the right place for it: --help is what someone reaches for after the 400.
New findings
| # | Sev | Where | Issue |
|---|---|---|---|
| 1 | Low | data_model.rs:446 |
On the measured server the if never fires, so --dev-mode prints exactly what a plain create-branch prints — while having re-pointed the per-credential session at a branch that rejects writes. The docs warn about both halves; the command that causes it is silent |
| 2 | Low | reference/cli.mdx:178 |
The corrected example leaves the reader in dev mode with the dev-… fork behind — the two things the CI section on the same page explicitly releases and prunes, and the fork now matters more given this commit's finding that a live session pins the deployment to a dev build |
Neither blocks; both are about the gap between what the prose on this page now knows and what the adjacent command/example does.
On the rest
Two files, +28/−11. dbt.rs, wait.rs, client.rs, deployments.rs, util.rs are byte-identical to the last round, so every prior verdict stands and the --wait --json document, exit codes, stderr progress and the CI recipe's jq paths are unaffected. The 20-flag walk and its exact count are untouched; create-branch <name>'s guard is still the one outside the walk's filter, as its doc comment says.
I traced the new example against the guards: DEV=$(… | jq -r .branchName) can yield an empty value in an interactive shell with no pipefail, but put/commit's --branch now carry value_parser = util::nonempty, so it fails at parse time rather than targeting whatever the server would have picked — the class fix from f4697cb covering a snippet written after it.
I could not run cargo fmt / test / clippy in this environment (toolchain invocation not permitted) — the same limitation as every prior round. The Rust half of this diff is two comment blocks and one doc-comment line, so the build risk is nil; the live measurements in the commit message are the evidence for the rest, and both of the test plan's originally-unchecked boxes have now been exercised, including the two checks this thread was holding open.
…kflow
Two review points, both about the measured path being the quiet one.
`create-branch --dev-mode` printed the same line as a plain `create-branch` — the fork
branch never fires on the measured server — while re-pointing per-credential dev-mode
state at a branch that rejects writes. The PR's own docs warn about both halves in
prose, so the command causing it was the one place staying silent. It now says so:
✓ Created branch cli-msg-check; your dev-mode session now points at it
File writes still need a dev-… branch: run `cube data-model dev-mode 21286 cli-msg-check`.
Verified the claim it makes is true (a write to that name is refused 400) rather than
just plausible.
The Git-workflow example stopped two commands before the CI recipe on the same page
does, leaving the reader in dev mode with an accumulating `dev-…` fork — the state the
gate's Warning and prune step exist for. It now ends with `exit-dev-mode` and
`delete-branch`, verified end to end.
Trimmed one claim while writing that: review's summary of the earlier finding was that
a live session "pins the deployment to a dev build", and I nearly repeated it. What I
measured is narrower — commands that omit `--branch` target the dev branch — so that is
what the sentence says.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude finished @MikeNitsenko's task in 3m 9s —— View job Follow-up review of
|
| # | Prior issue | Status in 4883794 |
|---|---|---|
| 1 | create-branch --dev-mode printed the same line as a plain create-branch while silently re-pointing per-credential session state at a branch that rejects writes |
Fixed — a distinct success line plus a pointer to dev-mode <name>, and the claim was verified (a write to that name is refused 400) rather than assumed |
| 2 | The Git-workflow example left the reader in dev mode with an accumulating dev-… fork |
Fixed — ends with exit-dev-mode and delete-branch, and the paragraph says why those two lines are part of the workflow rather than tidying |
The correction in the commit message is the part I'd call out. My summary of the earlier finding said a live dev-mode session "pins the deployment to a dev build"; what was actually measured is narrower — commands that omit --branch target the dev branch — and the new prose says only that. I checked the narrower claim against the tree and it's well supported by the CLI's own help text rather than by two measurements: put/delete/rename/commit/merge/merge-to-default all document "defaults to your active dev-mode branch", and build-status documents "defaults to the active dev-mode branch, else the deploy branch" (deployments.rs:268). So "every command that omits --branch" is a fair generalisation, not an extrapolation — worth noting since that's exactly the kind of sentence this review has been narrowing all round.
New findings
| # | Sev | Where | Issue |
|---|---|---|---|
| 1 | Low | reference/cli.mdx:185 |
The paragraph accounts for what the workflow leaves behind, but merge-to-default sends removeBranchAfterMerge: !keep_branch (data_model.rs:611) — so my-branch is deleted by the merge, which the prose never says. It also explains why this snippet needs one delete-branch where the CI recipe needs two |
| 2 | Low | data_model.rs:459 |
The branch above reads the response (branchName != name); this one fires on the flag we sent, so --dev-mode asserts the session moved even for a response with no branchName — the shape a server that ignores enterDevMode would return, which isn't hypothetical here given enable-branch 404s on the same tenant |
Neither blocks. 1 is a clause; 2 is about a claim's provenance, and its advice half is correct either way.
On the rest
Two files, +15 lines, no logic outside one else if arm and five docs lines. dbt.rs, wait.rs, client.rs, deployments.rs, util.rs are byte-identical to the last round, so every prior verdict stands and the --wait --json document, exit codes, stderr progress and the CI recipe's jq paths are unaffected. The 20-flag walk and its exact count are untouched. The new println! follows the existing dev_branch_hint convention (stdout, non---json only), and the --json path is unchanged.
The ordering in the new example is worth one note for the reader, not a finding: merge-to-default removes my-branch while $DEV is still forked from it, so it exercises the re-parenting behaviour you measured earlier (deleting a parent re-parents children rather than removing them) — which is what leaves delete-branch "$DEV" something to delete two lines later. Your end-to-end verification covers it.
I could not run cargo fmt / test / clippy in this environment (toolchain invocation not permitted) — the same limitation as every prior round. The Rust half is one else if arm with two format strings and no new bindings, so the build risk is nil; the live checks in the commit message are the evidence for the behaviour.
Both of the test plan's originally-unchecked boxes have been exercised against a live tenant, and there are no threads left open from any prior round.
…d nothing
Found by rehearsing the flow end to end for a demo, which is the first time anything
ran the query step as written. Two faults, and together they are a false green in the
gate this page adds:
- the query went to `$CUBE_API_URL`, the Cube Cloud API host. The dev-mode API lives on
the deployment's own host (`deploymentUrl`, e.g. sapphire-tick.aws-us-east-2…). The
API host answers that path with 200 and an HTML page, so `curl -sf` exits 0;
- nothing checked the response shape, so 8KB of HTML passed as a successful query.
Now it resolves the host via `deployments get --json | jq -r .deploymentUrl` and pipes
the result through `jq -e '.data'`, so a non-result fails the step. Verified against
deployment 21286: the four generated cubes are visible over /meta and
`dbt_fct_orders.count` returns [{"dbt_fct_orders.count":"3"}].
The example also queried `orders.count`, which pre-existed the sync — a measure that
would answer identically if the sync had generated nothing. It now names a generated
cube, with a sentence saying why that matters.
Also from review: `merge-to-default` sends removeBranchAfterMerge unless
`--keep-branch`, so the Git-workflow example's branch is deleted by the merge — the
prose said nothing and a reader would expect it to survive. Said plainly, and the
cleanup is reordered to release and prune the fork *before* the merge, which no longer
depends on what the merge does to a fork whose parent it removes.
And `create-branch --dev-mode`'s new line asserted an outcome from the flag we sent. The
response's `active` is false whether or not `--dev-mode` was passed, so it cannot
confirm the session moved; the line now reports the request instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude finished @MikeNitsenko's task in 3m 10s —— View job Follow-up review of
|
| # | Sev | Where | Issue |
|---|---|---|---|
| 1 | Medium | reference/cli.mdx:286 |
jq -e '.data' also fails on the one 200 that isn't an error: /load answers a slow query with 200 + {"error": "Continue wait"} and expects the client to retry. Cold dev-mode environment, uncached warehouse, 5s default threshold — so the documented gate can go red on a healthy model, intermittently, with jq: error as the only log line |
| 2 | Low | reference/cli.mdx:284 |
-f discards the 4xx body, so the exact mistake the new paragraph warns about (a measure the sync didn't generate) fails with nothing but exit 22 — the 400 names the missing member, and --fail-with-body keeps it |
1 is the only one I'd want settled before merge, and it's the same shape as the bug this commit fixes pointed the other way: the assertion is right, it just can't tell a retryable 200 from a wrong one.
Two nits not worth an inline:
deployments getprints JSON unconditionally (output::print_json, noctx.jsonbranch), so the--jsonin the recipe is inert. Harmless, and arguably clearer to leave for symmetry with the other--jsoncalls.- I couldn't establish from the schema whether
deploymentUrlcarries a scheme (api.yamltypes it as a barestring, and the commit message's example is truncated). If it doesn't,curlguesseshttp://, the redirect isn't followed without-L, and the step fails on an empty body rather than passing — loud, so not a correctness risk either way, but your live run is what settles it.
On the rest
Two files, +26/−9. The Rust half is one string and a comment block, so no behaviour outside that message; dbt.rs, wait.rs, client.rs, deployments.rs, util.rs are byte-identical to the last round, and every prior verdict stands — the --wait --json document, exit codes, stderr progress, the 20-flag walk and its exact count are all untouched.
I could not run cargo fmt / test / clippy in this environment (toolchain invocation not permitted) — the same limitation as every prior round. The one-line format! uses its only interpolated binding, so the build risk is nil; the live rehearsal quoted in the commit message (four cubes over /meta, dbt_fct_orders.count → 3) is the evidence for the recipe.
… body
Two review points on the query step I fixed in the last commit — the assertion was right
but it turned one false green into an intermittent false red.
`/cubejs-api/v1/load` answers a slow query with status 200 and `{"error": "Continue
wait"}`, and clients are expected to retry until a result appears — our own REST API
reference says so. `jq -e '.data'` treats that as a failure, so the gate would go red on
a healthy model, intermittently, with nothing in the log but `jq: error`. And it is the
likely case here, not an exotic one: the first query runs seconds after a cold dev-mode
environment finished building, against an uncached warehouse. The step now retries
`Continue wait` and fails on anything else, including on exhausting the retries — no
silent pass if the query never answers.
`-f` also discards the body on a 4xx, so the mistake the paragraph explicitly warns
about — naming a measure the sync didn't generate — failed with an empty log. Measured:
curl -sfG … → exit 56, output []
curl -sG --fail-with-body … → exit 22, {"error":"Error: Cube 'nope' not found for
path 'nope.count'"}
Ran the replacement loop on both paths against deployment 21286 before writing it in:
the success path prints the data and breaks, the 4xx path prints the message and exits.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude finished @MikeNitsenko's task in 3m 3s —— View job Follow-up review of
|
| # | Prior issue | Status in 02eec1d |
|---|---|---|
| 1 | Medium — jq -e '.data' fails on {"error": "Continue wait"}, a 200 the API expects clients to retry, so the gate goes intermittently red on a healthy model |
Fixed — a bounded retry loop that retries only Continue wait, fails on anything else, and fails on exhausting the retries rather than falling through green |
| 2 | Low — -f discards the 4xx body, so naming a measure the sync didn't generate failed with nothing but exit 22 |
Fixed — --fail-with-body, with the measured before/after in the commit message |
The half worth calling out is the one I didn't ask for: [ -n "$OK" ] || { echo …; exit 1; }. A retry loop's natural failure mode is falling out of the loop and continuing as if it had succeeded, which would have re-created the false green this whole line of commits exists to remove — and the OK sentinel is the cheapest way to make "the loop ended" and "the query answered" different facts. Retrying only the one documented status, and treating an unrecognised body as fatal, is also the same asymmetry as is_transient's whitelist and NOT_BUILDING: never let an unrecognised response buy more waiting.
Checked the assertion chain by hand for the wrong-host case the previous commit fixed, since the loop changed it: an HTML body fails jq -e '.data' with a parse error, then fails the Continue wait test, so it exits 1 with the body — the guard still holds through the rewrite. set -e doesn't interfere (the jq calls are if conditions and || left-hands), and the RES=$(…) || … form keeps the body available on the failure path.
I verified the anchor rather than trusting it, since a bad one slipped through in 3a3dc92: the heading is ## `Continue wait` at reference/core-data-apis/rest-api/index.mdx:289, and that page links to it internally as #continue-wait. Resolves.
New findings
| # | Sev | Where | Issue |
|---|---|---|---|
| 1 | Low | reference/cli.mdx:286 |
-s without -S leaves the transport failure silent — the same empty log the -f fix removed, from the other side; and reachable from the recipe, since jq -r .deploymentUrl prints null for an absent field. Plus the loop retries Continue wait and nothing else, so one 502 fails the gate where wait.rs absorbs five |
| 2 | Low | reference/cli.mdx:319 |
This PR now spells the same page two ways: the new link omits /index, cli.mdx:422 (from 610be2b) includes it, and 11 other docset pages side with the new one |
Neither blocks. 1 is one flag plus a decision to record; 2 is a suffix.
Two nits not worth an inline:
jq -e '.data'prints its output, so everyContinue waititeration emits a barenullline before the real result — a slow first query logs a column ofnulls.>/dev/nullon the test plusecho "$RES"on the success branch reads better.- The budget is implicit: 30 iterations × (
CUBEJS_CONTINUE_WAIT_TIMEOUT+ 5s) ≈ 5 minutes. Every other wait in this PR takes an explicit--timeout; this one is aseqliteral, and the paragraph doesn't say how long the gate will sit there. (--fail-with-bodyalso needs curl ≥ 7.76, which is fine on current runners and not on an old self-hosted one — it fails withoption --fail-with-body: is unknown, which at least names itself.)
On the rest
One file, +26/−7, all YAML and prose. No Rust touched — dbt.rs, wait.rs, client.rs, deployments.rs, util.rs, data_model.rs are byte-identical to the last round, so every prior verdict stands and the --wait --json document, exit codes, stderr progress, the 20-flag walk and its exact count are unaffected. The other steps' jq paths are untouched, and shell: bash still covers the one step that pipes.
I could not run cargo fmt / test / clippy in this environment (toolchain invocation not permitted) — the same limitation as every prior round. For a docs-only diff there is nothing to build; the live rehearsal in the commit message (both paths of the replacement loop run against deployment 21286) is the evidence for the recipe, and it's the third time in this PR that running it found what reading it couldn't.
| TOKEN=$(cube deployments token "$DEPLOYMENT_ID") | ||
| OK= | ||
| for _ in $(seq 30); do | ||
| RES=$(curl -sG --fail-with-body "$API/dev-mode/$DEV_BRANCH/cubejs-api/v1/load" \ |
There was a problem hiding this comment.
--fail-with-body closes the 4xx half, but -s still silences the case the commit's own reasoning applies to just as well: a transport failure. On a connection reset, DNS failure or timeout, curl prints nothing (that's what -s does), $RES is empty, and the step fails with a blank line followed by exit 1 — the same empty log the -f fix removed, arrived at from the other side.
It's reachable from the recipe rather than only from a bad network: API=$(cube deployments get … | jq -r .deploymentUrl) prints the literal null if the field is ever absent (the distinction you documented in 4cd383a), so the URL becomes null/dev-mode/…, curl fails to resolve, and the log says nothing at all about why.
-S keeps -s's quiet progress meter and restores the error message:
| RES=$(curl -sG --fail-with-body "$API/dev-mode/$DEV_BRANCH/cubejs-api/v1/load" \ | |
| RES=$(curl -sSG --fail-with-body "$API/dev-mode/$DEV_BRANCH/cubejs-api/v1/load" \ |
Related, and the reason it matters more here than in an ordinary script: this loop retries Continue wait and nothing else, so one 502 or 503 from the deployment's ingress fails the gate — while wait.rs absorbs five consecutive ones for exactly this reason, and a 30-iteration loop is already sitting right here. Worth deciding deliberately rather than by omission; if the answer is "a 5xx here is a real failure", a clause in the paragraph saying so would settle it.
|
|
||
| The loop is there because not every `200` is an answer: a query that takes longer than | ||
| `CUBEJS_CONTINUE_WAIT_TIMEOUT` returns [`Continue | ||
| wait`](/reference/core-data-apis/rest-api#continue-wait) with status 200, and clients |
There was a problem hiding this comment.
This link is right, and it's now inconsistent with the other one this PR added: cli.mdx:422 defines [ref-rest-api]: /reference/core-data-apis/rest-api/**index** (from 610be2b), pointing at the same page.
The rest of the docset is unanimous on the form used here — 11 other pages spell it /reference/core-data-apis/rest-api (recipes/core-data-api/real-time-data-fetch.mdx:93, reference/control-plane-api.mdx:322, reference/data-modeling/dimensions.mdx:1378, …) — and cli.mdx:422 is the only place /index appears anywhere in docs-mintlify. So the outlier is the older of the two, and dropping its suffix makes both agree with the convention.
I did verify the anchor rather than assume it: the heading is ## `Continue wait` at reference/core-data-apis/rest-api/index.mdx:289, and that page links to it internally as #continue-wait, so #continue-wait resolves.
Summary
Adds a
cube dbtcommand group so a dbt sync can be run and followed from the CLI, and--waittocube deployments build-statusso the compile step can be waited on too. Together these let a CI job gate a merge on a dbt change: sync the ref under review, compile the generated model, query it, and fail before anything reaches production.--refis what makes a pull-request gate meaningful. Without it a sync clones the branch saved on the dbt integration, so every run would compile the tracked branch and report green for a change that breaks the model.--waitpolls to a terminal state and exits non-zero when the sync fails or the timeout elapses, so a CI step needs no extra scripting. Progress goes to stderr, and only when the stage changes, so a fifteen-minute sync prints a handful of lines rather than one per poll and--jsonstdout stays a single parseable document. With--wait --jsonthat document carries both halves a pipeline needs — the branch to compile next, and how the sync ended.build-status --waitgives up early, with an explanation, when a branch reports that nothing is building it. A shared branch — which is what a sync produces — only compiles once someone opens it in dev mode; waiting on it otherwise would sit out the whole timeout for no reason.Two supporting pieces:
Client::get_optionaltreats a 404 as an answer rather than an abort, because it is a normal state twice here (a sync not yet visible, and a result asked for while the sync still runs) andgetturning it into an error would end a wait instead of continuing it. It recognises the case through a typedNotFounderror rather than matching message text, and that error'sDisplayis unchanged, so nothing that merely prints it reads differently.util::parse_durationfor the wait flags, whose useful range spans a seconds-long poll interval and a tens-of-minutes sync — a bare number would have to pick one and silently surprise anyone who meant the other.Docs: a
dbtrow in the CLI command reference, adbt syncsection with the CI-gate recipe, and a pointer from the dbt integration page's CI/CD section, which until now offered only the raw REST endpoint.Test plan
cargo fmt --all --checkandcargo clippy --all-targets -- -D warningscleancargo test— 12 pass, 5 new (duration parsing in both directions, three status-label shapes)--helpfor every new subcommand, shell completions still generate, and a bad--timeoutis rejected at parse time with a usable messagesync --refon a non-tracked branch through toCOMPLETED, the merged--wait --jsondocument,result,cancelmid-run, and a foreignsyncJobId(should report the sync as unknown)build-status --waiton a dev-mode branch, and the early give-up on a shared branchNotes
rustcolder than 1.88 cannot build the crate at all — a transitive dependency (home) requires it. CI installs current stable, so this only affects local builds; I did not pin the lockfile to work around it.🤖 Generated with Claude Code