Skip to content
Merged
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
5 changes: 4 additions & 1 deletion .github/skills/devlake-dev-architecture/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ gh devlake
│ │ ├── add # Add repo/org scopes to a connection
│ │ ├── list # List scopes on a connection
│ │ └── delete # Remove a scope from a connection
│ └── project # Create project + blueprint + trigger sync
│ └── project # Manage DevLake projects
│ ├── add # Create project + blueprint + trigger sync
│ ├── list # List all projects
│ └── delete # Delete a project
├── status # Health check + connection summary
└── cleanup # Tear down (local or Azure)
```
Expand Down
10 changes: 1 addition & 9 deletions .github/skills/devlake-dev-planning/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,7 @@ Semantic versioning: `MAJOR.MINOR.PATCH`

## Current Release Plan

| Version | Theme | Status |
|---------|-------|--------|
| v0.3.3 | Enterprise Support | Shipped — scope ID fix, connection testing, rate limit, enterprise threading |
| v0.3.4 | CLI Restructure | Shipped — singular commands, --plugin flag, list command |
| v0.3.5 | Connection Lifecycle | Shipped — delete, test, and update commands |
| v0.3.6 | Skills & Polish | Shipped — roadmap skill, skill rename/consolidation |
| v0.4.0 | Multi-Tool Expansion | Future — GitLab, Azure DevOps, per-plugin token chains |

> **Note:** Always query GitHub milestones for the latest status — this table is a snapshot.
> **Note:** Always query GitHub milestones, current and upcoming releases, and issues for the latest status — this table is a snapshot.

## CLI Command Architecture (Option A)

Expand Down
25 changes: 25 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,31 @@ internal/
- **Discovery chain**: explicit `--url` → state file → well-known ports
- **Generic API helpers**: `doPost[T]`, `doGet[T]`, `doPut[T]`, `doPatch[T]` in `internal/devlake/client.go`

### Command Tree

```
gh devlake
├── init # Interactive wizard (deploy + configure full)
├── deploy
│ ├── local # Docker Compose on this machine
│ └── azure # Azure Container Apps
├── configure
│ ├── full # connection + scope + project in one session
│ ├── connection # Manage plugin connections (CRUD)
│ │ ├── add # Create a new connection
│ │ ├── list # List all connections
│ │ ├── update # Update token/settings
│ │ ├── delete # Remove a connection
│ │ └── test # Test a saved connection
│ ├── scope # Add scopes to existing connections
│ └── project # Manage DevLake projects
│ ├── add # Create project + blueprint + trigger sync
│ ├── list # List all projects
│ └── delete # Delete a project
├── status # Health check + connection summary
└── cleanup # Tear down (local or Azure)
```

### Plugin System
Plugins are defined via `ConnectionDef` structs in `cmd/connection_types.go`. Each entry declares the plugin slug, endpoint, rate limits, prompt labels, and PAT resolution keys. To add a new DevOps tool, add a `ConnectionDef` to `connectionRegistry` — token resolution, org prompts, and connection creation all derive from these fields automatically. See the `devlake-dev-integration` skill for full details.

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ DORA patterns (deployment workflow, production environment, incident label) use
### Step 4: Create a Project and Sync

```bash
gh devlake configure project
gh devlake configure project add
```

Discovers your connections and scopes, creates a DevLake project with DORA metrics enabled, sets up a daily sync blueprint, and triggers the first data collection.
Expand Down Expand Up @@ -211,7 +211,10 @@ See [Token Handling](docs/token-handling.md) for env key names and multi-plugin
| `gh devlake configure scope add` | Add repo/org scopes to a connection | [configure-scope.md](docs/configure-scope.md) |
| `gh devlake configure scope list` | List scopes on a connection | [configure-scope.md](docs/configure-scope.md) |
| `gh devlake configure scope delete` | Remove a scope from a connection | [configure-scope.md](docs/configure-scope.md) |
| `gh devlake configure project` | Create project + blueprint + first sync | [configure-project.md](docs/configure-project.md) |
| `gh devlake configure project` | Manage DevLake projects (subcommands below) | [configure-project.md](docs/configure-project.md) |
| `gh devlake configure project add` | Create a project + blueprint + first sync | [configure-project.md](docs/configure-project.md) |
| `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 cleanup` | Tear down local or Azure resources | [cleanup.md](docs/cleanup.md) |

Expand Down
2 changes: 1 addition & 1 deletion cmd/configure_full.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Equivalent to 'gh devlake init' but skips the deploy phase.
For scripted/CI use, chain individual commands instead:
gh devlake configure connection add --plugin github --org my-org
gh devlake configure scope --plugin github --org my-org --repos owner/repo1
gh devlake configure project --project-name my-project`,
gh devlake configure project add --project-name my-project`,
RunE: runConfigureFull,
}

Expand Down
49 changes: 49 additions & 0 deletions cmd/configure_project_add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package cmd

import (
"time"

"github.com/spf13/cobra"
)

func newProjectAddCmd() *cobra.Command {
var opts ProjectOpts
cmd := &cobra.Command{
Use: "add",
Short: "Create a DevLake project and start data collection",
Long: `Creates a DevLake project that groups data from your connections.

A project ties together existing scopes (repos, orgs) from your connections
into a single view with DORA metrics. It creates a sync schedule (blueprint)
that collects data on a cron schedule (daily by default).

Prerequisites: run 'gh devlake configure scope' first to add scopes.

This command will:
1. Discover existing scopes on your connections
2. Let you choose which scopes to include
3. Create the project with DORA metrics enabled
4. Configure a sync blueprint
5. Trigger the first data collection

