feat: Add markdown file support to bd create command
Implement `bd create -f file.md` to parse markdown files and create multiple issues in one command. This enables drafting features in markdown and converting them to tracked issues. Features: - Parse markdown H2 headers (##) as issue titles - Support all issue fields via H3 sections (### Priority, ### Type, etc.) - Handle multiple issues per file - Comprehensive validation and error handling - Full test coverage with 5 test cases Closes bd-91 (GH-9) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,16 @@
|
|||||||
{"id":"bd-168","title":"Another test with multiple deps","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-14T03:24:32.746757-07:00","updated_at":"2025-10-14T03:24:46.261275-07:00","closed_at":"2025-10-14T03:24:46.261275-07:00","dependencies":[{"issue_id":"bd-168","depends_on_id":"bd-89","type":"blocks","created_at":"2025-10-14T03:24:32.747029-07:00","created_by":"stevey"},{"issue_id":"bd-168","depends_on_id":"bd-90","type":"blocks","created_at":"2025-10-14T03:24:32.747181-07:00","created_by":"stevey"}]}
|
{"id":"bd-168","title":"Another test with multiple deps","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-14T03:24:32.746757-07:00","updated_at":"2025-10-14T03:24:46.261275-07:00","closed_at":"2025-10-14T03:24:46.261275-07:00","dependencies":[{"issue_id":"bd-168","depends_on_id":"bd-89","type":"blocks","created_at":"2025-10-14T03:24:32.747029-07:00","created_by":"stevey"},{"issue_id":"bd-168","depends_on_id":"bd-90","type":"blocks","created_at":"2025-10-14T03:24:32.747181-07:00","created_by":"stevey"}]}
|
||||||
{"id":"bd-169","title":"Fix: bd init --prefix test -q flag not recognized","description":"The init command doesn't recognize the -q flag. When running 'bd init --prefix test -q', it fails silently or behaves unexpectedly. The flag should either be implemented for quiet mode or removed from documentation if not supported.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-10-14T12:33:51.614293-07:00","updated_at":"2025-10-14T12:33:51.614293-07:00"}
|
{"id":"bd-169","title":"Fix: bd init --prefix test -q flag not recognized","description":"The init command doesn't recognize the -q flag. When running 'bd init --prefix test -q', it fails silently or behaves unexpectedly. The flag should either be implemented for quiet mode or removed from documentation if not supported.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-10-14T12:33:51.614293-07:00","updated_at":"2025-10-14T12:33:51.614293-07:00"}
|
||||||
{"id":"bd-17","title":"Update documentation for collision resolution","description":"Update README.md with collision resolution section. Update CLAUDE.md with new workflow. Document --resolve-collisions and --dry-run flags. Add example scenarios showing branch merge workflows.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.866649-07:00","updated_at":"2025-10-14T03:04:05.959647-07:00","closed_at":"2025-10-12T17:06:14.930928-07:00","dependencies":[{"issue_id":"bd-17","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.970302-07:00","created_by":"stevey"}]}
|
{"id":"bd-17","title":"Update documentation for collision resolution","description":"Update README.md with collision resolution section. Update CLAUDE.md with new workflow. Document --resolve-collisions and --dry-run flags. Add example scenarios showing branch merge workflows.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.866649-07:00","updated_at":"2025-10-14T03:04:05.959647-07:00","closed_at":"2025-10-12T17:06:14.930928-07:00","dependencies":[{"issue_id":"bd-17","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.970302-07:00","created_by":"stevey"}]}
|
||||||
|
{"id":"bd-170","title":"Add OAuth2 support","description":"Implement OAuth2 authentication flow with support for Google and GitHub providers.","design":"- Create OAuth2 provider interface\n- Implement Google provider\n- Implement GitHub provider\n- Add callback handler\n- Store tokens securely","acceptance_criteria":"- Users can authenticate with Google\n- Users can authenticate with GitHub\n- Tokens are stored securely in database\n- Token refresh works automatically","status":"closed","priority":1,"issue_type":"feature","assignee":"alice","created_at":"2025-10-14T12:40:30.990247-07:00","updated_at":"2025-10-14T12:40:50.292308-07:00","closed_at":"2025-10-14T12:40:50.292308-07:00"}
|
||||||
|
{"id":"bd-171","title":"Add rate limiting to auth endpoints","description":"Auth endpoints are vulnerable to brute force attacks. Need to add rate limiting.","status":"closed","priority":0,"issue_type":"bug","assignee":"bob","created_at":"2025-10-14T12:40:30.996332-07:00","updated_at":"2025-10-14T12:40:50.293099-07:00","closed_at":"2025-10-14T12:40:50.293099-07:00"}
|
||||||
|
{"id":"bd-172","title":"Improve session management","description":"Current session management is basic. Need to improve with better expiration handling.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-14T12:40:30.997104-07:00","updated_at":"2025-10-14T12:40:50.29329-07:00","closed_at":"2025-10-14T12:40:50.29329-07:00"}
|
||||||
|
{"id":"bd-173","title":"Refactor parseMarkdownFile to reduce cyclomatic complexity","description":"The parseMarkdownFile function in cmd/bd/markdown.go has a cyclomatic complexity of 38, which exceeds the recommended threshold of 30. This makes the function harder to understand, test, and maintain.","design":"Split the function into smaller, focused units:\n\n1. parseMarkdownFile(filepath) - Main entry point, handles file I/O\n2. parseMarkdownContent(scanner) - Core parsing logic\n3. processIssueSection(issue, section, content) - Handle section finalization (current switch statement)\n4. parseLabels(content) []string - Extract labels from content\n5. parseDependencies(content) []string - Extract dependencies from content\n6. parsePriority(content) int - Parse and validate priority\n\nBenefits:\n- Each function has a single responsibility\n- Easier to test individual components\n- Lower cognitive load when reading code\n- Better encapsulation of parsing logic","acceptance_criteria":"- parseMarkdownFile complexity \u003c 15\n- New helper functions each have complexity \u003c 10\n- All existing tests still pass\n- No change in functionality or behavior\n- Code coverage maintained or improved","status":"closed","priority":3,"issue_type":"task","created_at":"2025-10-14T12:51:21.241236-07:00","updated_at":"2025-10-14T12:55:35.140001-07:00","closed_at":"2025-10-14T12:55:35.140001-07:00","dependencies":[{"issue_id":"bd-173","depends_on_id":"bd-91","type":"discovered-from","created_at":"2025-10-14T12:51:21.24297-07:00","created_by":"stevey"}]}
|
||||||
|
{"id":"bd-174","title":"Add OAuth2 support","description":"Implement OAuth2 authentication flow with support for Google and GitHub providers.","design":"- Create OAuth2 provider interface\n- Implement Google provider\n- Implement GitHub provider\n- Add callback handler\n- Store tokens securely","acceptance_criteria":"- Users can authenticate with Google\n- Users can authenticate with GitHub\n- Tokens are stored securely in database\n- Token refresh works automatically","status":"closed","priority":1,"issue_type":"feature","assignee":"alice","created_at":"2025-10-14T12:55:09.226351-07:00","updated_at":"2025-10-14T12:55:17.818093-07:00","closed_at":"2025-10-14T12:55:17.818093-07:00"}
|
||||||
|
{"id":"bd-175","title":"Add rate limiting to auth endpoints","description":"Auth endpoints are vulnerable to brute force attacks. Need to add rate limiting.","status":"closed","priority":0,"issue_type":"bug","assignee":"bob","created_at":"2025-10-14T12:55:09.228394-07:00","updated_at":"2025-10-14T12:55:17.819352-07:00","closed_at":"2025-10-14T12:55:17.819352-07:00"}
|
||||||
|
{"id":"bd-176","title":"Improve session management","description":"Current session management is basic. Need to improve with better expiration handling.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-14T12:55:09.228919-07:00","updated_at":"2025-10-14T12:55:17.819557-07:00","closed_at":"2025-10-14T12:55:17.819557-07:00"}
|
||||||
|
{"id":"bd-177","title":"Add OAuth2 support","description":"Implement OAuth2 authentication flow with support for Google and GitHub providers.","design":"- Create OAuth2 provider interface\n- Implement Google provider\n- Implement GitHub provider\n- Add callback handler\n- Store tokens securely","acceptance_criteria":"- Users can authenticate with Google\n- Users can authenticate with GitHub\n- Tokens are stored securely in database\n- Token refresh works automatically","status":"closed","priority":1,"issue_type":"feature","assignee":"alice","created_at":"2025-10-14T13:01:35.935497-07:00","updated_at":"2025-10-14T13:01:35.950067-07:00","closed_at":"2025-10-14T13:01:35.950067-07:00"}
|
||||||
|
{"id":"bd-178","title":"Add rate limiting to auth endpoints","description":"Auth endpoints are vulnerable to brute force attacks. Need to add rate limiting.","status":"closed","priority":0,"issue_type":"bug","assignee":"bob","created_at":"2025-10-14T13:01:35.937662-07:00","updated_at":"2025-10-14T13:01:35.950387-07:00","closed_at":"2025-10-14T13:01:35.950387-07:00"}
|
||||||
|
{"id":"bd-179","title":"Improve session management","description":"Current session management is basic. Need to improve with better expiration handling.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-14T13:01:35.93812-07:00","updated_at":"2025-10-14T13:01:35.950517-07:00","closed_at":"2025-10-14T13:01:35.950517-07:00"}
|
||||||
{"id":"bd-18","title":"Add design/notes/acceptance_criteria fields to update command","description":"Currently bd update only supports status, priority, title, assignee. Add support for --design, --notes, --acceptance-criteria flags. This makes it easier to add detailed designs to issues after creation.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-10-12T14:40:57.032395-07:00","updated_at":"2025-10-14T03:04:05.959826-07:00","closed_at":"2025-10-12T17:10:53.958318-07:00"}
|
{"id":"bd-18","title":"Add design/notes/acceptance_criteria fields to update command","description":"Currently bd update only supports status, priority, title, assignee. Add support for --design, --notes, --acceptance-criteria flags. This makes it easier to add detailed designs to issues after creation.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-10-12T14:40:57.032395-07:00","updated_at":"2025-10-14T03:04:05.959826-07:00","closed_at":"2025-10-12T17:10:53.958318-07:00"}
|
||||||
{"id":"bd-19","title":"Fix import zero-value field handling","description":"Import uses zero-value checks (Priority != 0) to determine field updates. This prevents setting priority to 0 or clearing string fields. Export/import round-trip not fully idempotent for zero values. Consider JSON presence detection or explicit preserve-existing semantics. Location: cmd/bd/import.go:95-106","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-10-12T15:13:17.895083-07:00","updated_at":"2025-10-14T03:04:05.959987-07:00"}
|
{"id":"bd-19","title":"Fix import zero-value field handling","description":"Import uses zero-value checks (Priority != 0) to determine field updates. This prevents setting priority to 0 or clearing string fields. Export/import round-trip not fully idempotent for zero values. Consider JSON presence detection or explicit preserve-existing semantics. Location: cmd/bd/import.go:95-106","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-10-12T15:13:17.895083-07:00","updated_at":"2025-10-14T03:04:05.959987-07:00"}
|
||||||
{"id":"bd-2","title":"Add PostgreSQL backend","description":"Implement PostgreSQL storage backend as alternative to SQLite for larger teams","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-10-12T00:43:03.457453-07:00","updated_at":"2025-10-14T03:04:05.960143-07:00","closed_at":"2025-10-12T14:15:04.00695-07:00"}
|
{"id":"bd-2","title":"Add PostgreSQL backend","description":"Implement PostgreSQL storage backend as alternative to SQLite for larger teams","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-10-12T00:43:03.457453-07:00","updated_at":"2025-10-14T03:04:05.960143-07:00","closed_at":"2025-10-12T14:15:04.00695-07:00"}
|
||||||
@@ -158,7 +168,7 @@
|
|||||||
{"id":"bd-89","title":"GH-6: Fix race condition in parallel issue creation","description":"Creating multiple issues rapidly in parallel causes 'UNIQUE constraint failed: issues.id' error. The ID generation has a race condition. Reproducible with: for i in {26..35}; do ./bd create parallel_ 2\u003e\u00261 \u0026 done","status":"open","priority":0,"issue_type":"bug","created_at":"2025-10-14T02:44:55.510776-07:00","updated_at":"2025-10-14T03:04:05.97313-07:00","closed_at":"2025-10-14T02:58:22.645874-07:00","external_ref":"gh-6"}
|
{"id":"bd-89","title":"GH-6: Fix race condition in parallel issue creation","description":"Creating multiple issues rapidly in parallel causes 'UNIQUE constraint failed: issues.id' error. The ID generation has a race condition. Reproducible with: for i in {26..35}; do ./bd create parallel_ 2\u003e\u00261 \u0026 done","status":"open","priority":0,"issue_type":"bug","created_at":"2025-10-14T02:44:55.510776-07:00","updated_at":"2025-10-14T03:04:05.97313-07:00","closed_at":"2025-10-14T02:58:22.645874-07:00","external_ref":"gh-6"}
|
||||||
{"id":"bd-9","title":"Build collision resolution tooling for distributed branch workflows","description":"When branches diverge and both create issues, auto-incrementing IDs collide on merge. Build excellent tooling to detect collisions during import, auto-renumber issues with fewer dependencies, update all references in descriptions and dependency links, and provide clear user feedback. Goal: keep beautiful brevity of numeric IDs (bd-302) while handling distributed creation gracefully.","status":"in_progress","priority":1,"issue_type":"feature","created_at":"2025-10-12T13:39:34.608218-07:00","updated_at":"2025-10-14T03:04:05.973251-07:00"}
|
{"id":"bd-9","title":"Build collision resolution tooling for distributed branch workflows","description":"When branches diverge and both create issues, auto-incrementing IDs collide on merge. Build excellent tooling to detect collisions during import, auto-renumber issues with fewer dependencies, update all references in descriptions and dependency links, and provide clear user feedback. Goal: keep beautiful brevity of numeric IDs (bd-302) while handling distributed creation gracefully.","status":"in_progress","priority":1,"issue_type":"feature","created_at":"2025-10-12T13:39:34.608218-07:00","updated_at":"2025-10-14T03:04:05.973251-07:00"}
|
||||||
{"id":"bd-90","title":"GH-7: Package available in AUR (beads-git)","description":"Community member created AUR package for Arch Linux: https://aur.archlinux.org/packages/beads-git. This is informational - no action needed, but good to track for release process and documentation.","status":"open","priority":4,"issue_type":"chore","created_at":"2025-10-14T02:44:56.4535-07:00","updated_at":"2025-10-14T03:04:05.973364-07:00","external_ref":"gh-7"}
|
{"id":"bd-90","title":"GH-7: Package available in AUR (beads-git)","description":"Community member created AUR package for Arch Linux: https://aur.archlinux.org/packages/beads-git. This is informational - no action needed, but good to track for release process and documentation.","status":"open","priority":4,"issue_type":"chore","created_at":"2025-10-14T02:44:56.4535-07:00","updated_at":"2025-10-14T03:04:05.973364-07:00","external_ref":"gh-7"}
|
||||||
{"id":"bd-91","title":"GH-9: Support markdown files in bd create","description":"Request to support markdown files as input to bd create, which would parse the markdown and split it into multiple issues. Use case: developers keep feature drafts in markdown files in version control, then want to convert them into issues. Example: bd create -f feature-draft.md","status":"open","priority":2,"issue_type":"feature","created_at":"2025-10-14T02:44:57.405586-07:00","updated_at":"2025-10-14T03:04:05.973505-07:00","external_ref":"gh-9"}
|
{"id":"bd-91","title":"GH-9: Support markdown files in bd create","description":"Request to support markdown files as input to bd create, which would parse the markdown and split it into multiple issues. Use case: developers keep feature drafts in markdown files in version control, then want to convert them into issues. Example: bd create -f feature-draft.md","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-10-14T02:44:57.405586-07:00","updated_at":"2025-10-14T12:42:14.457949-07:00","closed_at":"2025-10-14T12:42:14.457949-07:00","external_ref":"gh-9"}
|
||||||
{"id":"bd-92","title":"GH-11: Add Docker support for hosted/shared instance","description":"Request for Docker container hosting to use beads across multiple dev machines. Would need to consider: centralized database (PostgreSQL?), authentication, concurrent access, API server, etc. This is a significant architectural change from the current local-first model.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-10-14T02:44:58.469094-07:00","updated_at":"2025-10-14T03:04:05.973622-07:00","external_ref":"gh-11"}
|
{"id":"bd-92","title":"GH-11: Add Docker support for hosted/shared instance","description":"Request for Docker container hosting to use beads across multiple dev machines. Would need to consider: centralized database (PostgreSQL?), authentication, concurrent access, API server, etc. This is a significant architectural change from the current local-first model.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-10-14T02:44:58.469094-07:00","updated_at":"2025-10-14T03:04:05.973622-07:00","external_ref":"gh-11"}
|
||||||
{"id":"bd-93","title":"GH-18: Add --deps flag to bd create for one-command issue creation","description":"Request to add dependency specification to bd create command instead of requiring separate 'bd dep add' command. Proposed syntax: bd create 'Fix bug' --deps discovered-from=bd-20. This would be especially useful for aider integration and reducing command verbosity.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-10-14T02:44:59.610192-07:00","updated_at":"2025-10-14T03:26:59.536349-07:00","closed_at":"2025-10-14T03:26:59.536349-07:00","external_ref":"gh-18"}
|
{"id":"bd-93","title":"GH-18: Add --deps flag to bd create for one-command issue creation","description":"Request to add dependency specification to bd create command instead of requiring separate 'bd dep add' command. Proposed syntax: bd create 'Fix bug' --deps discovered-from=bd-20. This would be especially useful for aider integration and reducing command verbosity.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-10-14T02:44:59.610192-07:00","updated_at":"2025-10-14T03:26:59.536349-07:00","closed_at":"2025-10-14T03:26:59.536349-07:00","external_ref":"gh-18"}
|
||||||
{"id":"bd-94","title":"parallel_test_1","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-14T02:55:46.913771-07:00","updated_at":"2025-10-14T02:55:46.913771-07:00"}
|
{"id":"bd-94","title":"parallel_test_1","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-14T02:55:46.913771-07:00","updated_at":"2025-10-14T02:55:46.913771-07:00"}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ bd create "Issue title" -t bug|feature|task -p 0-4 -d "Description" --json
|
|||||||
# Create with explicit ID (for parallel workers)
|
# Create with explicit ID (for parallel workers)
|
||||||
bd create "Issue title" --id worker1-100 -p 1 --json
|
bd create "Issue title" --id worker1-100 -p 1 --json
|
||||||
|
|
||||||
|
# Create multiple issues from markdown file
|
||||||
|
bd create -f feature-plan.md --json
|
||||||
|
|
||||||
# Update issue status
|
# Update issue status
|
||||||
bd update <id> --status in_progress --json
|
bd update <id> --status in_progress --json
|
||||||
|
|
||||||
|
|||||||
88
README.md
88
README.md
@@ -207,9 +207,13 @@ bd create "Worker task" --id worker1-100 -p 1
|
|||||||
|
|
||||||
# Get JSON output for programmatic use
|
# Get JSON output for programmatic use
|
||||||
bd create "Fix bug" -d "Description" --json
|
bd create "Fix bug" -d "Description" --json
|
||||||
|
|
||||||
|
# Create multiple issues from a markdown file
|
||||||
|
bd create -f feature-plan.md
|
||||||
```
|
```
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
- `-f, --file` - Create multiple issues from markdown file
|
||||||
- `-d, --description` - Issue description
|
- `-d, --description` - Issue description
|
||||||
- `-p, --priority` - Priority (0-4, 0=highest)
|
- `-p, --priority` - Priority (0-4, 0=highest)
|
||||||
- `-t, --type` - Type (bug|feature|task|epic|chore)
|
- `-t, --type` - Type (bug|feature|task|epic|chore)
|
||||||
@@ -218,6 +222,90 @@ Options:
|
|||||||
- `--id` - Explicit issue ID (e.g., `worker1-100` for ID space partitioning)
|
- `--id` - Explicit issue ID (e.g., `worker1-100` for ID space partitioning)
|
||||||
- `--json` - Output in JSON format
|
- `--json` - Output in JSON format
|
||||||
|
|
||||||
|
#### Creating Issues from Markdown
|
||||||
|
|
||||||
|
You can draft multiple issues in a markdown file and create them all at once. This is useful for planning features or converting written notes into tracked work.
|
||||||
|
|
||||||
|
Markdown format:
|
||||||
|
```markdown
|
||||||
|
## Issue Title
|
||||||
|
|
||||||
|
Optional description text here.
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
1
|
||||||
|
|
||||||
|
### Type
|
||||||
|
feature
|
||||||
|
|
||||||
|
### Description
|
||||||
|
More detailed description (overrides text after title).
|
||||||
|
|
||||||
|
### Design
|
||||||
|
Design notes and implementation details.
|
||||||
|
|
||||||
|
### Acceptance Criteria
|
||||||
|
- Must do this
|
||||||
|
- Must do that
|
||||||
|
|
||||||
|
### Assignee
|
||||||
|
username
|
||||||
|
|
||||||
|
### Labels
|
||||||
|
label1, label2, label3
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
bd-10, bd-20
|
||||||
|
```
|
||||||
|
|
||||||
|
Example markdown file (`auth-improvements.md`):
|
||||||
|
```markdown
|
||||||
|
## Add OAuth2 support
|
||||||
|
|
||||||
|
We need to support OAuth2 authentication.
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
1
|
||||||
|
|
||||||
|
### Type
|
||||||
|
feature
|
||||||
|
|
||||||
|
### Assignee
|
||||||
|
alice
|
||||||
|
|
||||||
|
### Labels
|
||||||
|
auth, high-priority
|
||||||
|
|
||||||
|
## Add rate limiting
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
0
|
||||||
|
|
||||||
|
### Type
|
||||||
|
bug
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Auth endpoints are vulnerable to brute force attacks.
|
||||||
|
|
||||||
|
### Labels
|
||||||
|
security, urgent
|
||||||
|
```
|
||||||
|
|
||||||
|
Create all issues:
|
||||||
|
```bash
|
||||||
|
bd create -f auth-improvements.md
|
||||||
|
# ✓ Created 2 issues from auth-improvements.md:
|
||||||
|
# bd-42: Add OAuth2 support [P1, feature]
|
||||||
|
# bd-43: Add rate limiting [P0, bug]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- Each `## Heading` creates a new issue
|
||||||
|
- Sections (`### Priority`, `### Type`, etc.) are optional
|
||||||
|
- Defaults: Priority=2, Type=task
|
||||||
|
- Text immediately after the title becomes the description (unless overridden by `### Description`)
|
||||||
|
- All standard issue fields are supported
|
||||||
|
|
||||||
### Viewing Issues
|
### Viewing Issues
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
134
cmd/bd/main.go
134
cmd/bd/main.go
@@ -527,11 +527,140 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().BoolVar(&noAutoImport, "no-auto-import", false, "Disable automatic JSONL import when newer than DB")
|
rootCmd.PersistentFlags().BoolVar(&noAutoImport, "no-auto-import", false, "Disable automatic JSONL import when newer than DB")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createIssuesFromMarkdown parses a markdown file and creates multiple issues
|
||||||
|
func createIssuesFromMarkdown(cmd *cobra.Command, filepath string) {
|
||||||
|
// Parse markdown file
|
||||||
|
templates, err := parseMarkdownFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error parsing markdown file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(templates) == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "No issues found in markdown file\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
createdIssues := []*types.Issue{}
|
||||||
|
failedIssues := []string{}
|
||||||
|
|
||||||
|
// Create each issue
|
||||||
|
for _, template := range templates {
|
||||||
|
issue := &types.Issue{
|
||||||
|
Title: template.Title,
|
||||||
|
Description: template.Description,
|
||||||
|
Design: template.Design,
|
||||||
|
AcceptanceCriteria: template.AcceptanceCriteria,
|
||||||
|
Status: types.StatusOpen,
|
||||||
|
Priority: template.Priority,
|
||||||
|
IssueType: template.IssueType,
|
||||||
|
Assignee: template.Assignee,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.CreateIssue(ctx, issue, actor); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error creating issue '%s': %v\n", template.Title, err)
|
||||||
|
failedIssues = append(failedIssues, template.Title)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add labels
|
||||||
|
for _, label := range template.Labels {
|
||||||
|
if err := store.AddLabel(ctx, issue.ID, label, actor); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: failed to add label %s to %s: %v\n", label, issue.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dependencies
|
||||||
|
for _, depSpec := range template.Dependencies {
|
||||||
|
depSpec = strings.TrimSpace(depSpec)
|
||||||
|
if depSpec == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var depType types.DependencyType
|
||||||
|
var dependsOnID string
|
||||||
|
|
||||||
|
// Parse format: "type:id" or just "id" (defaults to "blocks")
|
||||||
|
if strings.Contains(depSpec, ":") {
|
||||||
|
parts := strings.SplitN(depSpec, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: invalid dependency format '%s' for %s\n", depSpec, issue.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
depType = types.DependencyType(strings.TrimSpace(parts[0]))
|
||||||
|
dependsOnID = strings.TrimSpace(parts[1])
|
||||||
|
} else {
|
||||||
|
depType = types.DepBlocks
|
||||||
|
dependsOnID = depSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
if !depType.IsValid() {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: invalid dependency type '%s' for %s\n", depType, issue.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dep := &types.Dependency{
|
||||||
|
IssueID: issue.ID,
|
||||||
|
DependsOnID: dependsOnID,
|
||||||
|
Type: depType,
|
||||||
|
}
|
||||||
|
if err := store.AddDependency(ctx, dep, actor); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: failed to add dependency %s -> %s: %v\n", issue.ID, dependsOnID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createdIssues = append(createdIssues, issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule auto-flush
|
||||||
|
if len(createdIssues) > 0 {
|
||||||
|
markDirtyAndScheduleFlush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report failures if any
|
||||||
|
if len(failedIssues) > 0 {
|
||||||
|
red := color.New(color.FgRed).SprintFunc()
|
||||||
|
fmt.Fprintf(os.Stderr, "\n%s Failed to create %d issues:\n", red("✗"), len(failedIssues))
|
||||||
|
for _, title := range failedIssues {
|
||||||
|
fmt.Fprintf(os.Stderr, " - %s\n", title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
outputJSON(createdIssues)
|
||||||
|
} else {
|
||||||
|
green := color.New(color.FgGreen).SprintFunc()
|
||||||
|
fmt.Printf("%s Created %d issues from %s:\n", green("✓"), len(createdIssues), filepath)
|
||||||
|
for _, issue := range createdIssues {
|
||||||
|
fmt.Printf(" %s: %s [P%d, %s]\n", issue.ID, issue.Title, issue.Priority, issue.IssueType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var createCmd = &cobra.Command{
|
var createCmd = &cobra.Command{
|
||||||
Use: "create [title]",
|
Use: "create [title]",
|
||||||
Short: "Create a new issue",
|
Short: "Create a new issue (or multiple issues from markdown file)",
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(0), // Changed to allow no args when using -f
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
file, _ := cmd.Flags().GetString("file")
|
||||||
|
|
||||||
|
// If file flag is provided, parse markdown and create multiple issues
|
||||||
|
if file != "" {
|
||||||
|
if len(args) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: cannot specify both title and --file flag\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
createIssuesFromMarkdown(cmd, file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original single-issue creation logic
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: title required (or use --file to create from markdown)\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
title := args[0]
|
title := args[0]
|
||||||
description, _ := cmd.Flags().GetString("description")
|
description, _ := cmd.Flags().GetString("description")
|
||||||
design, _ := cmd.Flags().GetString("design")
|
design, _ := cmd.Flags().GetString("design")
|
||||||
@@ -649,6 +778,7 @@ var createCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
createCmd.Flags().StringP("file", "f", "", "Create multiple issues from markdown file")
|
||||||
createCmd.Flags().StringP("description", "d", "", "Issue description")
|
createCmd.Flags().StringP("description", "d", "", "Issue description")
|
||||||
createCmd.Flags().String("design", "", "Design notes")
|
createCmd.Flags().String("design", "", "Design notes")
|
||||||
createCmd.Flags().String("acceptance", "", "Acceptance criteria")
|
createCmd.Flags().String("acceptance", "", "Acceptance criteria")
|
||||||
|
|||||||
289
cmd/bd/markdown.go
Normal file
289
cmd/bd/markdown.go
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
// Package main provides the bd command-line interface.
|
||||||
|
// This file implements markdown file parsing for bulk issue creation from structured markdown documents.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// h2Regex matches markdown H2 headers (## Title) for issue titles.
|
||||||
|
// Compiled once at package init for performance.
|
||||||
|
h2Regex = regexp.MustCompile(`^##\s+(.+)$`)
|
||||||
|
|
||||||
|
// h3Regex matches markdown H3 headers (### Section) for issue sections.
|
||||||
|
// Compiled once at package init for performance.
|
||||||
|
h3Regex = regexp.MustCompile(`^###\s+(.+)$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// IssueTemplate represents a parsed issue from markdown
|
||||||
|
type IssueTemplate struct {
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Design string
|
||||||
|
AcceptanceCriteria string
|
||||||
|
Priority int
|
||||||
|
IssueType types.IssueType
|
||||||
|
Assignee string
|
||||||
|
Labels []string
|
||||||
|
Dependencies []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePriority extracts and validates a priority value from content.
|
||||||
|
// Returns the parsed priority (0-4) or -1 if invalid.
|
||||||
|
func parsePriority(content string) int {
|
||||||
|
var p int
|
||||||
|
if _, err := fmt.Sscanf(content, "%d", &p); err == nil && p >= 0 && p <= 4 {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
return -1 // Invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIssueType extracts and validates an issue type from content.
|
||||||
|
// Returns the validated type or empty string if invalid.
|
||||||
|
func parseIssueType(content, issueTitle string) types.IssueType {
|
||||||
|
issueType := types.IssueType(strings.TrimSpace(content))
|
||||||
|
|
||||||
|
// Validate issue type
|
||||||
|
validTypes := map[types.IssueType]bool{
|
||||||
|
types.TypeBug: true,
|
||||||
|
types.TypeFeature: true,
|
||||||
|
types.TypeTask: true,
|
||||||
|
types.TypeEpic: true,
|
||||||
|
types.TypeChore: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validTypes[issueType] {
|
||||||
|
// Warn but continue with default
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: invalid issue type '%s' in '%s', using default 'task'\n",
|
||||||
|
issueType, issueTitle)
|
||||||
|
return types.TypeTask
|
||||||
|
}
|
||||||
|
|
||||||
|
return issueType
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseStringList extracts a list of strings from content, splitting by comma or whitespace.
|
||||||
|
// This is a generic helper used by parseLabels and parseDependencies.
|
||||||
|
func parseStringList(content string) []string {
|
||||||
|
var items []string
|
||||||
|
fields := strings.FieldsFunc(content, func(r rune) bool {
|
||||||
|
return r == ',' || r == ' ' || r == '\n'
|
||||||
|
})
|
||||||
|
for _, item := range fields {
|
||||||
|
item = strings.TrimSpace(item)
|
||||||
|
if item != "" {
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLabels extracts labels from content, splitting by comma or whitespace.
|
||||||
|
func parseLabels(content string) []string {
|
||||||
|
return parseStringList(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDependencies extracts dependencies from content, splitting by comma or whitespace.
|
||||||
|
func parseDependencies(content string) []string {
|
||||||
|
return parseStringList(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// processIssueSection processes a parsed section and updates the issue template.
|
||||||
|
func processIssueSection(issue *IssueTemplate, section, content string) {
|
||||||
|
content = strings.TrimSpace(content)
|
||||||
|
if content == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(section) {
|
||||||
|
case "priority":
|
||||||
|
if p := parsePriority(content); p != -1 {
|
||||||
|
issue.Priority = p
|
||||||
|
}
|
||||||
|
case "type":
|
||||||
|
issue.IssueType = parseIssueType(content, issue.Title)
|
||||||
|
case "description":
|
||||||
|
issue.Description = content
|
||||||
|
case "design":
|
||||||
|
issue.Design = content
|
||||||
|
case "acceptance criteria", "acceptance":
|
||||||
|
issue.AcceptanceCriteria = content
|
||||||
|
case "assignee":
|
||||||
|
issue.Assignee = strings.TrimSpace(content)
|
||||||
|
case "labels":
|
||||||
|
issue.Labels = parseLabels(content)
|
||||||
|
case "dependencies", "deps":
|
||||||
|
issue.Dependencies = parseDependencies(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateMarkdownPath validates and cleans a markdown file path to prevent security issues.
|
||||||
|
// It checks for directory traversal attempts and ensures the file is a markdown file.
|
||||||
|
func validateMarkdownPath(path string) (string, error) {
|
||||||
|
// Clean the path
|
||||||
|
cleanPath := filepath.Clean(path)
|
||||||
|
|
||||||
|
// Prevent directory traversal
|
||||||
|
if strings.Contains(cleanPath, "..") {
|
||||||
|
return "", fmt.Errorf("invalid file path: directory traversal not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure it's a markdown file
|
||||||
|
ext := strings.ToLower(filepath.Ext(cleanPath))
|
||||||
|
if ext != ".md" && ext != ".markdown" {
|
||||||
|
return "", fmt.Errorf("invalid file type: only .md and .markdown files are supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file exists and is not a directory
|
||||||
|
info, err := os.Stat(cleanPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot access file: %w", err)
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return "", fmt.Errorf("path is a directory, not a file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMarkdownFile parses a markdown file and extracts issue templates.
|
||||||
|
// Expected format:
|
||||||
|
// ## Issue Title
|
||||||
|
// Description text...
|
||||||
|
//
|
||||||
|
// ### Priority
|
||||||
|
// 2
|
||||||
|
//
|
||||||
|
// ### Type
|
||||||
|
// feature
|
||||||
|
//
|
||||||
|
// ### Description
|
||||||
|
// Detailed description...
|
||||||
|
//
|
||||||
|
// ### Design
|
||||||
|
// Design notes...
|
||||||
|
//
|
||||||
|
// ### Acceptance Criteria
|
||||||
|
// - Criterion 1
|
||||||
|
// - Criterion 2
|
||||||
|
//
|
||||||
|
// ### Assignee
|
||||||
|
// username
|
||||||
|
//
|
||||||
|
// ### Labels
|
||||||
|
// label1, label2
|
||||||
|
//
|
||||||
|
// ### Dependencies
|
||||||
|
// bd-10, bd-20
|
||||||
|
func parseMarkdownFile(path string) ([]*IssueTemplate, error) {
|
||||||
|
// Validate and clean the file path
|
||||||
|
cleanPath, err := validateMarkdownPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// #nosec G304 -- Path is validated by validateMarkdownPath which prevents traversal
|
||||||
|
file, err := os.Open(cleanPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = file.Close() // Close errors on read-only operations are not actionable
|
||||||
|
}()
|
||||||
|
|
||||||
|
var issues []*IssueTemplate
|
||||||
|
var currentIssue *IssueTemplate
|
||||||
|
var currentSection string
|
||||||
|
var sectionContent strings.Builder
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
// Increase buffer size for large markdown files
|
||||||
|
const maxScannerBuffer = 1024 * 1024 // 1MB
|
||||||
|
buf := make([]byte, maxScannerBuffer)
|
||||||
|
scanner.Buffer(buf, maxScannerBuffer)
|
||||||
|
|
||||||
|
// Helper to finalize current section
|
||||||
|
finalizeSection := func() {
|
||||||
|
if currentIssue == nil || currentSection == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content := sectionContent.String()
|
||||||
|
processIssueSection(currentIssue, currentSection, content)
|
||||||
|
sectionContent.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
// Check for H2 (new issue)
|
||||||
|
if matches := h2Regex.FindStringSubmatch(line); matches != nil {
|
||||||
|
// Finalize previous section if any
|
||||||
|
finalizeSection()
|
||||||
|
|
||||||
|
// Save previous issue if any
|
||||||
|
if currentIssue != nil {
|
||||||
|
issues = append(issues, currentIssue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new issue
|
||||||
|
currentIssue = &IssueTemplate{
|
||||||
|
Title: strings.TrimSpace(matches[1]),
|
||||||
|
Priority: 2, // Default priority
|
||||||
|
IssueType: "task", // Default type
|
||||||
|
}
|
||||||
|
currentSection = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for H3 (section within issue)
|
||||||
|
if matches := h3Regex.FindStringSubmatch(line); matches != nil {
|
||||||
|
// Finalize previous section
|
||||||
|
finalizeSection()
|
||||||
|
|
||||||
|
// Start new section
|
||||||
|
currentSection = strings.TrimSpace(matches[1])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular content line - append to current section
|
||||||
|
if currentIssue != nil && currentSection != "" {
|
||||||
|
if sectionContent.Len() > 0 {
|
||||||
|
sectionContent.WriteString("\n")
|
||||||
|
}
|
||||||
|
sectionContent.WriteString(line)
|
||||||
|
} else if currentIssue != nil && currentSection == "" && currentIssue.Description == "" {
|
||||||
|
// First lines after title (before any section) become description
|
||||||
|
if line != "" {
|
||||||
|
if currentIssue.Description != "" {
|
||||||
|
currentIssue.Description += "\n"
|
||||||
|
}
|
||||||
|
currentIssue.Description += line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize last section and issue
|
||||||
|
finalizeSection()
|
||||||
|
if currentIssue != nil {
|
||||||
|
issues = append(issues, currentIssue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we found any issues
|
||||||
|
if len(issues) == 0 {
|
||||||
|
return nil, fmt.Errorf("no issues found in markdown file (expected ## Issue Title format)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues, nil
|
||||||
|
}
|
||||||
238
cmd/bd/markdown_test.go
Normal file
238
cmd/bd/markdown_test.go
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseMarkdownFile(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
content string
|
||||||
|
expected []*IssueTemplate
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple issue",
|
||||||
|
content: `## Fix authentication bug
|
||||||
|
|
||||||
|
This is a critical bug in the auth system.
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
1
|
||||||
|
|
||||||
|
### Type
|
||||||
|
bug
|
||||||
|
`,
|
||||||
|
expected: []*IssueTemplate{
|
||||||
|
{
|
||||||
|
Title: "Fix authentication bug",
|
||||||
|
Description: "This is a critical bug in the auth system.",
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: "bug",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple issues",
|
||||||
|
content: `## First Issue
|
||||||
|
|
||||||
|
Description for first issue.
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
0
|
||||||
|
|
||||||
|
### Type
|
||||||
|
feature
|
||||||
|
|
||||||
|
## Second Issue
|
||||||
|
|
||||||
|
Description for second issue.
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
2
|
||||||
|
|
||||||
|
### Type
|
||||||
|
task
|
||||||
|
`,
|
||||||
|
expected: []*IssueTemplate{
|
||||||
|
{
|
||||||
|
Title: "First Issue",
|
||||||
|
Description: "Description for first issue.",
|
||||||
|
Priority: 0,
|
||||||
|
IssueType: "feature",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Second Issue",
|
||||||
|
Description: "Description for second issue.",
|
||||||
|
Priority: 2,
|
||||||
|
IssueType: "task",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "issue with all fields",
|
||||||
|
content: `## Comprehensive Issue
|
||||||
|
|
||||||
|
Initial description text.
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
1
|
||||||
|
|
||||||
|
### Type
|
||||||
|
feature
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Detailed description here.
|
||||||
|
Multi-line support.
|
||||||
|
|
||||||
|
### Design
|
||||||
|
Design notes go here.
|
||||||
|
|
||||||
|
### Acceptance Criteria
|
||||||
|
- Must do this
|
||||||
|
- Must do that
|
||||||
|
|
||||||
|
### Assignee
|
||||||
|
alice
|
||||||
|
|
||||||
|
### Labels
|
||||||
|
backend, urgent
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
bd-10, bd-20
|
||||||
|
`,
|
||||||
|
expected: []*IssueTemplate{
|
||||||
|
{
|
||||||
|
Title: "Comprehensive Issue",
|
||||||
|
Description: "Detailed description here.\nMulti-line support.",
|
||||||
|
Design: "Design notes go here.",
|
||||||
|
AcceptanceCriteria: "- Must do this\n- Must do that",
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: "feature",
|
||||||
|
Assignee: "alice",
|
||||||
|
Labels: []string{"backend", "urgent"},
|
||||||
|
Dependencies: []string{"bd-10", "bd-20"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dependencies with types",
|
||||||
|
content: `## Issue with typed dependencies
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
2
|
||||||
|
|
||||||
|
### Type
|
||||||
|
task
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
blocks:bd-10, discovered-from:bd-20
|
||||||
|
`,
|
||||||
|
expected: []*IssueTemplate{
|
||||||
|
{
|
||||||
|
Title: "Issue with typed dependencies",
|
||||||
|
Priority: 2,
|
||||||
|
IssueType: "task",
|
||||||
|
Dependencies: []string{"blocks:bd-10", "discovered-from:bd-20"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default values",
|
||||||
|
content: `## Minimal Issue
|
||||||
|
|
||||||
|
Just a title and description.
|
||||||
|
`,
|
||||||
|
expected: []*IssueTemplate{
|
||||||
|
{
|
||||||
|
Title: "Minimal Issue",
|
||||||
|
Description: "Just a title and description.",
|
||||||
|
Priority: 2, // default
|
||||||
|
IssueType: "task", // default
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create temp file
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
tmpFile := filepath.Join(tmpDir, "test.md")
|
||||||
|
if err := os.WriteFile(tmpFile, []byte(tt.content), 0600); err != nil {
|
||||||
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse file
|
||||||
|
got, err := parseMarkdownFile(tmpFile)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("parseMarkdownFile() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(got) != len(tt.expected) {
|
||||||
|
t.Errorf("parseMarkdownFile() got %d issues, want %d", len(got), len(tt.expected))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare each issue
|
||||||
|
for i, gotIssue := range got {
|
||||||
|
wantIssue := tt.expected[i]
|
||||||
|
|
||||||
|
if gotIssue.Title != wantIssue.Title {
|
||||||
|
t.Errorf("Issue %d: Title = %q, want %q", i, gotIssue.Title, wantIssue.Title)
|
||||||
|
}
|
||||||
|
if gotIssue.Description != wantIssue.Description {
|
||||||
|
t.Errorf("Issue %d: Description = %q, want %q", i, gotIssue.Description, wantIssue.Description)
|
||||||
|
}
|
||||||
|
if gotIssue.Priority != wantIssue.Priority {
|
||||||
|
t.Errorf("Issue %d: Priority = %d, want %d", i, gotIssue.Priority, wantIssue.Priority)
|
||||||
|
}
|
||||||
|
if gotIssue.IssueType != wantIssue.IssueType {
|
||||||
|
t.Errorf("Issue %d: IssueType = %q, want %q", i, gotIssue.IssueType, wantIssue.IssueType)
|
||||||
|
}
|
||||||
|
if gotIssue.Design != wantIssue.Design {
|
||||||
|
t.Errorf("Issue %d: Design = %q, want %q", i, gotIssue.Design, wantIssue.Design)
|
||||||
|
}
|
||||||
|
if gotIssue.AcceptanceCriteria != wantIssue.AcceptanceCriteria {
|
||||||
|
t.Errorf("Issue %d: AcceptanceCriteria = %q, want %q", i, gotIssue.AcceptanceCriteria, wantIssue.AcceptanceCriteria)
|
||||||
|
}
|
||||||
|
if gotIssue.Assignee != wantIssue.Assignee {
|
||||||
|
t.Errorf("Issue %d: Assignee = %q, want %q", i, gotIssue.Assignee, wantIssue.Assignee)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare slices
|
||||||
|
if !stringSlicesEqual(gotIssue.Labels, wantIssue.Labels) {
|
||||||
|
t.Errorf("Issue %d: Labels = %v, want %v", i, gotIssue.Labels, wantIssue.Labels)
|
||||||
|
}
|
||||||
|
if !stringSlicesEqual(gotIssue.Dependencies, wantIssue.Dependencies) {
|
||||||
|
t.Errorf("Issue %d: Dependencies = %v, want %v", i, gotIssue.Dependencies, wantIssue.Dependencies)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMarkdownFile_FileNotFound(t *testing.T) {
|
||||||
|
_, err := parseMarkdownFile("/nonexistent/file.md")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for non-existent file, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringSlicesEqual(a, b []string) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(a) == 0 && len(b) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user