Skip to content

feat: devops-support - #13

Merged
jopmiddelkamp merged 2 commits into
developfrom
feature/devops-support
Jul 29, 2026
Merged

feat: devops-support#13
jopmiddelkamp merged 2 commits into
developfrom
feature/devops-support

Conversation

@jopmiddelkamp

Copy link
Copy Markdown
Contributor

No description provided.

Auto-detect the hosting provider from the origin remote URL
(dev.azure.com / *.visualstudio.com -> Azure DevOps, else GitHub) with a
bflow.hosting.provider git-config override. PRs on Azure DevOps are
created via the az CLI (azure-devops extension); the PR web URL is built
from the parsed org/project/repo since az's repository.webUrl is
unreliable. No new dependencies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI 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

This PR adds Azure DevOps support to bflow by auto-detecting the hosting provider from the origin remote URL (with a git-config override) and routing PR creation/auth checks through the appropriate CLI (gh or az).

Changes:

  • Introduces hosting provider detection (origin URL parsing + bflow.hosting.provider override).
  • Adds an Azure DevOps hosting backend using the Azure CLI + azure-devops extension.
  • Refactors hosting initialization and URL opening to be provider-agnostic.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/common/mod.rs Extends MockGit to support remote_url() for provider detection tests and flows.
src/menu.rs Simplifies Up-arrow selection logic using saturating_sub.
src/main.rs Adds provider detection + dynamic hosting backend creation and preflight checks.
src/hosting/mod.rs Adds provider modules and centralizes browser URL opening as a default trait method.
src/hosting/github.rs Removes provider-specific URL opening and adds Default impl.
src/hosting/devops.rs New Azure DevOps implementation using az repos pr ... commands.
src/hosting/detect.rs New provider detection logic based on origin remote and config override.
src/git/mod.rs Extends Git trait and GitCli with remote_url() for detection.
README.md Documents Azure DevOps requirements, provider detection, and template fallback behavior.
.claude/skills/bflow/SKILL.md Updates operational guidance to include Azure DevOps CLI commands.

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

Comment thread src/hosting/devops.rs
Comment on lines +128 to +135
fn check_auth(&self) -> Result<()> {
// Explicit extension check first: it also prevents az's interactive
// dynamic-install prompt from firing inside a non-tty command later.
self.run_az(&["extension".into(), "show".into(), "--name".into(), "azure-devops".into()])
.map_err(|_| "Azure DevOps CLI extension is missing. Run 'az extension add --name azure-devops'.".to_string())?;
self.run_az(&["account".into(), "show".into()]).map(|_| ())
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 287f27d. check_auth now probes actual repo access with az repos show (scoped to the detected org/project/repo) instead of az account show. This passes for both az login sessions and PAT auth via az devops login / AZURE_DEVOPS_EXT_PAT, and it also verifies the user can reach the repo itself. README and the bflow skill doc now mention PAT auth.

Comment thread src/hosting/mod.rs
Comment on lines +19 to +30
/// Open a URL in the OS default browser (platform dispatch, provider-agnostic).
pub fn open_in_browser(url: &str) -> Result<()> {
use std::process::Command;
#[cfg(target_os = "macos")]
let result = Command::new("open").arg(url).output();
#[cfg(target_os = "windows")]
let result = Command::new("cmd").args(["/C", "start", "", url]).output();
#[cfg(target_os = "linux")]
let result = Command::new("xdg-open").arg(url).output();
result.map_err(|e| format!("Failed to open URL: {e}"))?;
Ok(())
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 287f27d. open_in_browser now defines result as an Err on targets outside macOS/Windows/Linux, so it compiles everywhere and reports the platform as unsupported at runtime. It also checks the child exit status and surfaces stderr when the opener returns non-zero.

@jopmiddelkamp

Copy link
Copy Markdown
Contributor Author

Code review

Found 2 issues:

  1. AzureDevOps::check_auth discards the real az error, hiding the cause of auth failures (mirrors a bug already fixed for GitHub in ea3216c, which changed map_err(|_| "GitHub CLI is not authenticated...") to propagate the real stderr)

If az extension show fails for any reason other than a missing extension (expired login, wrong subscription, network error), the user only sees "extension is missing" with the real cause discarded.

// Explicit extension check first: it also prevents az's interactive
// dynamic-install prompt from firing inside a non-tty command later.
self.run_az(&["extension".into(), "show".into(), "--name".into(), "azure-devops".into()])
.map_err(|_| "Azure DevOps CLI extension is missing. Run 'az extension add --name azure-devops'.".to_string())?;
self.run_az(&["account".into(), "show".into()]).map(|_| ())
}

  1. Duplicated CLI-invocation helper between github.rs::run_gh and devops.rs::run_az (CLAUDE.md says "REMEMBER to ALWAYS keep things KISS, DRY and SOLID")

Both methods are structurally identical (spawn Command, check status.success(), trim stdout or format stderr into "<cli> <args> failed: <stderr>"), differing only in program name and arg-slice type. The PR already extracted open_in_browser into a shared function in hosting/mod.rs for the same reason, but didn't apply it here.

fn run_gh(&self, args: &[&str]) -> Result<String> {
let output = Command::new("gh").args(args).output()
.map_err(|e| format!("Failed to run gh: {e}"))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
Err(format!("gh {} failed: {}", args.join(" "), stderr))
}
}
}

