Skip to content

feat(cli): add ado-aw run#595

Closed
jamesadevine wants to merge 1 commit into
mainfrom
feat/cli-run
Closed

feat(cli): add ado-aw run#595
jamesadevine wants to merge 1 commit into
mainfrom
feat/cli-run

Conversation

@jamesadevine
Copy link
Copy Markdown
Collaborator

Summary

Implements PR 6 of the Phase 1 CLI overhaul. Adds ado-aw run [PATH] — queues a build for every ADO definition that matches a local fixture, with optional --wait-then-exit-by-aggregate-result behaviour.

CLI surface

ado-aw run [PATH]
  --org <url-or-name>          # default: from git remote
  --project <name>             # default: from git remote
  --pat <pat>                  # else AZURE_DEVOPS_EXT_PAT, else az CLI
  --branch <ref>               # default: definition's defaultBranch
  --parameters k=v[,k=v...]    # repeatable; ADO templateParameters
  --wait                       # poll until completion
  --poll-interval <secs>       # default 10 (requires --wait)
  --timeout <secs>             # default 1800 (requires --wait)
  --dry-run

Naming nit

Module entry point is run::dispatch, not run::run, so call sites read crate::run::dispatch(...) cleanly. A doc comment in src/run.rs flags this so a later contributor doesn't "fix" the asymmetry.

