Skip to content
Open
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ See [Token Handling](docs/token-handling.md) for env key names and multi-plugin
| `gh devlake configure project list` | List all projects | [configure-project.md](docs/configure-project.md) |
| `gh devlake configure project delete` | Delete a project | [configure-project.md](docs/configure-project.md) |
| `gh devlake configure full` | Connections + scopes + project in one step | [configure-full.md](docs/configure-full.md) |
| `gh devlake query pipelines` | Query recent pipeline runs | [query.md](docs/query.md) |
| `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) |
| `gh devlake start` | Start stopped or exited DevLake services | [start.md](docs/start.md) |
| `gh devlake stop` | Stop running services (preserves containers and data) | [stop.md](docs/stop.md) |
| `gh devlake cleanup` | Tear down local or Azure resources | [cleanup.md](docs/cleanup.md) |
Expand Down
25 changes: 25 additions & 0 deletions cmd/query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package cmd

import (
"github.com/spf13/cobra"
)

var queryCmd = &cobra.Command{
Use: "query",
Short: "Query DevLake data and metrics",
Long: `Query DevLake's aggregated data and metrics.

Retrieve DORA metrics, Copilot usage data, pipeline status, and other
metrics in a structured format (JSON by default, --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)
}
74 changes: 74 additions & 0 deletions cmd/query_copilot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package cmd

import (
"fmt"

"github.com/DevExpGBB/gh-devlake/internal/devlake"
"github.com/DevExpGBB/gh-devlake/internal/query"
"github.com/spf13/cobra"
)

var (
queryCopilotProject string
queryCopilotTimeframe string
)

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)
}

func runQueryCopilot(cmd *cobra.Command, args []string) error {
// Validate project flag
if queryCopilotProject == "" {
return fmt.Errorf("--project flag is required")
}

// Discover DevLake instance
disc, err := devlake.Discover(cfgURL)
if err != nil {
return fmt.Errorf("discovering DevLake: %w", err)
}
client := devlake.NewClient(disc.URL)

// Get the query definition
queryDef, err := query.Get("copilot")
if err != nil {
return fmt.Errorf("getting copilot query: %w", err)
}

// Build parameters
params := map[string]interface{}{
"project": queryCopilotProject,
"timeframe": queryCopilotTimeframe,
}

// Execute the query
engine := query.NewEngine(client)
result, err := engine.Execute(queryDef, params)
if err != nil {
return fmt.Errorf("executing copilot query: %w", err)
}

// Output result as JSON
return printJSON(result)
}
73 changes: 73 additions & 0 deletions cmd/query_dora.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package cmd

import (
"fmt"

"github.com/DevExpGBB/gh-devlake/internal/devlake"
"github.com/DevExpGBB/gh-devlake/internal/query"
"github.com/spf13/cobra"
)

var (
queryDoraProject string
queryDoraTimeframe string
)

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)
}

func runQueryDora(cmd *cobra.Command, args []string) error {
// Validate project flag
if queryDoraProject == "" {
return fmt.Errorf("--project flag is required")
}

// Discover DevLake instance
disc, err := devlake.Discover(cfgURL)
if err != nil {
return fmt.Errorf("discovering DevLake: %w", err)
}
client := devlake.NewClient(disc.URL)

// Get the query definition
queryDef, err := query.Get("dora")
if err != nil {
return fmt.Errorf("getting dora query: %w", err)
}

// Build parameters
params := map[string]interface{}{
"project": queryDoraProject,
"timeframe": queryDoraTimeframe,
}

// Execute the query
engine := query.NewEngine(client)
result, err := engine.Execute(queryDef, params)
if err != nil {
return fmt.Errorf("executing dora query: %w", err)
}

// Output result as JSON
return printJSON(result)
}
129 changes: 129 additions & 0 deletions cmd/query_pipelines.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package cmd

import (
"fmt"
"strings"

"github.com/DevExpGBB/gh-devlake/internal/devlake"
"github.com/DevExpGBB/gh-devlake/internal/query"
"github.com/spf13/cobra"
)

var (
queryPipelinesProject string
queryPipelinesStatus string
queryPipelinesLimit int
queryPipelinesFormat string
)

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)
}

func runQueryPipelines(cmd *cobra.Command, args []string) error {
// Validate format flag
if queryPipelinesFormat != "json" && queryPipelinesFormat != "table" {
return fmt.Errorf("invalid --format value %q: must be 'json' or 'table'", queryPipelinesFormat)
}

// Discover DevLake instance
var client *devlake.Client
var err error

// Use quiet discovery for JSON output, verbose for table
if outputJSON || queryPipelinesFormat == "json" {
// Quiet discovery for JSON output
disc, err := devlake.Discover(cfgURL)
if err != nil {
return fmt.Errorf("discovering DevLake: %w", err)
}
client = devlake.NewClient(disc.URL)
} else {
// Verbose discovery for table output
var disc *devlake.DiscoveryResult
client, disc, err = discoverClient(cfgURL)
if err != nil {
return fmt.Errorf("discovering DevLake: %w", err)
}
_ = disc // disc is used by discoverClient for output
}
Comment on lines +43 to +69
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

New query pipelines behavior (JSON-by-default output, --format switching, and filtering flags) is not covered by tests. Given the repo already tests JSON output behavior (e.g., in cmd/json_output_test.go), consider adding tests to assert: (1) JSON output is valid/pure (no discovery banner on stdout) when --format json is used, and (2) invalid --format values return an error.

Copilot uses AI. Check for mistakes.

// Get the query definition
queryDef, err := query.Get("pipelines")
if err != nil {
return fmt.Errorf("getting pipelines query: %w", err)
}

// Build parameters
params := map[string]interface{}{
"limit": queryPipelinesLimit,
}
if queryPipelinesProject != "" {
params["project"] = queryPipelinesProject
}
if queryPipelinesStatus != "" {
params["status"] = queryPipelinesStatus
}

// Execute the query
engine := query.NewEngine(client)
result, err := engine.Execute(queryDef, params)
if err != nil {
return fmt.Errorf("executing pipelines query: %w", err)
}

// Cast result to slice of PipelineResult
pipelines, ok := result.([]query.PipelineResult)
if !ok {
return fmt.Errorf("unexpected result type: %T", result)
}

// Output
if outputJSON || queryPipelinesFormat == "json" {
return printJSON(pipelines)
}

// Table format
printBanner("DevLake — Pipeline Query")
if len(pipelines) == 0 {
fmt.Println("\n No pipelines found.")
return nil
}

fmt.Printf("\n Found %d pipeline(s)\n", len(pipelines))
fmt.Println(" " + strings.Repeat("─", 80))
fmt.Printf(" %-6s %-15s %-10s %-20s\n", "ID", "STATUS", "TASKS", "FINISHED AT")
fmt.Println(" " + strings.Repeat("─", 80))
for _, p := range pipelines {
status := p.Status
tasks := fmt.Sprintf("%d/%d", p.FinishedTasks, p.TotalTasks)
finished := p.FinishedAt
if finished == "" {
finished = "(running)"
}
fmt.Printf(" %-6d %-15s %-10s %-20s\n", p.ID, status, tasks, finished)
}
fmt.Println()

return nil
}
Loading
Loading