Skip to content

Addressing PR comments#130

Merged
ewega merged 6 commits intomainfrom
codex/add-azure-devops-support
Mar 6, 2026
Merged

Addressing PR comments#130
ewega merged 6 commits intomainfrom
codex/add-azure-devops-support

Conversation

@Codex
Copy link
Contributor

@Codex Codex AI commented Mar 5, 2026

No description provided.

@Codex Codex AI assigned Codex and ewega Mar 5, 2026
@Codex Codex AI changed the title [WIP] Add Azure DevOps as supported tool in CLI Add Azure DevOps plugin support with remote scope selection Mar 5, 2026
@ewega ewega marked this pull request as ready for review March 5, 2026 15:31
Copilot AI review requested due to automatic review settings March 5, 2026 15:31
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class Azure DevOps support to the gh-devlake GitHub CLI extension by enabling the upstream azuredevops_go plugin in the connection registry and providing an interactive remote-scope (project → repo) selection flow.

Changes:

  • Enable azuredevops_go in connectionRegistry, including PAT env var fallback (AZURE_DEVOPS_PATAZDO_PAT) and alias support for azure-devops.
  • Add an Azure DevOps scope handler that pages through remote scopes, lets users select projects/repositories, and PUTs selected scopes to DevLake.
  • Update tests and documentation/README to reflect Azure DevOps availability and token key names.

Reviewed changes

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

Show a summary per file
File Description
cmd/connection_types.go Enables Azure DevOps connection definition and adds azure-devopsazuredevops_go aliasing in lookup.
cmd/configure_scopes.go Implements Azure DevOps remote-scope browsing and scope creation (projects + repos).
cmd/connection_types_test.go Adds registry entry validation for Azure DevOps and updates token-expiry test plugin slug.
cmd/configure_connection_add_test.go Updates plugin selection test to validate alias resolution.
internal/token/resolve_test.go Adds AZDO_PAT fallback to Azure DevOps token resolution tests.
docs/token-handling.md Documents Azure DevOps/GitLab as supported and lists env key precedence and required access notes.
README.md Marks Azure DevOps as available in supported plugins table and updates required PAT access text.
Comments suppressed due to low confidence (1)

cmd/connection_types.go:336

  • The azure-devops alias is only normalized inside FindConnectionDef, but other flows (e.g. scope add) pass the raw plugin slug through to state lookups and DevLake API calls. If a user runs --plugin azure-devops, resolveConnectionID will try ListConnections("azure-devops") and fail (DevLake expects azuredevops_go), and it also won’t match the saved state (which stores def.Plugin). Consider canonicalizing the plugin slug once (e.g. return the canonical slug alongside the def, or add a NormalizePluginSlug helper used everywhere) and ensure all API/state calls use the canonical slug.
// FindConnectionDef returns the def for the given plugin slug, or nil.
func FindConnectionDef(plugin string) *ConnectionDef {
	switch plugin {
	case "azure-devops":
		plugin = "azuredevops_go"
	}
	for _, d := range connectionRegistry {
		if d.Plugin == plugin {
			return d
		}

Comment on lines +118 to +127
func TestResolve_AzureDevOps_AZDO_EnvVar(t *testing.T) {
t.Setenv("AZURE_DEVOPS_PAT", "")
t.Setenv("AZDO_PAT", "azdo_fallback")
result, err := Resolve(adoOpts("", ""))
if err != nil {
t.Fatal(err)
}
if result.Token != "azdo_fallback" || result.Source != "environment" {
t.Errorf("got token=%q source=%q, want token=%q source=%q", result.Token, result.Source, "azdo_fallback", "environment")
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

This env-var test calls Resolve(adoOpts("", "")), which makes Resolve check the default .devlake.env before env vars. If a developer has a .devlake.env in the repo (or CWD) with Azure keys, the test can become flaky by resolving from envfile instead of environment. To make the test deterministic, pass a non-existent env file path (or a temp file without those keys) when constructing ResolveOpts for env-var-only tests.

Copilot uses AI. Check for mistakes.
Comment on lines 60 to 71
func TestSelectPlugin_UnavailablePlugin(t *testing.T) {
_, err := selectPlugin("azure-devops")
if err == nil {
t.Fatal("expected error for unavailable plugin, got nil")
def, err := selectPlugin("azure-devops")
if err != nil {
t.Fatalf("expected alias resolution for azure-devops, got error: %v", err)
}
if !strings.Contains(err.Error(), "coming soon") {
t.Errorf("unexpected error message: %v", err)
if def == nil {
t.Fatal("expected ConnectionDef, got nil")
}
if def.Plugin != "azuredevops_go" {
t.Errorf("expected plugin %q, got %q", "azuredevops_go", def.Plugin)
}
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

The test name TestSelectPlugin_UnavailablePlugin no longer matches what the test is asserting (it now verifies alias resolution for azure-devops, not an unavailable plugin error). Renaming it to reflect alias behavior would keep the suite self-describing.

Copilot uses AI. Check for mistakes.
@ewega
Copy link
Contributor

ewega commented Mar 5, 2026

@codex[agent] Please fix: Rename TestSelectPlugin_UnavailablePlugin to reflect that it now tests alias resolution for azure-devops, not an unavailable plugin error. Something like TestSelectPlugin_AzureDevOpsAlias would be appropriate.

@Codex
Copy link
Contributor Author

Codex AI commented Mar 5, 2026

@codex[agent] Please fix: Rename TestSelectPlugin_UnavailablePlugin to reflect that it now tests alias resolution for azure-devops, not an unavailable plugin error. Something like `TestSelectPlugin_...

Renamed the test to cover the azure-devops alias explicitly (commit a789e78).

@Codex Codex AI changed the title Add Azure DevOps plugin support with remote scope selection Addressing PR comments Mar 5, 2026
@ewega ewega requested a review from Copilot March 5, 2026 17:38
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment on lines +329 to +332
switch plugin {
case "azure-devops":
plugin = "azuredevops_go"
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

FindConnectionDef maps the legacy slug "azure-devops" to "azuredevops_go", but other flows still use the user-supplied slug for state lookup and DevLake API calls (e.g., resolveConnectionID compares c.Plugin == plugin and calls client.ListConnections(plugin)). This means --plugin azure-devops can validate successfully but later fail to find/list connections because the backend/state uses the canonical "azuredevops_go" slug. Consider introducing a single slug-canonicalization helper (or alias map on the registry) and ensure callers use def.Plugin (canonical) for subsequent operations, not the raw flag value.

Copilot uses AI. Check for mistakes.
if _, ok := payload["name"]; !ok || payload["name"] == "" {
payload["name"] = child.Name
}
if _, ok := payload["fullName"]; !ok && child.FullName != "" {
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

azureDevOpsScopePayload only populates fullName when the key is missing, but not when it exists with an empty value. This is inconsistent with the id/name handling (missing OR empty) and can leave fullName blank even though child.FullName is available. It should probably set fullName when the existing value is empty as well.

Suggested change
if _, ok := payload["fullName"]; !ok && child.FullName != "" {
if _, ok := payload["fullName"]; (!ok || payload["fullName"] == "") && child.FullName != "" {

Copilot uses AI. Check for mistakes.
Comment on lines +618 to +647
func azureDevOpsScopePayload(child devlake.RemoteScopeChild, connID int) map[string]any {
var payload map[string]any
if len(child.Data) > 0 {
_ = json.Unmarshal(child.Data, &payload)
}
if payload == nil {
payload = make(map[string]any)
}
if _, ok := payload["id"]; !ok || payload["id"] == "" {
payload["id"] = child.ID
}
if _, ok := payload["name"]; !ok || payload["name"] == "" {
payload["name"] = child.Name
}
if _, ok := payload["fullName"]; !ok && child.FullName != "" {
payload["fullName"] = child.FullName
}
payload["connectionId"] = connID
return payload
}

func azureScopeLabel(child devlake.RemoteScopeChild) string {
if child.FullName != "" {
return child.FullName
}
if child.Name != "" {
return child.Name
}
return child.ID
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The new Azure DevOps scope helper logic (azureScopeLabel, azureDevOpsScopePayload, and pagination in listAzureDevOpsRemoteChildren) isn’t covered by unit tests. Since cmd/configure_scopes.go already has a substantial test suite, adding focused tests for these helpers (e.g., fullName/id/name fallback behavior) would help prevent regressions.

Copilot uses AI. Check for mistakes.
@ewega
Copy link
Contributor

ewega commented Mar 6, 2026

@codex[agent] Please fix these two issues:

  1. Slug canonicalization bugFindConnectionDef maps "azure-devops" to "azuredevops_go", but callers may still use the raw flag value "azure-devops" for subsequent API calls like client.ListConnections(plugin) and state lookups. Ensure that after selectPlugin returns, all subsequent code uses def.Plugin (the canonical "azuredevops_go") instead of the raw flag value. Check runAddConnection and runScopeAdd to make sure they pass def.Plugin to API calls, not the user-provided connPlugin/scopePlugin string.

  2. fullName empty-value handling — In azureDevOpsScopePayload, the fullName fallback only triggers when the key is missing from the map, but not when it exists with an empty string. Change the condition to also set fullName when the existing value is empty: if v, ok := payload["fullName"]; (!ok || v == "") && child.FullName != "" {

@Codex
Copy link
Contributor Author

Codex AI commented Mar 6, 2026

@codex[agent] Please fix these two issues:

  1. Slug canonicalization bugFindConnectionDef maps "azure-devops" to "azuredevops_go", but callers may still use the raw flag value `"azure-d...

Applied canonical slug usage after plugin selection and updated Azure DevOps fullName fallback (commit 15225ac).

@Codex Codex AI changed the title Addressing PR comments Address PR feedback for Azure DevOps alias handling and scope tests Mar 6, 2026
@ewega ewega requested a review from Copilot March 6, 2026 10:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Comment on lines 59 to 69
@@ -64,7 +65,7 @@ func runDeleteConnection(cmd *cobra.Command, args []string) error {
}

// ── Resolve plugin + ID ──
plugin := connDeletePlugin
plugin := canonicalPlugin
connID := connDeleteID
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

In flag mode, plugin is now canonicalized via canonicalPluginSlug, but the state update logic later removes entries only when c.Plugin == plugin. If an existing state file contains the legacy alias (e.g. azure-devops), the DevLake API delete will succeed but the state entry will not be removed, leaving stale local state. Consider canonicalizing both sides when filtering state connections (e.g., compare canonicalPluginSlug(c.Plugin) to plugin).

Copilot uses AI. Check for mistakes.
Comment on lines 58 to 83
@@ -78,7 +79,7 @@ func runUpdateConnection(cmd *cobra.Command, args []string) error {
var connID int

if flagMode {
plugin = updateConnPlugin
plugin = canonicalPlugin
connID = updateConnID
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

In flag mode, plugin is now canonicalized (canonicalPluginSlug(updateConnPlugin)), but the state update loop later matches with if c.Plugin == plugin && c.ConnectionID == updated.ID. If a state file contains the legacy alias (e.g. azure-devops), the connection will update successfully in DevLake but the local state file won’t be updated. Consider canonicalizing plugin slugs when matching state entries (e.g., compare canonicalPluginSlug(c.Plugin) to plugin) so alias handling stays consistent for existing state files.

Copilot uses AI. Check for mistakes.
func azureDevOpsScopePayload(child devlake.RemoteScopeChild, connID int) map[string]any {
var payload map[string]any
if len(child.Data) > 0 {
_ = json.Unmarshal(child.Data, &payload)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

azureDevOpsScopePayload ignores json.Unmarshal errors. If the remote-scope API ever returns malformed/partial JSON in child.Data, this will silently drop fields and proceed with a potentially invalid/incomplete payload. Consider checking the unmarshal error and returning it (or at least falling back to an empty payload with a warning) so failures are diagnosable and you don’t PUT scopes with unexpected shape.

Suggested change
_ = json.Unmarshal(child.Data, &payload)
if err := json.Unmarshal(child.Data, &payload); err != nil {
fmt.Printf("\n⚠️ failed to decode Azure DevOps scope data for child %s: %v\n", child.ID, err)
payload = make(map[string]any)
}

Copilot uses AI. Check for mistakes.
Comment on lines 71 to 78
if opts.Plugin != "" {
def, err := requirePlugin(opts.Plugin)
if err != nil {
return err
}
selectedPlugin = opts.Plugin
selectedDef = def
selectedPlugin = def.Plugin
// Warn about flags that don't apply to the selected plugin.
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

runScopeAdd now canonicalizes selectedPlugin to def.Plugin. However, connection auto-detection still relies on resolveConnectionID, which checks state.Connections[i].Plugin == plugin (exact string match). This will break resolving connection IDs from existing state files that stored the legacy alias (e.g. azure-devops) even though the CLI accepts that alias. Consider canonicalizing plugin slugs when comparing against state (e.g., compare canonicalPluginSlug(c.Plugin) to the selected plugin) so alias support works end-to-end.

Copilot uses AI. Check for mistakes.
@ewega
Copy link
Contributor

ewega commented Mar 6, 2026

@codex[agent] Please fix these two issues:

  1. Canonicalize plugin slugs in state file comparisons — The canonicalPluginSlug function exists but state file lookups still do exact string matches. In these files, when comparing c.Plugin == plugin, wrap the state side too: canonicalPluginSlug(c.Plugin) == plugin:

    • cmd/configure_connection_delete.go (line ~69)
    • cmd/configure_connection_update.go (line ~83)
    • cmd/configure_scope_add.go (line ~78, in resolveConnectionID)
      This ensures users with legacy "azure-devops" entries in their state files can still operate on those connections.
  2. Handle unmarshal errors in azureDevOpsScopePayloadjson.Unmarshal errors are silently ignored. Add error handling: if unmarshal fails, print a warning and initialize an empty payload map so the function still works but failures are diagnosable.

@Codex
Copy link
Contributor Author

Codex AI commented Mar 6, 2026

@codex[agent] Please fix these two issues:

  1. Canonicalize plugin slugs in state file comparisons — The canonicalPluginSlug function exists but state file lookups still do exact string matche...

Canonicalized state comparisons using canonicalPluginSlug and added Azure scope payload unmarshal warning handling (commit 9bb0346).

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.

@ewega
Copy link
Contributor

ewega commented Mar 6, 2026

@codex[agent] Please fix: Your changes to README.md and docs/token-handling.md convert CRLF line endings to LF in the modified sections, creating mixed line endings. Please ensure all modified lines use CRLF (\r\n) to match the rest of each file. This was flagged in Wave 1 too — please be careful with line endings.

@Codex
Copy link
Contributor Author

Codex AI commented Mar 6, 2026

@codex[agent] Please fix: Your changes to README.md and docs/token-handling.md convert CRLF line endings to LF in the modified sections, creating mixed line endings. Please ensure all modified lin...

Normalized README.md and docs/token-handling.md back to CRLF line endings (commit 2eb15c3).

@Codex Codex AI changed the title Address PR feedback for Azure DevOps alias handling and scope tests Addressing PR comments Mar 6, 2026
@ewega ewega requested a review from Copilot March 6, 2026 11:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Comment on lines +145 to 155
canonical := plugin
if state != nil {
for _, c := range state.Connections {
if c.Plugin == plugin {
if canonicalPluginSlug(c.Plugin) == canonical {
return c.ConnectionID, nil
}
}
}
conns, err := client.ListConnections(plugin)
conns, err := client.ListConnections(canonical)
if err != nil {
return 0, fmt.Errorf("could not list %s connections: %w", plugin, err)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

resolveConnectionID sets canonical := plugin but then compares canonicalPluginSlug(c.Plugin) against canonical and calls ListConnections(canonical). If plugin is an alias (e.g. azure-devops), this will not match state entries stored under the canonical slug and may call the API with the wrong plugin slug. Canonicalize the input up front (e.g. canonical := canonicalPluginSlug(plugin)) and use that for both the state comparison and the ListConnections call (while keeping user-facing messages based on the original plugin/display name).

Copilot uses AI. Check for mistakes.
@ewega ewega force-pushed the codex/add-azure-devops-support branch from 2eb15c3 to f181a2e Compare March 6, 2026 12:42
@ewega ewega merged commit 6d20edc into main Mar 6, 2026
10 checks passed
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.

3 participants