refactor: Remove YAML workflow system (bd-r6a.1)
Remove the entire YAML-based workflow system in preparation for redesigning templates as native Beads. Removed: - cmd/bd/templates/workflows/*.yaml - cmd/bd/workflow.go - internal/types/workflow.go (WorkflowTemplate types) The new design will use regular Beads with a template label, and a clone-with-substitution operation for instantiation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,385 +0,0 @@
|
||||
schema_version: 1
|
||||
|
||||
name: version-bump
|
||||
description: |
|
||||
Bumps version across all beads components and coordinates the release.
|
||||
Creates tasks for file updates, verification, git operations, CI monitoring,
|
||||
and local installation.
|
||||
|
||||
defaults:
|
||||
priority: 1
|
||||
type: task
|
||||
|
||||
variables:
|
||||
- name: version
|
||||
description: "New semantic version (e.g., 0.31.0)"
|
||||
required: true
|
||||
pattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$"
|
||||
|
||||
- name: previous_version
|
||||
description: "Current version (auto-detected from version.go)"
|
||||
required: false
|
||||
default_command: "grep 'Version = ' cmd/bd/version.go | sed 's/.*\"\\(.*\\)\".*/\\1/'"
|
||||
|
||||
preflight:
|
||||
- command: "git diff-index --quiet HEAD --"
|
||||
message: "Working directory must be clean (no uncommitted changes)"
|
||||
- command: "which jq >/dev/null 2>&1"
|
||||
message: "jq must be installed for JSON file updates"
|
||||
- command: "which gh >/dev/null 2>&1"
|
||||
message: "GitHub CLI (gh) must be installed"
|
||||
|
||||
epic:
|
||||
title: "Release v{{version}}"
|
||||
description: |
|
||||
## Version Bump Workflow
|
||||
|
||||
Coordinating release from {{previous_version}} to {{version}}.
|
||||
|
||||
### Components Updated
|
||||
- Go CLI (cmd/bd/version.go)
|
||||
- Claude Plugin (.claude-plugin/*.json)
|
||||
- MCP Server (integrations/beads-mcp/)
|
||||
- npm Package (npm-package/package.json)
|
||||
- Git hooks (cmd/bd/templates/hooks/)
|
||||
|
||||
### Release Channels
|
||||
- GitHub Releases (GoReleaser)
|
||||
- PyPI (beads-mcp)
|
||||
- npm (@beads/cli)
|
||||
- Homebrew (homebrew-beads tap)
|
||||
priority: 1
|
||||
labels:
|
||||
- release
|
||||
- v{{version}}
|
||||
|
||||
tasks:
|
||||
- id: update-version-go
|
||||
title: "Update cmd/bd/version.go to {{version}}"
|
||||
description: |
|
||||
Update the Version constant in cmd/bd/version.go:
|
||||
```go
|
||||
Version = "{{version}}"
|
||||
```
|
||||
verification:
|
||||
command: "grep -q 'Version = \"{{version}}\"' cmd/bd/version.go"
|
||||
|
||||
- id: update-changelog
|
||||
title: "Update CHANGELOG.md for {{version}}"
|
||||
description: |
|
||||
1. Change `## [Unreleased]` to `## [{{version}}] - {{today}}`
|
||||
2. Add new empty `## [Unreleased]` section at top
|
||||
3. Ensure all changes since {{previous_version}} are documented
|
||||
|
||||
- id: update-info-go
|
||||
title: "Add {{version}} to info.go release notes"
|
||||
description: |
|
||||
Update cmd/bd/info.go versionChanges map with release notes for {{version}}.
|
||||
Include any workflow-impacting changes for --whats-new output.
|
||||
depends_on:
|
||||
- update-changelog
|
||||
|
||||
- id: update-plugin-json
|
||||
title: "Update .claude-plugin/plugin.json to {{version}}"
|
||||
description: |
|
||||
Update version field in .claude-plugin/plugin.json:
|
||||
```json
|
||||
"version": "{{version}}"
|
||||
```
|
||||
verification:
|
||||
command: "jq -e '.version == \"{{version}}\"' .claude-plugin/plugin.json"
|
||||
|
||||
- id: update-marketplace-json
|
||||
title: "Update .claude-plugin/marketplace.json to {{version}}"
|
||||
description: |
|
||||
Update version field in .claude-plugin/marketplace.json:
|
||||
```json
|
||||
"version": "{{version}}"
|
||||
```
|
||||
verification:
|
||||
command: "jq -e '.plugins[0].version == \"{{version}}\"' .claude-plugin/marketplace.json"
|
||||
|
||||
- id: update-pyproject
|
||||
title: "Update integrations/beads-mcp/pyproject.toml to {{version}}"
|
||||
description: |
|
||||
Update version in pyproject.toml:
|
||||
```toml
|
||||
version = "{{version}}"
|
||||
```
|
||||
verification:
|
||||
command: "grep -q 'version = \"{{version}}\"' integrations/beads-mcp/pyproject.toml"
|
||||
|
||||
- id: update-python-init
|
||||
title: "Update beads_mcp/__init__.py to {{version}}"
|
||||
description: |
|
||||
Update __version__ in integrations/beads-mcp/src/beads_mcp/__init__.py:
|
||||
```python
|
||||
__version__ = "{{version}}"
|
||||
```
|
||||
verification:
|
||||
command: "grep -q '__version__ = \"{{version}}\"' integrations/beads-mcp/src/beads_mcp/__init__.py"
|
||||
|
||||
- id: update-npm-package
|
||||
title: "Update npm-package/package.json to {{version}}"
|
||||
description: |
|
||||
Update version field in npm-package/package.json:
|
||||
```json
|
||||
"version": "{{version}}"
|
||||
```
|
||||
verification:
|
||||
command: "jq -e '.version == \"{{version}}\"' npm-package/package.json"
|
||||
|
||||
- id: update-hook-templates
|
||||
title: "Update hook templates to {{version}}"
|
||||
description: |
|
||||
Update bd-hooks-version comment in all 4 hook templates:
|
||||
- cmd/bd/templates/hooks/pre-commit
|
||||
- cmd/bd/templates/hooks/post-merge
|
||||
- cmd/bd/templates/hooks/pre-push
|
||||
- cmd/bd/templates/hooks/post-checkout
|
||||
|
||||
Each should have:
|
||||
```bash
|
||||
# bd-hooks-version: {{version}}
|
||||
```
|
||||
verification:
|
||||
command: "grep -l 'bd-hooks-version: {{version}}' cmd/bd/templates/hooks/* | wc -l | grep -q '4'"
|
||||
|
||||
- id: verify-all-versions
|
||||
title: "Run check-versions.sh - all must pass"
|
||||
description: |
|
||||
Run the version consistency check:
|
||||
```bash
|
||||
./scripts/check-versions.sh
|
||||
```
|
||||
|
||||
All versions must match {{version}}.
|
||||
depends_on:
|
||||
- update-version-go
|
||||
- update-plugin-json
|
||||
- update-marketplace-json
|
||||
- update-pyproject
|
||||
- update-python-init
|
||||
- update-npm-package
|
||||
- update-hook-templates
|
||||
verification:
|
||||
command: "./scripts/check-versions.sh"
|
||||
|
||||
- id: git-commit-tag
|
||||
title: "Commit changes and create v{{version}} tag"
|
||||
description: |
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore: Bump version to {{version}}"
|
||||
git tag -a v{{version}} -m "Release v{{version}}"
|
||||
```
|
||||
depends_on:
|
||||
- verify-all-versions
|
||||
- update-changelog
|
||||
- update-info-go
|
||||
verification:
|
||||
command: "git describe --tags --exact-match HEAD 2>/dev/null | grep -q 'v{{version}}'"
|
||||
|
||||
- id: git-push
|
||||
title: "Push commit and tag to origin"
|
||||
description: |
|
||||
```bash
|
||||
git push origin main
|
||||
git push origin v{{version}}
|
||||
```
|
||||
|
||||
This triggers GitHub Actions:
|
||||
- GoReleaser build
|
||||
- PyPI publish
|
||||
- npm publish
|
||||
depends_on:
|
||||
- git-commit-tag
|
||||
verification:
|
||||
command: "git ls-remote origin refs/tags/v{{version}} | grep -q 'v{{version}}'"
|
||||
|
||||
- id: monitor-goreleaser
|
||||
title: "Monitor GoReleaser CI job"
|
||||
description: |
|
||||
Watch the GoReleaser action:
|
||||
https://github.com/steveyegge/beads/actions/workflows/release.yml
|
||||
|
||||
Should complete in ~10 minutes and create:
|
||||
- GitHub Release with binaries for all platforms
|
||||
- Checksums and signatures
|
||||
|
||||
Check status:
|
||||
```bash
|
||||
gh run list --workflow=release.yml -L 1
|
||||
gh run watch # to monitor live
|
||||
```
|
||||
|
||||
Verify release exists:
|
||||
```bash
|
||||
gh release view v{{version}}
|
||||
```
|
||||
priority: 2
|
||||
depends_on:
|
||||
- git-push
|
||||
verification:
|
||||
command: "gh release view v{{version}} --json tagName -q .tagName | grep -q 'v{{version}}'"
|
||||
|
||||
- id: monitor-pypi
|
||||
title: "Monitor PyPI publish"
|
||||
description: |
|
||||
Watch the PyPI publish action:
|
||||
https://github.com/steveyegge/beads/actions/workflows/pypi-publish.yml
|
||||
|
||||
Verify at: https://pypi.org/project/beads-mcp/{{version}}/
|
||||
|
||||
Check:
|
||||
```bash
|
||||
pip index versions beads-mcp 2>/dev/null | grep -q '{{version}}'
|
||||
```
|
||||
priority: 2
|
||||
depends_on:
|
||||
- git-push
|
||||
verification:
|
||||
command: "pip index versions beads-mcp 2>/dev/null | grep -q '{{version}}' || curl -s https://pypi.org/pypi/beads-mcp/json | jq -e '.releases[\"{{version}}\"]'"
|
||||
|
||||
- id: monitor-npm
|
||||
title: "Monitor npm publish"
|
||||
description: |
|
||||
Watch the npm publish action:
|
||||
https://github.com/steveyegge/beads/actions/workflows/npm-publish.yml
|
||||
|
||||
Verify at: https://www.npmjs.com/package/@anthropics/claude-code-beads-plugin/v/{{version}}
|
||||
|
||||
Check:
|
||||
```bash
|
||||
npm view @anthropics/claude-code-beads-plugin@{{version}} version
|
||||
```
|
||||
priority: 2
|
||||
depends_on:
|
||||
- git-push
|
||||
verification:
|
||||
command: "npm view @anthropics/claude-code-beads-plugin@{{version}} version 2>/dev/null | grep -q '{{version}}'"
|
||||
|
||||
- id: update-homebrew
|
||||
title: "Update Homebrew formula"
|
||||
description: |
|
||||
After GoReleaser completes, the Homebrew tap should be auto-updated.
|
||||
|
||||
If manual update needed:
|
||||
```bash
|
||||
./scripts/update-homebrew.sh v{{version}}
|
||||
```
|
||||
|
||||
Or manually update steveyegge/homebrew-beads with new SHA256.
|
||||
|
||||
Verify:
|
||||
```bash
|
||||
brew update
|
||||
brew info beads
|
||||
```
|
||||
priority: 2
|
||||
depends_on:
|
||||
- monitor-goreleaser
|
||||
|
||||
- id: local-go-install
|
||||
title: "Install {{version}} Go binary locally"
|
||||
description: |
|
||||
Rebuild and install the Go binary:
|
||||
```bash
|
||||
go install ./cmd/bd
|
||||
# OR
|
||||
make install
|
||||
```
|
||||
|
||||
Verify:
|
||||
```bash
|
||||
bd --version
|
||||
```
|
||||
depends_on:
|
||||
- monitor-goreleaser
|
||||
verification:
|
||||
command: "bd --version 2>&1 | grep -q '{{version}}'"
|
||||
|
||||
- id: restart-daemon
|
||||
title: "Restart beads daemon"
|
||||
description: |
|
||||
Kill any running daemons so they pick up the new version:
|
||||
```bash
|
||||
bd daemons killall
|
||||
```
|
||||
|
||||
Start fresh daemon:
|
||||
```bash
|
||||
bd list # triggers daemon start
|
||||
```
|
||||
|
||||
Verify daemon version:
|
||||
```bash
|
||||
bd version --daemon
|
||||
```
|
||||
depends_on:
|
||||
- local-go-install
|
||||
verification:
|
||||
command: "bd version --daemon 2>&1 | grep -q '{{version}}'"
|
||||
|
||||
- id: local-mcp-install
|
||||
title: "Install {{version}} MCP server locally"
|
||||
description: |
|
||||
Upgrade the MCP server (after PyPI publish):
|
||||
```bash
|
||||
pip install --upgrade beads-mcp
|
||||
# OR if using uv:
|
||||
uv tool upgrade beads-mcp
|
||||
```
|
||||
|
||||
Verify:
|
||||
```bash
|
||||
pip show beads-mcp | grep Version
|
||||
```
|
||||
depends_on:
|
||||
- monitor-pypi
|
||||
verification:
|
||||
command: "pip show beads-mcp 2>/dev/null | grep -q 'Version: {{version}}'"
|
||||
|
||||
- id: update-hooks
|
||||
title: "Update git hooks"
|
||||
description: |
|
||||
Install the updated hooks:
|
||||
```bash
|
||||
bd hooks install
|
||||
```
|
||||
|
||||
Verify hook version:
|
||||
```bash
|
||||
grep 'bd-hooks-version' .git/hooks/pre-commit
|
||||
```
|
||||
depends_on:
|
||||
- local-go-install
|
||||
verification:
|
||||
command: "grep -q 'bd-hooks-version: {{version}}' .git/hooks/pre-commit 2>/dev/null || echo 'Hooks may not be installed - verify manually'"
|
||||
|
||||
- id: final-verification
|
||||
title: "Final release verification"
|
||||
description: |
|
||||
Verify all release artifacts are accessible:
|
||||
|
||||
- [ ] `bd --version` shows {{version}}
|
||||
- [ ] `bd version --daemon` shows {{version}}
|
||||
- [ ] GitHub release exists: https://github.com/steveyegge/beads/releases/tag/v{{version}}
|
||||
- [ ] `brew upgrade beads && bd --version` shows {{version}} (if using Homebrew)
|
||||
- [ ] `pip show beads-mcp` shows {{version}}
|
||||
- [ ] npm package available at {{version}}
|
||||
- [ ] `bd info --whats-new` shows {{version}} notes
|
||||
|
||||
Run final checks:
|
||||
```bash
|
||||
bd --version
|
||||
bd version --daemon
|
||||
pip show beads-mcp | grep Version
|
||||
bd info --whats-new
|
||||
```
|
||||
depends_on:
|
||||
- restart-daemon
|
||||
- local-mcp-install
|
||||
- update-hooks
|
||||
- monitor-npm
|
||||
- update-homebrew
|
||||
@@ -1,925 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed templates/workflows/*.yaml
|
||||
var builtinWorkflows embed.FS
|
||||
|
||||
var workflowCmd = &cobra.Command{
|
||||
Use: "workflow",
|
||||
Short: "Manage workflow templates",
|
||||
Long: `Manage workflow templates for multi-step processes.
|
||||
|
||||
Workflows are YAML templates that define an epic with dependent child tasks.
|
||||
When instantiated, they create a structured set of issues with proper
|
||||
dependencies, enabling agents to work through complex processes step by step.
|
||||
|
||||
Templates can be built-in (version-bump) or custom templates
|
||||
stored in .beads/workflows/ directory.`,
|
||||
}
|
||||
|
||||
var workflowListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List available workflow templates",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
workflows, err := loadAllWorkflows()
|
||||
if err != nil {
|
||||
FatalError("loading workflows: %v", err)
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
outputJSON(workflows)
|
||||
return
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
blue := color.New(color.FgBlue).SprintFunc()
|
||||
dim := color.New(color.Faint).SprintFunc()
|
||||
|
||||
// Group by source
|
||||
builtins := []*types.WorkflowTemplate{}
|
||||
customs := []*types.WorkflowTemplate{}
|
||||
|
||||
for _, wf := range workflows {
|
||||
if isBuiltinWorkflow(wf.Name) {
|
||||
builtins = append(builtins, wf)
|
||||
} else {
|
||||
customs = append(customs, wf)
|
||||
}
|
||||
}
|
||||
|
||||
if len(builtins) > 0 {
|
||||
fmt.Printf("%s\n", green("Built-in Workflows:"))
|
||||
for _, wf := range builtins {
|
||||
fmt.Printf(" %s\n", blue(wf.Name))
|
||||
if wf.Description != "" {
|
||||
// Show first line of description
|
||||
desc := strings.Split(wf.Description, "\n")[0]
|
||||
fmt.Printf(" %s\n", dim(desc))
|
||||
}
|
||||
fmt.Printf(" Tasks: %d\n", len(wf.Tasks))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
if len(customs) > 0 {
|
||||
fmt.Printf("%s\n", green("Custom Workflows (.beads/workflows/):"))
|
||||
for _, wf := range customs {
|
||||
fmt.Printf(" %s\n", blue(wf.Name))
|
||||
if wf.Description != "" {
|
||||
desc := strings.Split(wf.Description, "\n")[0]
|
||||
fmt.Printf(" %s\n", dim(desc))
|
||||
}
|
||||
fmt.Printf(" Tasks: %d\n", len(wf.Tasks))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
if len(workflows) == 0 {
|
||||
fmt.Println("No workflow templates available")
|
||||
fmt.Println("Create one in .beads/workflows/ or use built-in templates")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var workflowShowCmd = &cobra.Command{
|
||||
Use: "show <template-name>",
|
||||
Short: "Show workflow template details",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
templateName := args[0]
|
||||
wf, err := loadWorkflow(templateName)
|
||||
if err != nil {
|
||||
FatalError("%v", err)
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
outputJSON(wf)
|
||||
return
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
blue := color.New(color.FgBlue).SprintFunc()
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
dim := color.New(color.Faint).SprintFunc()
|
||||
|
||||
fmt.Printf("%s %s\n", green("Workflow:"), blue(wf.Name))
|
||||
if wf.Description != "" {
|
||||
fmt.Printf("\n%s\n", wf.Description)
|
||||
}
|
||||
|
||||
if len(wf.Variables) > 0 {
|
||||
fmt.Printf("\n%s\n", green("Variables:"))
|
||||
for _, v := range wf.Variables {
|
||||
req := ""
|
||||
if v.Required {
|
||||
req = yellow(" (required)")
|
||||
}
|
||||
fmt.Printf(" %s%s: %s\n", blue(v.Name), req, v.Description)
|
||||
if v.Pattern != "" {
|
||||
fmt.Printf(" Pattern: %s\n", dim(v.Pattern))
|
||||
}
|
||||
if v.DefaultValue != "" {
|
||||
fmt.Printf(" Default: %s\n", dim(v.DefaultValue))
|
||||
}
|
||||
if v.DefaultCommand != "" {
|
||||
fmt.Printf(" Default command: %s\n", dim(v.DefaultCommand))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(wf.Preflight) > 0 {
|
||||
fmt.Printf("\n%s\n", green("Preflight Checks:"))
|
||||
for _, check := range wf.Preflight {
|
||||
fmt.Printf(" - %s\n", check.Message)
|
||||
fmt.Printf(" %s\n", dim(check.Command))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s\n", green("Epic:"))
|
||||
fmt.Printf(" Title: %s\n", wf.Epic.Title)
|
||||
if len(wf.Epic.Labels) > 0 {
|
||||
fmt.Printf(" Labels: %s\n", strings.Join(wf.Epic.Labels, ", "))
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s (%d total)\n", green("Tasks:"), len(wf.Tasks))
|
||||
for i, task := range wf.Tasks {
|
||||
deps := ""
|
||||
if len(task.DependsOn) > 0 {
|
||||
deps = dim(fmt.Sprintf(" (depends on: %s)", strings.Join(task.DependsOn, ", ")))
|
||||
}
|
||||
verify := ""
|
||||
if task.Verification != nil {
|
||||
verify = yellow(" [verified]")
|
||||
}
|
||||
fmt.Printf(" %d. %s%s%s\n", i+1, task.Title, deps, verify)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var workflowCreateCmd = &cobra.Command{
|
||||
Use: "create <template-name> [--var key=value...]",
|
||||
Short: "Create workflow instance from template",
|
||||
Long: `Create a workflow instance from a template.
|
||||
|
||||
This creates an epic with child tasks based on the template definition.
|
||||
Variables can be provided with --var flags, e.g.:
|
||||
bd workflow create version-bump --var version=0.31.0
|
||||
|
||||
The workflow creates hierarchical task IDs under the epic:
|
||||
bd-xyz123 (epic)
|
||||
bd-xyz123.1 (first task)
|
||||
bd-xyz123.2 (second task)
|
||||
...
|
||||
|
||||
Tasks are created with dependencies as defined in the template.
|
||||
Use 'bd ready' to see which tasks are ready to work on.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckReadonly("workflow create")
|
||||
|
||||
templateName := args[0]
|
||||
wf, err := loadWorkflow(templateName)
|
||||
if err != nil {
|
||||
FatalError("%v", err)
|
||||
}
|
||||
|
||||
// Parse variable flags
|
||||
varFlags, _ := cmd.Flags().GetStringSlice("var")
|
||||
vars := make(map[string]string)
|
||||
for _, v := range varFlags {
|
||||
parts := strings.SplitN(v, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
FatalError("invalid variable format: %s (expected key=value)", v)
|
||||
}
|
||||
vars[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
// Add built-in variables
|
||||
vars["today"] = time.Now().Format("2006-01-02")
|
||||
vars["user"] = actor
|
||||
if vars["user"] == "" {
|
||||
vars["user"] = os.Getenv("USER")
|
||||
}
|
||||
|
||||
// Process variables: apply defaults and validate required
|
||||
for _, v := range wf.Variables {
|
||||
if _, ok := vars[v.Name]; !ok {
|
||||
// Variable not provided
|
||||
if v.DefaultCommand != "" {
|
||||
// Run command to get default
|
||||
out, err := exec.Command("sh", "-c", v.DefaultCommand).Output()
|
||||
if err == nil {
|
||||
vars[v.Name] = strings.TrimSpace(string(out))
|
||||
}
|
||||
} else if v.DefaultValue != "" {
|
||||
vars[v.Name] = v.DefaultValue
|
||||
} else if v.Required {
|
||||
FatalError("required variable not provided: %s\n Description: %s", v.Name, v.Description)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate pattern if specified
|
||||
if v.Pattern != "" && vars[v.Name] != "" {
|
||||
matched, err := regexp.MatchString(v.Pattern, vars[v.Name])
|
||||
if err != nil {
|
||||
FatalError("invalid pattern for variable %s: %v", v.Name, err)
|
||||
}
|
||||
if !matched {
|
||||
FatalError("variable %s value '%s' does not match pattern: %s", v.Name, vars[v.Name], v.Pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check dry-run flag
|
||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||
if dryRun {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("%s Creating workflow from template: %s\n\n", yellow("[DRY RUN]"), templateName)
|
||||
fmt.Printf("%s\n", green("Variables:"))
|
||||
for k, v := range vars {
|
||||
fmt.Printf(" %s = %s\n", k, v)
|
||||
}
|
||||
fmt.Printf("\n%s %s\n", green("Epic:"), substituteVars(wf.Epic.Title, vars))
|
||||
fmt.Printf("\n%s\n", green("Tasks:"))
|
||||
for i, task := range wf.Tasks {
|
||||
fmt.Printf(" .%d %s\n", i+1, substituteVars(task.Title, vars))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Run preflight checks (unless skipped)
|
||||
skipPreflight, _ := cmd.Flags().GetBool("skip-preflight")
|
||||
if len(wf.Preflight) > 0 && !skipPreflight {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Println("Running preflight checks...")
|
||||
for _, check := range wf.Preflight {
|
||||
checkCmd := substituteVars(check.Command, vars)
|
||||
err := exec.Command("sh", "-c", checkCmd).Run()
|
||||
if err != nil {
|
||||
FatalError("preflight check failed: %s\n Command: %s", check.Message, checkCmd)
|
||||
}
|
||||
fmt.Printf(" %s %s\n", green("✓"), check.Message)
|
||||
}
|
||||
fmt.Println()
|
||||
} else if skipPreflight && len(wf.Preflight) > 0 {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("%s Skipping preflight checks\n\n", yellow("⚠"))
|
||||
}
|
||||
|
||||
// Create the workflow instance
|
||||
instance, err := createWorkflowInstance(wf, vars)
|
||||
if err != nil {
|
||||
FatalError("creating workflow: %v", err)
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
outputJSON(instance)
|
||||
return
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
blue := color.New(color.FgBlue).SprintFunc()
|
||||
dim := color.New(color.Faint).SprintFunc()
|
||||
|
||||
fmt.Printf("%s Created workflow from template: %s\n\n", green("✓"), templateName)
|
||||
fmt.Printf("Epic: %s %s\n", blue(instance.EpicID), substituteVars(wf.Epic.Title, vars))
|
||||
fmt.Printf("\nTasks:\n")
|
||||
|
||||
// Show created tasks
|
||||
for i, task := range wf.Tasks {
|
||||
taskID := instance.TaskMap[task.ID]
|
||||
status := "ready"
|
||||
if len(task.DependsOn) > 0 {
|
||||
blockedBy := []string{}
|
||||
for _, dep := range task.DependsOn {
|
||||
blockedBy = append(blockedBy, instance.TaskMap[dep])
|
||||
}
|
||||
status = fmt.Sprintf("blocked by: %s", strings.Join(blockedBy, ", "))
|
||||
}
|
||||
fmt.Printf(" %s %s %s\n", blue(taskID), substituteVars(task.Title, vars), dim("["+status+"]"))
|
||||
_ = i
|
||||
}
|
||||
|
||||
fmt.Printf("\nNext: %s\n", blue("bd update "+instance.TaskMap[wf.Tasks[0].ID]+" --status in_progress"))
|
||||
},
|
||||
}
|
||||
|
||||
var workflowStatusCmd = &cobra.Command{
|
||||
Use: "status <epic-id>",
|
||||
Short: "Show workflow instance progress",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
epicID := args[0]
|
||||
|
||||
// Resolve partial ID
|
||||
if daemonClient != nil {
|
||||
resolveArgs := &rpc.ResolveIDArgs{ID: epicID}
|
||||
resp, err := daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
FatalError("resolving ID: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(resp.Data, &epicID); err != nil {
|
||||
FatalError("parsing response: %v", err)
|
||||
}
|
||||
} else {
|
||||
ctx := rootCtx
|
||||
resolved, err := resolvePartialID(ctx, store, epicID)
|
||||
if err != nil {
|
||||
FatalError("resolving ID: %v", err)
|
||||
}
|
||||
epicID = resolved
|
||||
}
|
||||
|
||||
// Get epic and children
|
||||
var epic *types.Issue
|
||||
var children []*types.Issue
|
||||
|
||||
if daemonClient != nil {
|
||||
// Get epic via Show
|
||||
showArgs := &rpc.ShowArgs{ID: epicID}
|
||||
resp, err := daemonClient.Show(showArgs)
|
||||
if err != nil {
|
||||
FatalError("getting epic: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(resp.Data, &epic); err != nil {
|
||||
FatalError("parsing epic: %v", err)
|
||||
}
|
||||
|
||||
// Get children by listing with query for ID prefix
|
||||
listArgs := &rpc.ListArgs{
|
||||
Query: epicID + ".",
|
||||
}
|
||||
resp, err = daemonClient.List(listArgs)
|
||||
if err != nil {
|
||||
FatalError("getting children: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(resp.Data, &children); err != nil {
|
||||
FatalError("parsing children: %v", err)
|
||||
}
|
||||
} else {
|
||||
ctx := rootCtx
|
||||
var err error
|
||||
epic, err = store.GetIssue(ctx, epicID)
|
||||
if err != nil {
|
||||
FatalError("getting epic: %v", err)
|
||||
}
|
||||
if epic == nil {
|
||||
FatalError("epic not found: %s", epicID)
|
||||
}
|
||||
// Get children by searching for ID prefix
|
||||
children, err = store.SearchIssues(ctx, epicID+".", types.IssueFilter{})
|
||||
if err != nil {
|
||||
FatalError("getting children: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
result := map[string]interface{}{
|
||||
"epic": epic,
|
||||
"children": children,
|
||||
}
|
||||
outputJSON(result)
|
||||
return
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
blue := color.New(color.FgBlue).SprintFunc()
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
dim := color.New(color.Faint).SprintFunc()
|
||||
|
||||
// Count progress
|
||||
completed := 0
|
||||
inProgress := 0
|
||||
blocked := 0
|
||||
for _, child := range children {
|
||||
switch child.Status {
|
||||
case types.StatusClosed:
|
||||
completed++
|
||||
case types.StatusInProgress:
|
||||
inProgress++
|
||||
case types.StatusBlocked:
|
||||
blocked++
|
||||
}
|
||||
}
|
||||
|
||||
total := len(children)
|
||||
pct := 0
|
||||
if total > 0 {
|
||||
pct = completed * 100 / total
|
||||
}
|
||||
|
||||
fmt.Printf("Workflow: %s %s\n", blue(epicID), epic.Title)
|
||||
fmt.Printf("Progress: %d/%d tasks complete (%d%%)\n", completed, total, pct)
|
||||
if inProgress > 0 {
|
||||
fmt.Printf(" %s in progress\n", yellow(fmt.Sprintf("%d", inProgress)))
|
||||
}
|
||||
if blocked > 0 {
|
||||
fmt.Printf(" %s blocked\n", red(fmt.Sprintf("%d", blocked)))
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println("Tasks:")
|
||||
for _, child := range children {
|
||||
var icon string
|
||||
var statusStr string
|
||||
switch child.Status {
|
||||
case types.StatusClosed:
|
||||
icon = green("✓")
|
||||
statusStr = "closed"
|
||||
case types.StatusInProgress:
|
||||
icon = yellow("○")
|
||||
statusStr = "in_progress"
|
||||
case types.StatusBlocked:
|
||||
icon = red("✗")
|
||||
statusStr = "blocked"
|
||||
default:
|
||||
icon = dim("◌")
|
||||
statusStr = string(child.Status)
|
||||
}
|
||||
fmt.Printf(" %s %s %s %s\n", icon, blue(child.ID), child.Title, dim("["+statusStr+"]"))
|
||||
}
|
||||
|
||||
// Show current task if any in progress
|
||||
for _, child := range children {
|
||||
if child.Status == types.StatusInProgress {
|
||||
fmt.Printf("\nCurrent task: %s \"%s\"\n", blue(child.ID), child.Title)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var workflowVerifyCmd = &cobra.Command{
|
||||
Use: "verify <task-id>",
|
||||
Short: "Run verification command for a workflow task",
|
||||
Long: `Run the verification command defined for a workflow task.
|
||||
|
||||
This command looks up the task, finds its verification configuration,
|
||||
and runs the verification command. Results are displayed but the task
|
||||
status is not automatically changed - use 'bd close' to mark complete.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
taskID := args[0]
|
||||
|
||||
// Resolve partial ID
|
||||
if daemonClient != nil {
|
||||
resolveArgs := &rpc.ResolveIDArgs{ID: taskID}
|
||||
resp, err := daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
FatalError("resolving ID: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(resp.Data, &taskID); err != nil {
|
||||
FatalError("parsing response: %v", err)
|
||||
}
|
||||
} else {
|
||||
ctx := rootCtx
|
||||
resolved, err := resolvePartialID(ctx, store, taskID)
|
||||
if err != nil {
|
||||
FatalError("resolving ID: %v", err)
|
||||
}
|
||||
taskID = resolved
|
||||
}
|
||||
|
||||
// Get task
|
||||
var task *types.Issue
|
||||
if daemonClient != nil {
|
||||
showArgs := &rpc.ShowArgs{ID: taskID}
|
||||
resp, err := daemonClient.Show(showArgs)
|
||||
if err != nil {
|
||||
FatalError("getting task: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(resp.Data, &task); err != nil {
|
||||
FatalError("parsing task: %v", err)
|
||||
}
|
||||
} else {
|
||||
ctx := rootCtx
|
||||
var err error
|
||||
task, err = store.GetIssue(ctx, taskID)
|
||||
if err != nil {
|
||||
FatalError("getting task: %v", err)
|
||||
}
|
||||
if task == nil {
|
||||
FatalError("task not found: %s", taskID)
|
||||
}
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
blue := color.New(color.FgBlue).SprintFunc()
|
||||
dim := color.New(color.Faint).SprintFunc()
|
||||
|
||||
// Look for verification in task description
|
||||
// Format: ```verify\ncommand\n```
|
||||
verifyCmd := extractVerifyCommand(task.Description)
|
||||
if verifyCmd == "" {
|
||||
fmt.Printf("No verification command found for task: %s\n", blue(taskID))
|
||||
fmt.Printf("Add a verification block to the task description:\n")
|
||||
fmt.Printf(" %s\n", dim("```verify"))
|
||||
fmt.Printf(" %s\n", dim("./scripts/check-versions.sh"))
|
||||
fmt.Printf(" %s\n", dim("```"))
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Running verification for: %s \"%s\"\n", blue(taskID), task.Title)
|
||||
fmt.Printf("Command: %s\n\n", dim(verifyCmd))
|
||||
|
||||
// Run the command
|
||||
execCmd := exec.Command("sh", "-c", verifyCmd)
|
||||
execCmd.Stdout = os.Stdout
|
||||
execCmd.Stderr = os.Stderr
|
||||
err := execCmd.Run()
|
||||
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
fmt.Printf("%s Verification failed (exit code: %d)\n", red("✗"), exitErr.ExitCode())
|
||||
} else {
|
||||
fmt.Printf("%s Verification failed: %v\n", red("✗"), err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Verification passed\n", green("✓"))
|
||||
fmt.Printf("\nTo close this task: %s\n", blue("bd close "+taskID))
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
workflowCreateCmd.Flags().StringSlice("var", []string{}, "Variable values (key=value, repeatable)")
|
||||
workflowCreateCmd.Flags().Bool("dry-run", false, "Show what would be created without creating")
|
||||
workflowCreateCmd.Flags().Bool("skip-preflight", false, "Skip preflight checks (use with caution)")
|
||||
|
||||
workflowCmd.AddCommand(workflowListCmd)
|
||||
workflowCmd.AddCommand(workflowShowCmd)
|
||||
workflowCmd.AddCommand(workflowCreateCmd)
|
||||
workflowCmd.AddCommand(workflowStatusCmd)
|
||||
workflowCmd.AddCommand(workflowVerifyCmd)
|
||||
rootCmd.AddCommand(workflowCmd)
|
||||
}
|
||||
|
||||
// loadAllWorkflows loads both built-in and custom workflows
|
||||
func loadAllWorkflows() ([]*types.WorkflowTemplate, error) {
|
||||
workflows := []*types.WorkflowTemplate{}
|
||||
|
||||
// Load built-in workflows
|
||||
entries, err := builtinWorkflows.ReadDir("templates/workflows")
|
||||
if err == nil {
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSuffix(entry.Name(), ".yaml")
|
||||
wf, err := loadBuiltinWorkflow(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
workflows = append(workflows, wf)
|
||||
}
|
||||
}
|
||||
|
||||
// Load custom workflows from .beads/workflows/
|
||||
workflowsDir := filepath.Join(".beads", "workflows")
|
||||
if _, err := os.Stat(workflowsDir); err == nil {
|
||||
entries, err := os.ReadDir(workflowsDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading workflows directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSuffix(entry.Name(), ".yaml")
|
||||
wf, err := loadCustomWorkflow(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
workflows = append(workflows, wf)
|
||||
}
|
||||
}
|
||||
|
||||
return workflows, nil
|
||||
}
|
||||
|
||||
// loadWorkflow loads a workflow by name (checks custom first, then built-in)
|
||||
func loadWorkflow(name string) (*types.WorkflowTemplate, error) {
|
||||
if err := sanitizeTemplateName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Try custom workflows first
|
||||
wf, err := loadCustomWorkflow(name)
|
||||
if err == nil {
|
||||
return wf, nil
|
||||
}
|
||||
|
||||
// Fall back to built-in workflows
|
||||
return loadBuiltinWorkflow(name)
|
||||
}
|
||||
|
||||
// loadBuiltinWorkflow loads a built-in workflow template
|
||||
func loadBuiltinWorkflow(name string) (*types.WorkflowTemplate, error) {
|
||||
path := fmt.Sprintf("templates/workflows/%s.yaml", name)
|
||||
data, err := builtinWorkflows.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("workflow '%s' not found", name)
|
||||
}
|
||||
|
||||
var wf types.WorkflowTemplate
|
||||
if err := yaml.Unmarshal(data, &wf); err != nil {
|
||||
return nil, fmt.Errorf("parsing workflow: %w", err)
|
||||
}
|
||||
|
||||
return &wf, nil
|
||||
}
|
||||
|
||||
// loadCustomWorkflow loads a custom workflow from .beads/workflows/
|
||||
func loadCustomWorkflow(name string) (*types.WorkflowTemplate, error) {
|
||||
path := filepath.Join(".beads", "workflows", name+".yaml")
|
||||
// #nosec G304 - path is sanitized via sanitizeTemplateName before calling this function
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("workflow '%s' not found", name)
|
||||
}
|
||||
|
||||
var wf types.WorkflowTemplate
|
||||
if err := yaml.Unmarshal(data, &wf); err != nil {
|
||||
return nil, fmt.Errorf("parsing workflow: %w", err)
|
||||
}
|
||||
|
||||
return &wf, nil
|
||||
}
|
||||
|
||||
// isBuiltinWorkflow checks if a workflow name is a built-in workflow
|
||||
func isBuiltinWorkflow(name string) bool {
|
||||
_, err := builtinWorkflows.ReadFile(fmt.Sprintf("templates/workflows/%s.yaml", name))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// substituteVars replaces {{var}} placeholders with values
|
||||
func substituteVars(s string, vars map[string]string) string {
|
||||
result := s
|
||||
for k, v := range vars {
|
||||
result = strings.ReplaceAll(result, "{{"+k+"}}", v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// createWorkflowInstance creates an epic and child tasks from a workflow template
|
||||
func createWorkflowInstance(wf *types.WorkflowTemplate, vars map[string]string) (*types.WorkflowInstance, error) {
|
||||
ctx := rootCtx
|
||||
|
||||
// Substitute variables in epic
|
||||
epicTitle := substituteVars(wf.Epic.Title, vars)
|
||||
epicDesc := substituteVars(wf.Epic.Description, vars)
|
||||
epicLabels := make([]string, len(wf.Epic.Labels))
|
||||
for i, label := range wf.Epic.Labels {
|
||||
epicLabels[i] = substituteVars(label, vars)
|
||||
}
|
||||
// Always add workflow label
|
||||
epicLabels = append(epicLabels, "workflow")
|
||||
|
||||
instance := &types.WorkflowInstance{
|
||||
TemplateName: wf.Name,
|
||||
Variables: vars,
|
||||
TaskMap: make(map[string]string),
|
||||
}
|
||||
|
||||
if daemonClient != nil {
|
||||
// Create epic via daemon
|
||||
createArgs := &rpc.CreateArgs{
|
||||
Title: epicTitle,
|
||||
Description: epicDesc,
|
||||
IssueType: "epic",
|
||||
Priority: wf.Epic.Priority,
|
||||
Labels: epicLabels,
|
||||
}
|
||||
resp, err := daemonClient.Create(createArgs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating epic: %w", err)
|
||||
}
|
||||
var epic types.Issue
|
||||
if err := json.Unmarshal(resp.Data, &epic); err != nil {
|
||||
return nil, fmt.Errorf("parsing epic response: %w", err)
|
||||
}
|
||||
instance.EpicID = epic.ID
|
||||
|
||||
// Create child tasks
|
||||
for _, task := range wf.Tasks {
|
||||
taskTitle := substituteVars(task.Title, vars)
|
||||
taskDesc := substituteVars(task.Description, vars)
|
||||
taskType := task.Type
|
||||
if taskType == "" {
|
||||
taskType = wf.Defaults.Type
|
||||
if taskType == "" {
|
||||
taskType = "task"
|
||||
}
|
||||
}
|
||||
taskPriority := task.Priority
|
||||
if taskPriority == 0 {
|
||||
taskPriority = wf.Defaults.Priority
|
||||
if taskPriority == 0 {
|
||||
taskPriority = 2
|
||||
}
|
||||
}
|
||||
|
||||
// Add verification block to description if present
|
||||
if task.Verification != nil && task.Verification.Command != "" {
|
||||
verifyCmd := substituteVars(task.Verification.Command, vars)
|
||||
taskDesc += "\n\n```verify\n" + verifyCmd + "\n```"
|
||||
}
|
||||
|
||||
taskLabels := []string{"workflow"}
|
||||
|
||||
createArgs := &rpc.CreateArgs{
|
||||
Title: taskTitle,
|
||||
Description: taskDesc,
|
||||
IssueType: taskType,
|
||||
Priority: taskPriority,
|
||||
Parent: instance.EpicID,
|
||||
Labels: taskLabels,
|
||||
}
|
||||
resp, err := daemonClient.Create(createArgs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating task %s: %w", task.ID, err)
|
||||
}
|
||||
var created types.Issue
|
||||
if err := json.Unmarshal(resp.Data, &created); err != nil {
|
||||
return nil, fmt.Errorf("parsing task response: %w", err)
|
||||
}
|
||||
instance.TaskMap[task.ID] = created.ID
|
||||
}
|
||||
|
||||
// Add dependencies between tasks
|
||||
for _, task := range wf.Tasks {
|
||||
if len(task.DependsOn) == 0 {
|
||||
continue
|
||||
}
|
||||
taskID := instance.TaskMap[task.ID]
|
||||
for _, depName := range task.DependsOn {
|
||||
depID := instance.TaskMap[depName]
|
||||
if depID == "" {
|
||||
continue
|
||||
}
|
||||
depArgs := &rpc.DepAddArgs{
|
||||
FromID: taskID,
|
||||
ToID: depID,
|
||||
DepType: "blocks",
|
||||
}
|
||||
_, err := daemonClient.AddDependency(depArgs)
|
||||
if err != nil {
|
||||
// Non-fatal, just warn
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to add dependency %s -> %s: %v\n", taskID, depID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Direct mode
|
||||
// Create epic
|
||||
epic := &types.Issue{
|
||||
Title: epicTitle,
|
||||
Description: epicDesc,
|
||||
Status: types.StatusOpen,
|
||||
Priority: wf.Epic.Priority,
|
||||
IssueType: types.TypeEpic,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, epic, actor); err != nil {
|
||||
return nil, fmt.Errorf("creating epic: %w", err)
|
||||
}
|
||||
instance.EpicID = epic.ID
|
||||
|
||||
// Add epic labels
|
||||
for _, label := range epicLabels {
|
||||
if err := store.AddLabel(ctx, epic.ID, label, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to add label %s: %v\n", label, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create child tasks
|
||||
for _, task := range wf.Tasks {
|
||||
taskTitle := substituteVars(task.Title, vars)
|
||||
taskDesc := substituteVars(task.Description, vars)
|
||||
taskType := task.Type
|
||||
if taskType == "" {
|
||||
taskType = wf.Defaults.Type
|
||||
if taskType == "" {
|
||||
taskType = "task"
|
||||
}
|
||||
}
|
||||
taskPriority := task.Priority
|
||||
if taskPriority == 0 {
|
||||
taskPriority = wf.Defaults.Priority
|
||||
if taskPriority == 0 {
|
||||
taskPriority = 2
|
||||
}
|
||||
}
|
||||
|
||||
// Add verification block to description if present
|
||||
if task.Verification != nil && task.Verification.Command != "" {
|
||||
verifyCmd := substituteVars(task.Verification.Command, vars)
|
||||
taskDesc += "\n\n```verify\n" + verifyCmd + "\n```"
|
||||
}
|
||||
|
||||
// Get next child ID
|
||||
childID, err := store.GetNextChildID(ctx, instance.EpicID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting child ID: %w", err)
|
||||
}
|
||||
|
||||
child := &types.Issue{
|
||||
ID: childID,
|
||||
Title: taskTitle,
|
||||
Description: taskDesc,
|
||||
Status: types.StatusOpen,
|
||||
Priority: taskPriority,
|
||||
IssueType: types.IssueType(taskType),
|
||||
}
|
||||
if err := store.CreateIssue(ctx, child, actor); err != nil {
|
||||
return nil, fmt.Errorf("creating task %s: %w", task.ID, err)
|
||||
}
|
||||
instance.TaskMap[task.ID] = child.ID
|
||||
|
||||
// Add parent-child dependency
|
||||
dep := &types.Dependency{
|
||||
IssueID: child.ID,
|
||||
DependsOnID: instance.EpicID,
|
||||
Type: types.DepParentChild,
|
||||
}
|
||||
if err := store.AddDependency(ctx, dep, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to add parent-child dependency: %v\n", err)
|
||||
}
|
||||
|
||||
// Add workflow label
|
||||
if err := store.AddLabel(ctx, child.ID, "workflow", actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to add workflow label: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add task dependencies
|
||||
for _, task := range wf.Tasks {
|
||||
if len(task.DependsOn) == 0 {
|
||||
continue
|
||||
}
|
||||
taskID := instance.TaskMap[task.ID]
|
||||
for _, depName := range task.DependsOn {
|
||||
depID := instance.TaskMap[depName]
|
||||
if depID == "" {
|
||||
continue
|
||||
}
|
||||
dep := &types.Dependency{
|
||||
IssueID: taskID,
|
||||
DependsOnID: depID,
|
||||
Type: types.DepBlocks,
|
||||
}
|
||||
if err := store.AddDependency(ctx, dep, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to add dependency %s -> %s: %v\n", taskID, depID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
markDirtyAndScheduleFlush()
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// extractVerifyCommand extracts a verify command from task description
|
||||
// Looks for ```verify\ncommand\n``` block
|
||||
func extractVerifyCommand(description string) string {
|
||||
start := strings.Index(description, "```verify\n")
|
||||
if start == -1 {
|
||||
return ""
|
||||
}
|
||||
start += len("```verify\n")
|
||||
end := strings.Index(description[start:], "\n```")
|
||||
if end == -1 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(description[start : start+end])
|
||||
}
|
||||
|
||||
// resolvePartialID resolves a partial ID to a full ID (for direct mode)
|
||||
func resolvePartialID(ctx interface{}, s interface{}, id string) (string, error) {
|
||||
// This is a simplified version - the real implementation is in utils package
|
||||
// For now, just return the ID as-is if it looks complete
|
||||
return id, nil
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package types
|
||||
|
||||
// WorkflowTemplate represents a workflow definition loaded from YAML
|
||||
type WorkflowTemplate struct {
|
||||
SchemaVersion int `yaml:"schema_version" json:"schema_version"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Defaults WorkflowDefaults `yaml:"defaults" json:"defaults"`
|
||||
Variables []WorkflowVariable `yaml:"variables" json:"variables"`
|
||||
Preflight []PreflightCheck `yaml:"preflight" json:"preflight"`
|
||||
Epic WorkflowEpic `yaml:"epic" json:"epic"`
|
||||
Tasks []WorkflowTask `yaml:"tasks" json:"tasks"`
|
||||
}
|
||||
|
||||
// WorkflowDefaults contains default values for task fields
|
||||
type WorkflowDefaults struct {
|
||||
Priority int `yaml:"priority" json:"priority"`
|
||||
Type string `yaml:"type" json:"type"`
|
||||
}
|
||||
|
||||
// WorkflowVariable defines a variable that can be substituted in the template
|
||||
type WorkflowVariable struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Required bool `yaml:"required" json:"required"`
|
||||
Pattern string `yaml:"pattern" json:"pattern"` // Optional regex validation
|
||||
DefaultValue string `yaml:"default" json:"default"` // Static default
|
||||
DefaultCommand string `yaml:"default_command" json:"default_command"` // Command to run for default
|
||||
}
|
||||
|
||||
// PreflightCheck is a check that must pass before workflow creation
|
||||
type PreflightCheck struct {
|
||||
Command string `yaml:"command" json:"command"`
|
||||
Message string `yaml:"message" json:"message"`
|
||||
}
|
||||
|
||||
// WorkflowEpic defines the parent epic for the workflow
|
||||
type WorkflowEpic struct {
|
||||
Title string `yaml:"title" json:"title"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Priority int `yaml:"priority" json:"priority"`
|
||||
Labels []string `yaml:"labels" json:"labels"`
|
||||
}
|
||||
|
||||
// WorkflowTask defines a single task in the workflow
|
||||
type WorkflowTask struct {
|
||||
ID string `yaml:"id" json:"id"`
|
||||
Title string `yaml:"title" json:"title"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Type string `yaml:"type" json:"type"`
|
||||
Priority int `yaml:"priority" json:"priority"`
|
||||
Estimate int `yaml:"estimate" json:"estimate"` // Minutes
|
||||
DependsOn []string `yaml:"depends_on" json:"depends_on"`
|
||||
Verification *Verification `yaml:"verification" json:"verification"`
|
||||
}
|
||||
|
||||
// Verification defines how to verify a task was completed successfully
|
||||
type Verification struct {
|
||||
Command string `yaml:"command" json:"command"`
|
||||
ExpectExit *int `yaml:"expect_exit" json:"expect_exit"`
|
||||
ExpectStdout string `yaml:"expect_stdout" json:"expect_stdout"`
|
||||
}
|
||||
|
||||
// WorkflowInstance represents a created workflow (epic + tasks)
|
||||
type WorkflowInstance struct {
|
||||
EpicID string `json:"epic_id"`
|
||||
TemplateName string `json:"template_name"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
TaskMap map[string]string `json:"task_map"` // template task ID -> actual issue ID
|
||||
}
|
||||
Reference in New Issue
Block a user