Example:
gh devlake configure project add
gh devlake configure project add --project-name my-team`,
RunE: func(cmd *cobra.Command, args []string) error {
return runProjectAdd(cmd, args, &opts)
},
}

cmd.Flags().StringVar(&opts.ProjectName, "project-name", "", "DevLake project name")
cmd.Flags().StringVar(&opts.TimeAfter, "time-after", "", "Only collect data after this date (default: 6 months ago)")
cmd.Flags().StringVar(&opts.Cron, "cron", "0 0 * * *", "Blueprint cron schedule")
cmd.Flags().BoolVar(&opts.SkipSync, "skip-sync", false, "Skip triggering the first data sync")
cmd.Flags().BoolVar(&opts.Wait, "wait", true, "Wait for pipeline to complete")
cmd.Flags().DurationVar(&opts.Timeout, "timeout", 5*time.Minute, "Max time to wait for pipeline")

return cmd
}

func runProjectAdd(cmd *cobra.Command, args []string, opts *ProjectOpts) error {
return runConfigureProjects(cmd, args, opts)
}
95 changes: 95 additions & 0 deletions cmd/configure_project_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package cmd

import (
"fmt"
"strings"

"github.com/spf13/cobra"

"github.com/DevExpGBB/gh-devlake/internal/prompt"
)

func newProjectDeleteCmd() *cobra.Command {
var projectDeleteName string
cmd := &cobra.Command{
Use: "delete",
Short: "Delete a DevLake project",
Long: `Deletes a DevLake project by name.

If --name is not specified, prompts interactively.

⚠️ Deleting a project also removes its blueprint and sync schedule.

Examples:
gh devlake configure project delete
gh devlake configure project delete --name my-project`,
RunE: func(cmd *cobra.Command, args []string) error {
return runProjectDelete(cmd, args, projectDeleteName)
},
}
cmd.Flags().StringVar(&projectDeleteName, "name", "", "Name of the project to delete")
return cmd
}

func runProjectDelete(cmd *cobra.Command, args []string, projectDeleteName string) error {
printBanner("DevLake — Delete Project")

// ── Discover DevLake ──
client, _, err := discoverClient(cfgURL)
if err != nil {
return err
}

// ── Resolve project name ──
name := projectDeleteName
if name == "" {
// Interactive: list projects and let the user pick one
projects, err := client.ListProjects()
if err != nil {
return fmt.Errorf("listing projects: %w", err)
}
if len(projects) == 0 {
fmt.Println("\n No projects found.")
fmt.Println()
return nil
}

labels := make([]string, len(projects))
for i, p := range projects {
labels[i] = p.Name
}

fmt.Println()
chosen := prompt.Select("Select a project to delete", labels)
if chosen == "" {
fmt.Println("\n Deletion cancelled.")
fmt.Println()
return nil
}
name = chosen
}

// ── Confirm deletion ──
fmt.Printf("\n⚠️ This will delete project %q.\n", name)
fmt.Println(" The associated blueprint and sync schedule will also be removed.")
fmt.Println()
if !prompt.Confirm("Are you sure you want to delete this project?") {
fmt.Println("\n Deletion cancelled.")
fmt.Println()
return nil
}

// ── Delete project ──
fmt.Printf("\n🗑️ Deleting project %q...\n", name)
if err := client.DeleteProject(name); err != nil {
return fmt.Errorf("failed to delete project: %w", err)
}
fmt.Println(" ✅ Project deleted")

fmt.Println("\n" + strings.Repeat("─", 50))
fmt.Printf("✅ Project %q deleted\n", name)
fmt.Println(strings.Repeat("─", 50))
fmt.Println()

return nil
}
99 changes: 99 additions & 0 deletions cmd/configure_project_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package cmd

import (
"fmt"
"strings"
"text/tabwriter"

"github.com/spf13/cobra"

"github.com/DevExpGBB/gh-devlake/internal/devlake"
)

func newProjectListCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List all DevLake projects",
Long: `Lists all DevLake projects.

Example:
gh devlake configure project list`,
RunE: runProjectList,
}
return cmd
}

// projectListItem is the JSON representation of a single project entry.
type projectListItem struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
BlueprintID int `json:"blueprintId,omitempty"`
}

func runProjectList(cmd *cobra.Command, args []string) error {
// ── Discover DevLake ──
var client *devlake.Client
if outputJSON {
disc, err := devlake.Discover(cfgURL)
if err != nil {
return err
}
client = devlake.NewClient(disc.URL)
} else {
c, _, err := discoverClient(cfgURL)
if err != nil {
return err
}
client = c
}

// ── Fetch projects ──
projects, err := client.ListProjects()
if err != nil {
return fmt.Errorf("listing projects: %w", err)
}

// ── JSON output path ──
if outputJSON {
items := make([]projectListItem, len(projects))
for i, p := range projects {
item := projectListItem{
Name: p.Name,
Description: p.Description,
}
if p.Blueprint != nil {
item.BlueprintID = p.Blueprint.ID
}
items[i] = item
}
return printJSON(items)
}

// ── Render table ──
printBanner("DevLake — List Projects")
fmt.Println()
if len(projects) == 0 {
fmt.Println(" No projects found.")
fmt.Println()
return nil
}

w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "Name\tDescription\tBlueprint ID")
fmt.Fprintln(w, strings.Repeat("─", 30)+"\t"+strings.Repeat("─", 40)+"\t"+strings.Repeat("─", 12))
for _, p := range projects {
blueprintID := ""
if p.Blueprint != nil {
blueprintID = fmt.Sprintf("%d", p.Blueprint.ID)
}
desc := p.Description
if len(desc) > 40 {
desc = desc[:37] + "..."
}
fmt.Fprintf(w, "%s\t%s\t%s\n", p.Name, desc, blueprintID)
}
w.Flush()
fmt.Println()

return nil
}
Loading