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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,22 @@ Both flags route through `ado-aw`'s `discover_ado_aw_pipelines` machinery, which
- `--dry-run` - Print the planned actions (and the full POST body for creates) without calling the ADO API.
- `--also-set-token` - After creating a new definition, set its `GITHUB_TOKEN` variable (as an ADO secret).
- `--token <value>` - The token value for `--also-set-token`. Falls back to `$GITHUB_TOKEN`, then to an interactive prompt. Requires `--also-set-token`.

**Source-repo scope (Phase 1):** `enable` requires the local git remote to be an Azure DevOps Git remote (the source repo is what gets registered as the definition's repository). GitHub-hosted source repos are gated on a follow-up.
- `--service-connection <name-or-guid>` - GitHub service-connection name (e.g. `ado-aw-github`) or GUID. **Required when the source repository is on GitHub.** Rejected (with a clear error) when the source is Azure DevOps Git.
- `--repository-name <owner/repo>` - Source repository override (GitHub source only). Auto-detected from the git remote when omitted; useful when the local checkout's remote points at a fork rather than the canonical source repo the deployment should reference.

**Source-repo autodetect:** `enable` parses `git remote get-url origin` and routes to either the TfsGit or GitHub create-definition body shape automatically. ADO Git remotes (`dev.azure.com/...`, legacy `*.visualstudio.com`) emit a `TfsGit` body. `github.com` remotes (HTTPS or SSH) emit a `GitHub` body whose `repository.properties.connectedServiceId` references the project-level GitHub service connection resolved from `--service-connection`. If the local remote can't be parsed at all, pass both `--repository-name owner/repo` and `--service-connection` explicitly. GitHub Enterprise (`github.example.com`) is not supported in v1.

**GitHub-source pre-requisites:** create a GitHub service connection in the target ADO project (Project settings → Service connections → GitHub) before running `enable --service-connection ...`. ADO has no REST API for creating service connections — this is a one-time portal action. Then either install the Azure Pipelines GitHub App on the source repo or paste a fine-grained PAT.

**Example (smoke fixtures registered in `msazuresphere/AgentPlayground` from a `githubnext/ado-aw` checkout):**
```powershell
ado-aw enable `
--org msazuresphere --project AgentPlayground `
--service-connection ado-aw-github `
--folder '\smoke' `
tests/safe-outputs/
```
Re-running is idempotent. `disable`, `remove`, `list`, `status`, and `run` work against the same definitions from the same GitHub checkout — they match by YAML path (provider-agnostic) and require explicit `--org`/`--project` because the git remote can't infer the deployment target for GitHub-source pipelines.

- `disable [PATH]`- Set `queueStatus` to `disabled` (default) or `paused` on every ADO build definition that matches a local fixture under `PATH`. Refuses to touch any ADO definition that is not the target of a local fixture match — that safety property falls naturally out of the same yaml-path + name match used by `configure`. Skips definitions that are already at the requested status; fail-soft per fixture; exits non-zero if any patch failed or if zero local fixtures matched ADO definitions.
- `--org <url>` - Override: Azure DevOps organization (URL or bare org name). Inferred from git remote by default.
Expand Down
74 changes: 73 additions & 1 deletion src/ado/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,21 @@ fn normalize_repo_url(url: &str) -> String {
let decoded = percent_encoding::percent_decode_str(url)
.decode_utf8_lossy()
.into_owned();
decoded.trim_end_matches('/').to_ascii_lowercase()
// Three-pass trim handles every realistic and pathological form
// without depending on the order ADO and GitHub emit them in:
// `repo` → `repo`
// `repo/` → `repo`
// `repo.git` → `repo`
// `repo.git/` → `repo` (trailing `/` first, then `.git`)
// `repo/.git` → `repo` (no trailing `/`, then `.git`, then `/`)
// The final `/` trim only matters for the `/.git` form — no
// real-world ADO or GitHub URL takes that shape, but the cost
// here is one extra method call, so we may as well be exhaustive.
decoded
.trim_end_matches('/')
.trim_end_matches(".git")
.trim_end_matches('/')
.to_ascii_lowercase()
}

/// Build a `(normalized yamlFilename → local lock path)` lookup table
Expand Down Expand Up @@ -911,6 +925,64 @@ mod tests {
assert_eq!(kept[0].id, 2);
}

#[test]
fn scope_current_repo_matches_github_source_with_dotgit_suffix() {
// ADO stores the repository.url for a GitHub-source definition
// typically without a `.git` suffix, but `build_create_body`
// POSTs it with `.git`. `normalize_repo_url` strips the suffix
// so both forms compare equal — this pins that contract for
// the GitHub-source CurrentRepo filter.
let defs = vec![def_with(
42,
"gh-source",
None,
Some("https://github.com/githubnext/ado-aw"),
)];
let current = Some("https://github.com/githubnext/ado-aw.git".to_string());
let kept = apply_scope_filter(defs, &DiscoveryScope::CurrentRepo, &current);
assert_eq!(kept.len(), 1);
assert_eq!(kept[0].id, 42);

// And the reverse — stored with `.git`, queried without.
let defs = vec![def_with(
43,
"gh-source-rev",
None,
Some("https://github.com/githubnext/ado-aw.git"),
)];
let current = Some("https://github.com/githubnext/ado-aw".to_string());
let kept = apply_scope_filter(defs, &DiscoveryScope::CurrentRepo, &current);
assert_eq!(kept.len(), 1);
assert_eq!(kept[0].id, 43);
}

// ── normalize_repo_url ──────────────────────────────────────────

/// `normalize_repo_url`'s three-pass strip must collapse every
/// realistic *and* pathological suffix combination to the same
/// canonical form. Pins the contract so a future reorder of the
/// `.trim_end_matches` chain can't silently break it.
#[test]
fn normalize_repo_url_collapses_all_suffix_forms() {
let want = "https://github.com/githubnext/ado-aw";
for input in [
"https://github.com/githubnext/ado-aw",
"https://github.com/githubnext/ado-aw/",
"https://github.com/githubnext/ado-aw.git",
"https://github.com/githubnext/ado-aw.git/",
"https://github.com/githubnext/ado-aw/.git",
// Casing normalises too.
"HTTPS://GITHUB.COM/GitHubNext/ADO-AW.git",
] {
assert_eq!(
normalize_repo_url(input),
want,
"normalize_repo_url({:?}) should produce the canonical form",
input
);
}
}

// ── is_direct_match ──────────────────────────────────────────────

#[test]
Expand Down
Loading
Loading