-
Notifications
You must be signed in to change notification settings - Fork 4
Addressing PR comments #141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Claude
wants to merge
6
commits into
main
Choose a base branch
from
claude/add-gh-devlake-query-command
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
f6090ad
Initial plan
Claude dcfa565
Add gh devlake query command with pipelines subcommand
Claude 811b498
Refactor query commands to use internal/query abstraction
Claude 1620812
Address PR feedback: fix docs, engine defaults, and limit parsing
Claude 5a69bdc
Fix URL injection vulnerability in ListPipelines
Claude 5c6898c
Address GitHub review feedback on query commands
Claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
|
|
||
| // 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 | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New
query pipelinesbehavior (JSON-by-default output,--formatswitching, 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 jsonis used, and (2) invalid--formatvalues return an error.