Plumbing

  • Implements the queue_build and get_build stubs in src/ado/mod.rs (PR refactor(cli): extract shared ADO REST helpers into src/ado/mod.rs #580).
  • Two pure decision functions, 16 unit tests:
    • parse_parameters — comma-separated + repeatable, preserves = in values, rejects malformed pairs.
    • classify_build(status, result)InProgress / Succeeded / Failed.
  • --poll-interval and --timeout are gated on --wait via clap requires; the integration test pins that constraint.
  • Transient poll-time HTTP errors keep the target on the pending list and retry on the next tick; the polling loop bails on the overall --timeout.

Verified

  • cargo build
  • cargo clippy --all-targets --all-features
  • cargo test ✅ — 16 new unit tests in run.rs, 2 integration tests in tests/run_integration.rs.

Out of scope

PR 7 (status), PR 8 (secrets), PR 9 (help-text grouping), PR 10 (doc overhaul).

Implements PR 6 of the Phase 1 CLI overhaul. Queues an ADO build for
every definition that matches a local fixture. With `--wait`, polls
each queued build until completion and exits with an aggregate exit
code: 0 only if every queued build succeeded.

CLI surface:

    ado-aw run [PATH] --org --project --pat --branch
                      --parameters k=v[,k=v...]
                      --wait --poll-interval --timeout --dry-run

The module entry point is `run::dispatch`, not `run::run`, so call
sites read `crate::run::dispatch(...)` cleanly. A doc comment in
src/run.rs flags this so a later contributor doesn't "fix" the
asymmetry.

Implements the `queue_build` and `get_build` stubs in `src/ado/mod.rs`
that PR 1 (#580) introduced.

Decision logic is split into two pure functions exercised by 16 unit
tests:

- `parse_parameters` — accepts `--parameters a=1,b=2` and repeated
  occurrences; preserves equals signs inside values; rejects malformed
  pairs (missing `=`, empty key); skips blank entries.
- `classify_build` — `(status, result)` → InProgress / Succeeded /
  Failed terminal-state classifier. Treats canceled,
  partiallySucceeded, missing `result`, and any other non-`succeeded`
  result as Failed when `status == completed`; anything else is
  InProgress.

`--poll-interval` and `--timeout` are gated on `--wait` via clap
`requires`, so the integration test catches the constraint at parse
time. Transient poll-time HTTP errors keep the target on the pending
list and retry on the next tick; the polling loop bails on the
overall `--timeout`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Rust PR Review

Summary: Looks good — clean implementation with solid test coverage; two minor issues worth addressing before merge.

Findings

🐛 Bugs / Logic Issues

  • src/run.rs:39 — comma-split parameter values silently truncate at first comma
    parse_parameters splits every entry on , before splitting on =. A value that contains a comma (e.g. --parameters "filter=a,b") will be parsed as filter=a plus the bare token b, which then fails with "no '=' found". That's a confusing error message for what is effectively an undocumented limitation. Either document the restriction explicitly in the --parameters clap help text and error message, or switch to splitting on , only when no = appears to the right of the comma.

  • src/run.rs:228--poll-interval 0 causes a spin-loop
    clap applies default_value_t = 10 but there is no value_parser minimum. Passing --wait --poll-interval 0 will call tokio::time::sleep(Duration::from_secs(0)) in a tight loop against the ADO API for up to --timeout seconds. A simple value_parser = clap::value_parser!(u64).range(1..) on the poll_interval field would guard this.

⚠️ Suggestions

  • src/run.rs:277 — permanent 404s are silently retried until timeout
    The comment says "treat transient poll errors as still-in-progress", which is reasonable, but get_build returns an Err for any non-2xx response (including a permanent 404 if a build was deleted or the ID was wrong). The final summary then shows those as in_progress rather than failed. Consider checking whether the error is likely permanent (e.g. by inspecting the HTTP status code or the error message string) and counting those as failed rather than in_progress. At minimum, a warning that distinguishes "transient" from "permanent" would avoid a confusing wait-then-timeout result.

  • src/ado/mod.rs:1076templateParameters values aren't validated as strings
    The queue_build signature takes &serde_json::Map<String, serde_json::Value> and passes it through to ADO as-is. parse_parameters in run.rs always inserts serde_json::Value::String(...), so today every call-site is safe. But the function contract doesn't enforce string values, and a future caller (e.g. safeoutputs/queue_build.rs) could pass non-string values that ADO would reject with an opaque 400. Adding a note to the doc comment (or an assertion/validation inside queue_build) would protect future callers.

✅ What Looks Good

  • parse_parameters using split_once('=') correctly preserves = in values — exactly right.
  • classify_build is a pure, well-tested function with all ADO result variants covered (canceled, partiallySucceeded, missing result field).
  • The transient-error retry loop (keep-pending, sleep, retry) is clean and the timeout path exits gracefully.
  • clap requires = "wait" constraint on --poll-interval / --timeout is properly tested in tests/run_integration.rs.
  • dispatch entry point naming is a nice convention; the module-level doc comment preventing future "fixes" is appreciated.

Generated by Rust PR Reviewer for issue #595 · ● 360.4K ·

@jamesadevine
Copy link
Copy Markdown
Collaborator Author

Superseded by #602 ( eat/cli-lifecycle-family), which consolidates all six in-flight Phase 1 lifecycle PRs into a single linear-history branch. Closing this PR; review feedback should land on #602.

jamesadevine added a commit that referenced this pull request May 17, 2026
…emove/list/status/run/secrets) (#602)

* feat(cli): add ado-aw disable

Squash-merge of #591 (feat/cli-disable). See original PR for review history and detailed rationale.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(cli): add ado-aw remove

Squash-merge of #592 (feat/cli-remove).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(cli): add ado-aw list

Squash-merge of #594 (feat/cli-list).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(cli): add ado-aw status

Implements PR 7 of the Phase 1 CLI overhaul. Renders per-pipeline
status — name, id, folder, queueStatus, latest-run summary, and a
deep link — one block per matched ADO definition. Read-only.

`status` is intentionally a thin renderer over the same data path as
`list` (same `list_definitions` + `match_definitions` +
`get_latest_build` sequence). The `--json` shape is byte-for-byte
identical to `list --json` so scripts can use either.

CLI surface:

    ado-aw status [PATH] --org --project --pat --json

The block renderer is a pure function (`render_blocks`) tested
against six scenarios:

- empty → placeholder line
- succeeded run with url → all fields rendered
- run with no url → synthesizes
  `{org_url}/{project}/_build/results?buildId={id}`
- no last run → "never" + no url line
- in-progress run (no `result` yet) → shows `status` value instead
- missing queueStatus → renders `?` placeholder

Depends on PR 5 (#594) for `crate::list::{ListRow, LastRun,
build_rows, render_json}`; reuses the `get_latest_build` ado/mod.rs
helper landed there.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(cli): avoid duplicate ADO definition fetch in list and status

Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/c7c2866a-891d-48c3-8336-774bba086817

Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>

* feat(cli): add ado-aw run

Squash-merge of #595 (feat/cli-run).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(cli): rename configure to secrets with subcommands

Squash-merge of #600 (feat/cli-secrets).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(cli): percent-encode ctx.project in all ADO URL formatters

Code-review finding on PR #602: seven URL formatters in
`src/ado/mod.rs` interpolated `ctx.project` directly into the path
segment of the request URL while five sibling functions correctly
ran the value through `percent_encoding::utf8_percent_encode(...,
PATH_SEGMENT)`. Project names containing reserved characters
(spaces, `/`, `?`, `#`, `:` etc.) would have broken the URL or
silently produced surprising responses.

Affected functions, all now using the same encoder as
`get_repository_id`, `get_definition_full`, `patch_queue_status`,
`delete_definition`, and `create_definition`:

- `list_definitions`
- `get_definition_name`
- `update_pipeline_variable` (both GET and PUT URLs)
- `queue_build`
- `get_build`
- `get_latest_build`

The `info!()` log line at the top of `match_definitions` is
unaffected (logging, not URL construction).

The existing `path_segment_*` tests already cover the encoder
behaviour; no new test is needed since these are mechanical
substitutions of an existing pattern. Full `cargo test` (1567 unit
tests + integration crates) and `cargo clippy --all-targets
--all-features` are green after the fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(run): re-check --timeout between each in-flight get_build call

Code-review follow-up on PR #602: `poll_until_complete` only checked
`started.elapsed() >= timeout` at the top of each round, so with N
in-flight builds and reqwest's 30s per-call HTTP timeout, the
operator-visible wait could overshoot `--timeout` by up to N × 30s
in the pathological all-stalled case.

Re-checks the wall-clock budget between each individual `get_build`
call inside a round. When the budget is exhausted mid-round, the
current target and every remaining one are carried forward into
`pending` so the caller's `in_progress` count stays accurate (the
loop owes a status for everything it queued).

In the common case where the poll interval is several times the HTTP
timeout, the previous behaviour was indistinguishable from the new
one — the bug only matters when poll interval ≪ HTTP timeout, which
is an awkward but plausible configuration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(cli): percent-encode project in secrets/run; make --value-stdin conflict explicit

Three follow-ups on PR #602:

1. `src/secrets.rs::put_definition` was the last URL formatter using
   `ctx.project` unencoded. Now uses `PATH_SEGMENT` like every other
   builder in `src/ado/mod.rs`. `PATH_SEGMENT` was promoted from
   private `const` to `pub const` to support cross-module reuse.

2. `src/run.rs` was printing a deep-link to the queued build using
   unencoded `ado_ctx.project`. The URL is cosmetic (never used as an
   HTTP target), but it would render broken/unclickable for projects
   containing spaces or other URL-unsafe characters. Now encoded with
   the same `PATH_SEGMENT` encoder.

3. `ado-aw secrets set <value> --value-stdin` silently ignored
   `--value-stdin` when both were supplied (explicit positional value
   won). Added `conflicts_with = "value"` to the `value_stdin` clap
   arg so the combination is rejected at parse time with a clear
   error. Added an integration test in
   `tests/secrets_integration.rs` to pin the behaviour.

`cargo test` (1567 unit + 14 integration crates, all green) and
`cargo clippy --all-targets --all-features` pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(cli): encode synthesized URL, surface detect errors, document parse_parameters edge

Four follow-ups on PR #602:

1. `src/status.rs::render_blocks` synthesized fallback URL was passing
   `ado_project` verbatim into the path segment when `LastRun::url`
   was absent. Now uses `PATH_SEGMENT` like every other URL builder
   in the PR. URL is text-output only, but renders broken links for
   projects with spaces or reserved chars.

2. `src/list.rs` and `src/status.rs` were swallowing
   `detect_pipelines` errors via `.unwrap_or_default()`, making
   "detection failed" indistinguishable from "no pipelines compiled
   here" — both produce zero matches downstream. Both commands are
   read-only and useful even with partial inputs (`list --all`
   doesn't need fixtures at all), so we don't bail; we emit a
   `warning: failed to scan local pipelines: …` to stderr so the
   operator can distinguish the two cases.

3. `src/run.rs::parse_parameters` silently rejects values containing
   commas (the `,` split happens before the `=` split, so the
   trailing fragment falls into the "no `=`" rejection path). The
   behaviour is intentional — commas are the documented pair
   separator — but it was undocumented. Added a doc comment
   spelling out the constraint and the one-pair-per-flag
   workaround, plus a new
   `parse_parameters_values_with_commas_split_pre_equals` unit test
   that pins both the rejection and the workaround. The doc comment
   tells future contributors to update the test if comma escaping
   is ever added.

4. `src/secrets.rs::run_set_github_token` carries an undocumented
   invariant: the deprecation warning must be emitted before any
   fallible I/O, because the integration test
   `configure_invocation_still_works_and_warns` exercises it by
   driving the function with a path that triggers an early
   canonicalize failure. Added an `IMPORTANT — invariant for the
   integration test` doc comment so a later refactor that defers
   the `eprintln!` (e.g. lazy auth init) will spot the constraint.

`cargo test` (1568 unit + 14 integration crates, all green) and
`cargo clippy --all-targets --all-features` pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(cli): correct parse_parameters doc; cap consecutive poll errors; warn on empty status

Three follow-ups on PR #602:

1. `parse_parameters` doc-comment bug: the first bullet was tagged
   ✅ but described the same broken case as the third (✅
   `--parameters 'urls=a,b' --parameters mode=fast` still splits on
   the comma inside `urls=a,b` and fails on the trailing `b`
   fragment). Rewrote the bullet list so all broken examples are ❌
   and only the genuine "one pair per flag, no commas in values"
   workaround is ✅. Also clarified that there is currently no way
   to escape a comma inside a single `--parameters` argument, and
   pointed at the existing
   `parse_parameters_values_with_commas_split_pre_equals` unit
   test as the behaviour anchor.

2. `poll_until_complete` couldn't distinguish a permanent error
   (deleted build, revoked PAT, 404) from a transient one — both
   pushed the target back onto `next_pending` and silently retried
   until `--timeout`. Added a per-build `consecutive_errors:
   HashMap<u64, usize>` counter that resets on any successful poll
   and bails out of that specific build after
   `MAX_CONSECUTIVE_POLL_ERRORS = 3` consecutive failures, counting
   it as failed. Transient blips still retry; persistent failures
   surface within `3 × poll_interval` (default 30s) instead of
   waiting out the full `--timeout` (default 1800s).

3. `status` was silently rendering `(no matched definitions)` when
   the fixture matcher returned zero hits, which is
   indistinguishable from running in the wrong directory. Added an
   `eprintln!` warning that mirrors the existing
   `failed to scan local pipelines: …` message. The command stays
   non-fatal (read-only) by design, unlike `disable` which bails.

`cargo test` (1568 unit + 14 integration crates, all green) and
`cargo clippy --all-targets --all-features` pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(cli): no silent allowOverride downgrade; surface comma hint; harden dry-run

Four follow-ups on PR #602:

1. **`apply_variable_set`: silent `allowOverride` downgrade on
   secret rotation.** Previously, running `secrets set TOKEN <new>`
   without `--allow-override` would re-emit the variable with
   `allowOverride: false`, silently downgrading any variable that
   was previously created (manually or by another tool) with
   `allowOverride: true`. The legacy `configure` code in
   src/configure.rs had explicit preservation logic; the
   consolidated `apply_variable_set` had lost it.

   Changed the signature from `allow_override: bool` to
   `allow_override: Option<bool>`:
   - `Some(true)` / `Some(false)` — force the flag (CLI
     `--allow-override` passes `Some(true)`).
   - `None` — **preserve** existing variable's `allowOverride`
     when overwriting; default to `false` when creating.

   `run_set` translates the CLI flag: `--allow-override` → `Some(true)`;
   absence → `None`. The deprecation alias (`run_set_github_token`)
   stays at `allow_override: false` on the CLI side, which now maps
   to `None` (preserve) — restoring parity with the pre-consolidation
   `configure` body. Help text in `src/main.rs` and `docs/cli.md`
   updated. Five new unit tests pin the matrix:
   - `Some(true)` / `Some(false)` / `None` × create/overwrite
   - Specifically asserts `None` preserves `allowOverride: true`
     (the silent-downgrade regression guard).

2. **`run.rs::print_queue_plan` silent serialize-failure.**
   `serde_json::to_string_pretty(&body).unwrap_or_default()` would
   have printed blank output if serialization ever failed. The
   value is provably JSON-safe, but defensive code should surface
   regressions instead of silently swallowing them. Switched to
   `unwrap_or_else(|e| format!("<serialization error: {e}>"))`.

3. **`run.rs::parse_parameters` opaque comma-in-value error.**
   When a user writes `--parameters urls=https://a,b`, the error
   was `Invalid --parameters pair 'b': expected key=value (no '='
   found).` — technically accurate but doesn't hint at the comma
   constraint documented above the function. Added a
   raw-argument-contains-comma detection branch that produces a
   self-diagnosable hint: `... Hint: values must not contain
   commas. The raw argument '...' was split on ',' before the
   '=' split; use a separate --parameters flag per pair.`

4. **`run.rs::dispatch` deliberate partial-queue + `--wait`
   behaviour.** When `--wait` is set and some builds fail to
   queue, the code polls the successfully-queued ones rather than
   bailing early; `queue_failure` is folded into the final exit
   code. This is intentional and the only sensible UX, but lacked
   a comment. Added a multi-paragraph block explaining all three
   cases (partial queue, zero queued, all queued) and why
   `poll_until_complete` is called with the partial slice.

Not addressed (acknowledged follow-ups, tracked elsewhere):
- Sequential `get_latest_build` fanout in `list`/`status`. Already
  documented inline; tracked as a future improvement.

`cargo test` (1572 unit + 14 integration crates, all green) and
`cargo clippy --all-targets --all-features` pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(cli): remove bails when no fixtures found; surface --parameters comma constraint in --help

Two follow-ups on PR #602:

1. **`remove` silently exited Ok(()) when no fixtures were
   detected.** For a destructive command this is the wrong UX —
   running `ado-aw remove` in the wrong directory currently
   printed "No agentic pipelines found." and exited success,
   giving no signal that nothing happened. Now mirrors `disable`:
   bails with a non-zero exit and tells the operator which path
   was scanned plus the recovery hint:

       No local agentic pipeline fixtures were found under <path>.
       Run `ado-aw compile` first (or point `ado-aw remove` at the
       repo root). `remove` refuses to exit success in this state
       because it's destructive.

2. **`--parameters` comma constraint was documented in the module
   doc-comment but not in `--help` text.** A user who writes
   `--parameters redirect_uri=https://a,b` would only learn about
   the constraint by reading the source. Added an inline
   `VALUES MUST NOT CONTAIN COMMAS …` blurb to the clap `help`
   attribute and updated `docs/cli.md` to match. The integration
   test now asserts the constraint appears in `--help` so a
   refactor that drops the warning will be caught at CI.

`cargo test` (1572 unit + 14 integration crates, all green) and
`cargo clippy --all-targets --all-features` pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant