Conversation
gh devlake query command with pipelines subcommand
|
@claude[agent] Thanks — this is a good start, but it does not satisfy issue #62 as written, so please continue on the same PR rather than sending this for review yet. Blocking gaps to address:
Please revise this PR to implement the agreed architecture and acceptance criteria for #62, including the real Validation still expected before handoff:
|
Understood. I've investigated the DB access requirement and confirmed a hard architectural constraint: DevLake does not expose database credentials to external clients. The MySQL database is internal to the DevLake container/service and not accessible from the CLI. State files ( Evidence:
Two paths forward: Option A (Recommended): Implement Option B: Request upstream DevLake enhancement to expose metrics API endpoints for DORA/Copilot, then implement as originally specified once available. Which approach should I take? If Option A, I'll implement the full |
gh devlake query command with pipelines subcommand|
@claude[agent] I verified the architecture constraint in the repo and I am accepting Option A. Please continue this PR as an API backed |
✅ Implementation complete (811b498) What was done:
What's clearly documented as impossible without upstream changes:
Validation: All checks pass: The query foundation is now mergeable and moves #62 materially forward. |
|
@claude[agent] Nice improvement — this is much closer. Please address these last items on the same PR before we move it into formal review:
After updating, rerun:
Then comment back with the new commit hash. |
✅ All items addressed (1620812)
Validation: All checks pass - |
|
@claude[agent] One substantive blocker remains before I move this into formal review: URL/query injection in
After fixing, rerun:
Then comment back with the new commit hash. |
✅ URL injection vulnerability fixed (5a69bdc) Replaced manual string concatenation with Validation: All checks pass - |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
docs/query.md:151
- The Copilot JSON example shows a nested
projectobject inavailableData, but the implementation currently returnsprojectName,copilotConnectionsFound, and optionallyconnections. Update the example to match the actual output shape to prevent breaking user scripts.
```json
{
"project": "my-team",
"timeframe": "30d",
"availableData": {
"project": { "name": "my-team", "blueprint": {...} },
"connections": [...]
},
"limitations": "Copilot metrics in _tool_gh_copilot_* tables require metrics API..."
}
README.md
Outdated
| | `gh devlake query dora` | Query DORA metrics (placeholder — requires API) | [query.md](docs/query.md) | | ||
| | `gh devlake query copilot` | Query Copilot metrics (placeholder — requires API) | [query.md](docs/query.md) | |
cmd/query.go
Outdated
| metrics in a structured format (JSON by default, --format table for | ||
| human-readable output). |
docs/query.md
Outdated
| "project": "my-team", | ||
| "timeframe": "30d", | ||
| "availableData": { | ||
| "project": { "name": "my-team", "blueprint": {...} } |
internal/query/copilot.go
Outdated
| } | ||
|
|
||
| if len(connections) > 0 { | ||
| availableData["connections"] = connections |
README.md
Outdated
| | `gh devlake query dora` | Query DORA metrics (placeholder — requires API) | [query.md](docs/query.md) | | ||
| | `gh devlake query copilot` | Query Copilot metrics (placeholder — requires API) | [query.md](docs/query.md) | |
internal/query/copilot.go
Outdated
| if len(connections) > 0 { | ||
| availableData["connections"] = connections | ||
| } | ||
|
|
internal/query/registry.go
Outdated
| func Register(def *QueryDef) { | ||
| if def == nil || def.Name == "" { | ||
| panic("cannot register nil or unnamed query") | ||
| } |
internal/query/types.go
Outdated
| // QueryResult wraps the output of a query execution. | ||
| type QueryResult struct { | ||
| Data interface{} // the actual result data | ||
| Metadata map[string]string // optional metadata about the query | ||
| } |
internal/query/engine.go
Outdated
| // Apply defaults and validate parameters | ||
| for _, param := range queryDef.Params { | ||
| if _, ok := params[param.Name]; !ok { | ||
| // Parameter not provided | ||
| if param.Default != "" { | ||
| // Apply default value | ||
| params[param.Name] = param.Default | ||
| } else if param.Required { | ||
| // Required parameter missing with no default | ||
| return nil, fmt.Errorf("required parameter %q not provided", param.Name) | ||
| } |
cmd/query.go
Outdated
| metrics in a structured format (JSON by default, --format table for | ||
| human-readable output). |
| q.Set("blueprint_id", fmt.Sprintf("%d", blueprintID)) | ||
| } | ||
| if page > 0 { | ||
| q.Set("page", fmt.Sprintf("%d", page)) | ||
| } | ||
| if pageSize > 0 { | ||
| q.Set("pagesize", fmt.Sprintf("%d", pageSize)) |
cmd/query_pipelines.go
Outdated
| if queryPipelinesFormat != "json" && queryPipelinesFormat != "table" { | ||
| return fmt.Errorf("invalid --format value %q: must be 'json' or 'table'", queryPipelinesFormat) | ||
| } | ||
|
|
cmd/query_pipelines_test.go
Outdated
| func TestQueryPipelines_JSONOutputNoBanner(t *testing.T) { | ||
| // Mock DevLake API | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| if r.URL.Path == "/ping" { | ||
| w.WriteHeader(http.StatusOK) | ||
| return | ||
| } | ||
| if r.URL.Path == "/pipelines" { | ||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(devlake.PipelineListResponse{ | ||
| Pipelines: []devlake.Pipeline{ | ||
| { | ||
| ID: 123, | ||
| Status: "TASK_COMPLETED", | ||
| FinishedTasks: 10, | ||
| TotalTasks: 10, | ||
| }, | ||
| }, | ||
| Count: 1, | ||
| }) | ||
| return | ||
| } |
| q.Set("blueprint_id", fmt.Sprintf("%d", blueprintID)) | ||
| } | ||
| if page > 0 { | ||
| q.Set("page", fmt.Sprintf("%d", page)) | ||
| } | ||
| if pageSize > 0 { | ||
| q.Set("pagesize", fmt.Sprintf("%d", pageSize)) |
README.md
Outdated
|
|
||
| This CLI makes that setup fast and repeatable from the terminal (instead of clicking through the Config UI) — especially when you want to re-run the same configuration across teams. | ||
|
|
||
| > **Blog post:** [Beyond Copilot Dashboards: Measuring What AI Actually Changes](<!-- TODO: replace with actual blog URL -->) — why DORA + Copilot correlation matters and what this tool enables. |
cmd/query.go
Outdated
| var queryCmd = &cobra.Command{ | ||
| Use: "query", | ||
| Short: "Query DevLake data and metrics", | ||
| Long: `Query DevLake's aggregated data and metrics. | ||
|
|
||
| Retrieve pipeline status plus DORA/Copilot query results in structured | ||
| JSON output. Individual subcommands may provide extra formatting options | ||
| such as query pipelines --format table for human-readable output. | ||
|
|
||
| Examples: | ||
| gh devlake query pipelines --project my-team | ||
| gh devlake query pipelines --limit 20 | ||
| gh devlake query pipelines --status TASK_COMPLETED`, | ||
| } | ||
|
|
||
| func init() { | ||
| queryCmd.GroupID = "operate" | ||
| rootCmd.AddCommand(queryCmd) | ||
| } |
cmd/query_pipelines.go
Outdated
| var queryPipelinesCmd = &cobra.Command{ | ||
| Use: "pipelines", | ||
| Short: "Query recent pipeline runs", | ||
| Long: `Query recent pipeline runs for a project or across all projects. | ||
|
|
||
| Retrieves pipeline execution history with status, timing, and task completion | ||
| information. Output is JSON by default; use --format table for human-readable display. | ||
|
|
||
| Examples: | ||
| gh devlake query pipelines | ||
| gh devlake query pipelines --project my-team | ||
| gh devlake query pipelines --status TASK_COMPLETED --limit 10 | ||
| gh devlake query pipelines --format table`, | ||
| RunE: runQueryPipelines, | ||
| } | ||
|
|
||
| func init() { | ||
| queryPipelinesCmd.Flags().StringVar(&queryPipelinesProject, "project", "", "Filter by project name") | ||
| queryPipelinesCmd.Flags().StringVar(&queryPipelinesStatus, "status", "", "Filter by status (TASK_CREATED, TASK_RUNNING, TASK_COMPLETED, TASK_FAILED)") | ||
| queryPipelinesCmd.Flags().IntVar(&queryPipelinesLimit, "limit", 20, "Maximum number of pipelines to return") | ||
| queryPipelinesCmd.Flags().StringVar(&queryPipelinesFormat, "format", "json", "Output format (json or table)") | ||
| queryCmd.AddCommand(queryPipelinesCmd) | ||
| } |
cmd/query_dora.go
Outdated
| var queryDoraCmd = &cobra.Command{ | ||
| Use: "dora", | ||
| Short: "Query DORA metrics (limited by available API data)", | ||
| Long: `Query DORA (DevOps Research and Assessment) metrics for a project. | ||
|
|
||
| NOTE: Full DORA metric calculations (deployment frequency, lead time, change | ||
| failure rate, MTTR) require SQL queries against DevLake's domain layer tables. | ||
| DevLake does not expose database credentials or a metrics API endpoint. | ||
|
|
||
| This command returns project metadata and explains what additional API | ||
| endpoints would be needed to compute DORA metrics via CLI. | ||
|
|
||
| DORA metrics are currently available in Grafana dashboards at your DevLake | ||
| Grafana endpoint (shown in 'gh devlake status').`, | ||
| RunE: runQueryDora, | ||
| } | ||
|
|
||
| func init() { | ||
| queryDoraCmd.Flags().StringVar(&queryDoraProject, "project", "", "Project name (required)") | ||
| queryDoraCmd.Flags().StringVar(&queryDoraTimeframe, "timeframe", "30d", "Time window for metrics (e.g., 7d, 30d, 90d)") | ||
| queryCmd.AddCommand(queryDoraCmd) | ||
| } |
cmd/query_copilot.go
Outdated
| var queryCopilotCmd = &cobra.Command{ | ||
| Use: "copilot", | ||
| Short: "Query Copilot usage metrics (limited by available API data)", | ||
| Long: `Query GitHub Copilot usage metrics for a project. | ||
|
|
||
| NOTE: GitHub Copilot usage metrics (total seats, active users, acceptance rates, | ||
| language breakdowns, editor usage) are stored in _tool_gh_copilot_* tables and | ||
| visualized in Grafana dashboards, but DevLake does not expose a /metrics or | ||
| /copilot API endpoint. | ||
|
|
||
| This command returns available connection metadata and explains what additional | ||
| API endpoints would be needed to retrieve Copilot metrics via CLI. | ||
|
|
||
| Copilot metrics are currently available in Grafana dashboards at your DevLake | ||
| Grafana endpoint (shown in 'gh devlake status').`, | ||
| RunE: runQueryCopilot, | ||
| } | ||
|
|
||
| func init() { | ||
| queryCopilotCmd.Flags().StringVar(&queryCopilotProject, "project", "", "Project name (required)") | ||
| queryCopilotCmd.Flags().StringVar(&queryCopilotTimeframe, "timeframe", "30d", "Time window for metrics (e.g., 7d, 30d, 90d)") | ||
| queryCmd.AddCommand(queryCopilotCmd) | ||
| } |
README.md
Outdated
|
|
||
| This CLI makes that setup fast and repeatable from the terminal (instead of clicking through the Config UI) — especially when you want to re-run the same configuration across teams. | ||
|
|
||
| > **Blog post:** [Beyond Copilot Dashboards: Measuring What AI Actually Changes](<!-- TODO: replace with actual blog URL -->) — why DORA + Copilot correlation matters and what this tool enables. |
| "github.com/DevExpGBB/gh-devlake/internal/devlake" | ||
| "github.com/DevExpGBB/gh-devlake/internal/query" | ||
| "github.com/spf13/cobra" |
| var disc *devlake.DiscoveryResult | ||
| client, disc, err = discoverClient(cfgURL) | ||
| if err != nil { | ||
| return fmt.Errorf("discovering DevLake: %w", err) | ||
| } | ||
| _ = disc |
| fmt.Println(" " + strings.Repeat("─", 80)) | ||
| fmt.Printf(" %-6s %-15s %-10s %-20s\n", "ID", "STATUS", "TASKS", "FINISHED AT") | ||
| fmt.Println(" " + strings.Repeat("─", 80)) |
| "github.com/DevExpGBB/gh-devlake/internal/devlake" | ||
| "github.com/DevExpGBB/gh-devlake/internal/query" | ||
| "github.com/spf13/cobra" |
| RunE: runQueryDora, | ||
| } | ||
| cmd.Flags().StringVar(&queryDoraProject, "project", "", "Project name (required)") | ||
| cmd.Flags().StringVar(&queryDoraTimeframe, "timeframe", "30d", "Time window for metrics (e.g., 7d, 30d, 90d)") |
| "github.com/DevExpGBB/gh-devlake/internal/devlake" | ||
| "github.com/DevExpGBB/gh-devlake/internal/query" | ||
| "github.com/spf13/cobra" |
| Copilot metrics are currently available in Grafana dashboards at your DevLake | ||
| Grafana endpoint (shown in 'gh devlake status').`, | ||
| RunE: runQueryCopilot, | ||
| } | ||
| cmd.Flags().StringVar(&queryCopilotProject, "project", "", "Project name (required)") | ||
| cmd.Flags().StringVar(&queryCopilotTimeframe, "timeframe", "30d", "Time window for metrics (e.g., 7d, 30d, 90d)") |
| // PipelineListResponse is the response from GET /pipelines. | ||
| type PipelineListResponse struct { | ||
| Pipelines []Pipeline `json:"pipelines"` | ||
| Count int64 `json:"count"` |
- Created `query` parent command with three subcommands - Implemented `query pipelines` with full JSON/table output - Added `ListPipelines` method to devlake client - Created placeholder `query dora` and `query copilot` commands - Updated README command reference table - Added docs/query.md documentation Note: DORA and Copilot subcommands are placeholders that explain the architectural constraint (DevLake does not expose metrics APIs). The pipelines subcommand is fully functional using the existing /pipelines REST API endpoint. Co-authored-by: ewega <26189114+ewega@users.noreply.github.com>
- Move query logic from cmd handlers to internal/query package - Implement DORA query with available API data + clear limitations - Implement Copilot query with available API data + clear limitations - Keep pipelines query functional through the new abstraction - All validation passes: go build, go test, go vet Co-authored-by: ewega <26189114+ewega@users.noreply.github.com>
- Update docs/query.md to describe DORA/Copilot as partial implementations - Fix engine.go to apply defaults consistently for all optional params - Fix pipelines.go to return error on invalid limit parsing instead of silently ignoring - All validation passes: go build, go test, go vet Co-authored-by: ewega <26189114+ewega@users.noreply.github.com>
- Replace manual string concatenation with url.Values for query params - All query parameters now properly URL-encoded via q.Encode() - Remove unused strings import - Prevents injection of additional query parameters via --status flag - All validation passes: go build, go test, go vet Co-authored-by: ewega <26189114+ewega@users.noreply.github.com>
1. Fix JSON output corruption: use quiet discovery for JSON, verbose for table 2. Add --format validation: return error for invalid format values 3. Fix nil map safety: initialize params map if nil in engine.Execute 4. Fix swallowed error: propagate ListConnections errors in copilot query 5. Add tests: query_pipelines_test.go validates JSON output and format validation All validation passes: go build, go test, go vet Co-authored-by: ewega <26189114+ewega@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bbb0ffc to
63a5ffa
Compare
| var err error | ||
|
|
||
| if outputJSON || queryPipelinesFormat == "json" { | ||
| disc, err := devlake.Discover(cfgURL) | ||
| if err != nil { | ||
| return fmt.Errorf("discovering DevLake: %w", err) | ||
| } | ||
| client = devlake.NewClient(disc.URL) | ||
| } else { | ||
| var disc *devlake.DiscoveryResult | ||
| client, disc, err = discoverClient(cfgURL) | ||
| if err != nil { | ||
| return fmt.Errorf("discovering DevLake: %w", err) | ||
| } | ||
| _ = disc |
| case "int": | ||
| return strconv.Atoi(param.Default) | ||
| default: | ||
| return param.Default, nil |
| func runQueryDora(cmd *cobra.Command, args []string) error { | ||
| if queryDoraProject == "" { | ||
| return fmt.Errorf("--project flag is required") | ||
| } | ||
|
|
||
| disc, err := devlake.Discover(cfgURL) | ||
| if err != nil { | ||
| return fmt.Errorf("discovering DevLake: %w", err) | ||
| } | ||
| client := devlake.NewClient(disc.URL) | ||
|
|
||
| queryDef, err := query.Get("dora") | ||
| if err != nil { | ||
| return fmt.Errorf("getting dora query: %w", err) | ||
| } | ||
|
|
||
| params := map[string]interface{}{ | ||
| "project": queryDoraProject, | ||
| "timeframe": queryDoraTimeframe, | ||
| } | ||
|
|
||
| engine := query.NewEngine(client) | ||
| result, err := engine.Execute(queryDef, params) | ||
| if err != nil { | ||
| return fmt.Errorf("executing dora query: %w", err) | ||
| } | ||
|
|
||
| return printJSON(result) | ||
| } |
| func runQueryCopilot(cmd *cobra.Command, args []string) error { | ||
| if queryCopilotProject == "" { | ||
| return fmt.Errorf("--project flag is required") | ||
| } | ||
|
|
||
| disc, err := devlake.Discover(cfgURL) | ||
| if err != nil { | ||
| return fmt.Errorf("discovering DevLake: %w", err) | ||
| } | ||
| client := devlake.NewClient(disc.URL) | ||
|
|
||
| queryDef, err := query.Get("copilot") | ||
| if err != nil { | ||
| return fmt.Errorf("getting copilot query: %w", err) | ||
| } | ||
|
|
||
| params := map[string]interface{}{ | ||
| "project": queryCopilotProject, | ||
| "timeframe": queryCopilotTimeframe, | ||
| } | ||
|
|
||
| engine := query.NewEngine(client) | ||
| result, err := engine.Execute(queryDef, params) | ||
| if err != nil { | ||
| return fmt.Errorf("executing copilot query: %w", err) | ||
| } | ||
|
|
||
| return printJSON(result) | ||
| } |
| t.Cleanup(func() { os.Stdout = orig }) | ||
|
|
||
| // Run the command | ||
| if err := runQueryPipelines(nil, nil); err != nil { | ||
| t.Fatalf("runQueryPipelines returned error: %v", err) | ||
| } | ||
|
|
||
| w.Close() | ||
| var buf bytes.Buffer | ||
| buf.ReadFrom(r) |
No description provided.