fn run_az(&self, args: &[String]) -> Result<String> {
let output = Command::new("az").args(args).output()
.map_err(|e| format!("Failed to run az: {e}"))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
Err(format!("az {} failed: {}", args.join(" "), stderr))
}
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Comment thread src/menu.rs Outdated
if selected > 0 {
selected -= 1;
}
selected = selected.saturating_sub(1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This one-line refactor of show_select's Up-arrow handler (if selected > 0 { selected -= 1; }selected = selected.saturating_sub(1);) is behaviorally identical, but show_select is a generic terminal-menu widget completely unrelated to the Azure DevOps hosting feature this PR delivers — no call site or type in src/hosting/** or src/main.rs's create_hosting() touches it.

Per CLAUDE.md's Minimal Impact rule ("Changes should only touch what's necessary"), drive-by cleanups like this should be split into a separate commit/PR so the feature diff stays scoped. Not a bug — pure scope concern.

Reference:

beans-gitflow/src/menu.rs

Lines 167 to 171 in 34efd59

}
(KeyCode::Up, _) => {
selected = selected.saturating_sub(1);
}
(KeyCode::Down, _) => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted in 287f27d. The saturating_sub cleanup came from a clippy suggestion, but it is unrelated to this feature, so src/menu.rs is back to the develop version and the diff stays scoped. The cleanup can go in a separate chore PR.

- Extract shared run_cli helper in hosting/mod.rs; run_gh/run_az delegate
  to it (DRY)
- check_auth (Azure DevOps): propagate the real az error instead of
  swallowing it, and probe repo access via 'az repos show' instead of
  'az account show' so PAT auth (az devops login) passes preflight
- open_in_browser: compile-safe fallback for unsupported platforms and
  surface non-zero exit statuses as errors
- Revert unrelated menu.rs cleanup to keep the feature diff scoped
- Sync README.md and bflow SKILL.md auth wording (PAT support)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jopmiddelkamp

Copy link
Copy Markdown
Contributor Author

Both issues from the review are fixed in 287f27d.

1. check_auth error swallowing

The extension check now appends the real az error to the hint instead of discarding it, matching the pattern from ea3216c. The az account show step is replaced by an az repos show probe, which also resolves the PAT-auth preflight issue Copilot raised.

2. Duplicated run_gh / run_az

Extracted a shared run_cli(program, args) helper in hosting/mod.rs; both run_gh and run_az are now one-line delegates to it.

Also reverted the unrelated menu.rs cleanup and fixed the open_in_browser portability/exit-status issue from the inline comments. All 96 tests pass.

@jopmiddelkamp
jopmiddelkamp merged commit ffa55a8 into develop Jul 29, 2026
13 checks passed
@jopmiddelkamp
jopmiddelkamp deleted the feature/devops-support branch July 29, 2026 03:25
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.

2 participants