Skip to content
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ This CLI makes that setup fast and repeatable from the terminal (instead of clic

> **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.

> [!NOTE]
> **GitHub Copilot plugin:** The Copilot metrics plugin is currently available in the [DevExpGBB/incubator-devlake](https://github.com/DevExpGBB/incubator-devlake) fork while the upstream PR ([apache/incubator-devlake#8728](https://github.com/apache/incubator-devlake/pull/8728)) is under review. The `deploy local` and `deploy azure` commands handle custom images automatically — no manual image builds needed. Once the PR merges, the official Apache images will include it.

<!-- SCREENSHOT:Grafana DORA dashboard + Copilot Adoption dashboard side-by-side — the "payoff" -->

---
Expand Down Expand Up @@ -221,6 +224,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 metadata now; full metrics remain API/DB limited | [query.md](docs/query.md) |
| `gh devlake query copilot` | Query Copilot metadata now; full metrics remain API/DB limited | [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
29 changes: 29 additions & 0 deletions cmd/query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cmd

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

func newQueryCmd() *cobra.Command {
cmd := &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`,
}
cmd.GroupID = "operate"
cmd.AddCommand(newQueryPipelinesCmd(), newQueryDoraCmd(), newQueryCopilotCmd())
return cmd
}

func init() {
rootCmd.AddCommand(newQueryCmd())
}
67 changes: 67 additions & 0 deletions cmd/query_copilot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package cmd

import (
"fmt"

"github.com/DevExpGBB/gh-devlake/internal/devlake"
"github.com/DevExpGBB/gh-devlake/internal/query"
"github.com/spf13/cobra"
Comment on lines +6 to +8
)

var (
queryCopilotProject string
queryCopilotTimeframe string
)

func newQueryCopilotCmd() *cobra.Command {
cmd := &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,
}
cmd.Flags().StringVar(&queryCopilotProject, "project", "", "Project name (required)")
cmd.Flags().StringVar(&queryCopilotTimeframe, "timeframe", "30d", "Time window for metrics (e.g., 7d, 30d, 90d)")
Comment on lines +30 to +35
return cmd
}

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)
}
Comment on lines +39 to +67
66 changes: 66 additions & 0 deletions cmd/query_dora.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package cmd

import (
"fmt"

"github.com/DevExpGBB/gh-devlake/internal/devlake"
"github.com/DevExpGBB/gh-devlake/internal/query"
"github.com/spf13/cobra"
Comment on lines +6 to +8
)

var (
queryDoraProject string
queryDoraTimeframe string
)

func newQueryDoraCmd() *cobra.Command {
cmd := &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,
}
cmd.Flags().StringVar(&queryDoraProject, "project", "", "Project name (required)")
cmd.Flags().StringVar(&queryDoraTimeframe, "timeframe", "30d", "Time window for metrics (e.g., 7d, 30d, 90d)")
return cmd
}

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)
}
Comment on lines +38 to +66
120 changes: 120 additions & 0 deletions cmd/query_pipelines.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package cmd

import (
"fmt"
"strings"

"github.com/DevExpGBB/gh-devlake/internal/devlake"
"github.com/DevExpGBB/gh-devlake/internal/query"
"github.com/spf13/cobra"
Comment on lines +7 to +9
)

var (
queryPipelinesProject string
queryPipelinesStatus string
queryPipelinesLimit int
queryPipelinesFormat string
)

func newQueryPipelinesCmd() *cobra.Command {
cmd := &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,
}
cmd.Flags().StringVar(&queryPipelinesProject, "project", "", "Filter by project name")
cmd.Flags().StringVar(&queryPipelinesStatus, "status", "", "Filter by status (TASK_CREATED, TASK_RUNNING, TASK_COMPLETED, TASK_FAILED)")
cmd.Flags().IntVar(&queryPipelinesLimit, "limit", 20, "Maximum number of pipelines to return")
cmd.Flags().StringVar(&queryPipelinesFormat, "format", "json", "Output format (json or table)")
return cmd
}

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

var client *devlake.Client
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
Comment on lines +60 to +65
Comment on lines +51 to +65
}

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

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

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

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

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

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))
Comment on lines +105 to +107
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