diff --git a/docs/superpowers/specs/2026-05-28-batch-issue-design.md b/docs/superpowers/specs/2026-05-28-batch-issue-design.md new file mode 100644 index 0000000..23681a6 --- /dev/null +++ b/docs/superpowers/specs/2026-05-28-batch-issue-design.md @@ -0,0 +1,350 @@ +# Design: Batch Issue Operations + +**Date:** 2026-05-28 +**Status:** Approved + +--- + +## 1. Overview + +Add batch operations for Issues in gitlink-cli, extending the existing `issue +batch-close` pattern with five new commands and a shared engine abstraction. + +### Commands + +``` +gitlink-cli issue +batch-open # Batch reopen issues +gitlink-cli issue +batch-close # Batch close issues (refactored) +gitlink-cli issue +batch-create # Batch create issues (CSV-driven) +gitlink-cli issue +batch-update # Batch update issues (full fields) +gitlink-cli issue +batch-assign # Batch assign assignee +gitlink-cli issue +batch-label # Batch label management (add/remove/set) +``` + +### Design decisions + +| Decision | Rationale | +|---|---| +| Separate commands (not unified `+batch --action X`) | Each command has focused flags and `--help`. Consistent with existing `+` prefix convention. | +| Single-issue commands coexist with batch commands | Different use cases: single-item is quick and direct; batch is for aggregated bulk operations with summary output. | +| Shared engine as functions, not interfaces | Keeps the codebase simple. A function callback pattern is sufficient; no need for interfaces with only one implementation. | +| Continue on Error | Aggregates results at end. Each issue is independent, no cascading failure risk. | +| Sequential execution | No concurrency. Simpler, safer, with `--delay` flag for API rate limiting. | + +--- + +## 2. Shared Engine: `batch_common.go` + +Extract common concerns from the existing `batch.go` into a function-level shared engine. + +### Types + +```go +type BatchResult struct { + ID string `json:"id" yaml:"id"` // Issue number + Action string `json:"action" yaml:"action"` // e.g. "close", "open", "assign" + Status string `json:"status" yaml:"status"` // "success" | "failed" + Error string `json:"error,omitempty" yaml:"error,omitempty"` +} + +type BatchSummary struct { + Repository string `json:"repository" yaml:"repository"` + DryRun bool `json:"dry_run" yaml:"dry_run"` + Total int `json:"total" yaml:"total"` + Succeeded int `json:"succeeded" yaml:"succeeded"` + Failed int `json:"failed" yaml:"failed"` + Results []BatchResult `json:"results" yaml:"results"` +} +``` + +### Shared functions + +```go +// ReadCSV reads a CSV file, returns headers and rows +func ReadCSV(path string) (headers []string, rows [][]string, err error) + +// FindColumn finds a column index by alias priority (e.g., "number" or "issue_number" or "project_issues_index") +func FindColumn(headers []string, aliases ...string) int + +// ResolveIssueNumbers resolves issue numbers from --numbers, --from CSV, or --search filters +func ResolveIssueNumbers(ctx *common.RuntimeContext) ([]string, error) + +// RunBatch iterates over numbers, calls fn for each, aggregates results +func RunBatch(ctx *common.RuntimeContext, numbers []string, action string, dryRun bool, + fn func(ctx *common.RuntimeContext, number string) error) (*BatchSummary, error) +``` + +### Common flags + +All batch commands share these flags (except `batch-create` which omits `--numbers` and `--search`): + +| Flag | Type | Default | Description | +|---|---|---|---| +| `--numbers` | string | — | Comma-separated issue numbers | +| `--from` / `-f` | string | — | CSV file path | +| `--search` | string | — | Keyword search filter | +| `--state` | string | — | State filter (open/closed) | +| `--label` | string | — | Label filter | +| `--dry-run` | bool | false | Preview without executing | +| `--confirm` | bool | false | Confirmation gate (required when not dry-run) | +| `--max` | int | 100 | Maximum items to process (safety guard) | +| `--delay` | int | 0 | Delay between API calls in milliseconds | +| `--verbose` / `-v` | bool | false | Print per-item progress | + +--- + +## 3. Command Design + +### 3.1 `+batch-open` + +Reopen closed issues. + +```bash +gitlink-cli issue +batch-open --numbers 1,2,3 +gitlink-cli issue +batch-open --from issues.csv +gitlink-cli issue +batch-open --search "bug" --state closed +``` + +- API: `PUT /v1/{owner}/{repo}/issues/{number}` with `status_id: 1` +- Requires two API calls per item: GET (fetch subject) → PUT (update status) +- Same pattern as `batch-close`, symmetric + +### 3.2 `+batch-close` (refactored) + +Existing command, refactored to use shared engine. + +- No functional changes +- CSV parsing, number resolution, iteration loop, and summary now use `batch_common.go` functions +- API: `PUT /v1/{owner}/{repo}/issues/{number}` with `status_id: 5` + +### 3.3 `+batch-create` + +Create new issues from CSV template. + +```bash +gitlink-cli issue +batch-create --from issues.csv +gitlink-cli issue +batch-create --print-schema +``` + +**CSV schema (`--print-schema` output):** + +``` +title,body,assignee,milestone,label,priority +``` + +| Column | Flag equivalent | Required | Notes | +|---|---|---|---| +| `title` | `--title` | Yes | Issue title | +| `body` | `--body` | No | Issue description | +| `assignee` | `--assignee` | No | Username or user ID | +| `milestone` | `--milestone` | No | Milestone name or ID | +| `label` | `--label` | No | Label names, separated by `;` | +| `priority` | `--priority` | No | Priority number | + +- API: `POST /v1/{owner}/{repo}/issues` +- Username-to-ID and name-to-ID lookups for assignee, milestone, and labels +- Does NOT use `ResolveIssueNumbers` or `RunBatch` — iterates over CSV rows directly +- Reuses `ReadCSV`, `FindColumn`, and `BatchSummary` + +### 3.4 `+batch-update` + +Update existing issues with full field support. + +**Mode A: CSV-driven** + +```bash +gitlink-cli issue +batch-update --from updates.csv +``` + +CSV column to API field mapping: + +| CSV column | API field | Coercion | +|---|---|---| +| `number` | URL path param | — | +| `title` | `subject` | Direct | +| `body` | `description` | Direct | +| `state` | `status_id` | `open` → 1, `closed` → 5 | +| `assignee` | `assigned_to_id` | Username → user ID lookup | +| `milestone` | `fixed_version_id` | Name → milestone ID lookup | +| `label` | `label_ids` | Name → label ID lookup | +| `priority` | `priority_id` | Direct | + +Only `number` is required. Other columns are optional — only the columns present are updated. + +**Mode B: Unified update via flags** + +```bash +gitlink-cli issue +batch-update --numbers 1,2,3 --state closed --assignee zhangsan +``` + +All listed issues receive the same update. + +**Execution flow per issue:** +``` +1. GET /v1/{owner}/{repo}/issues/{number} → fetch current subject + fields +2. Merge: current values + CSV/flags overrides +3. PUT /v1/{owner}/{repo}/issues/{number} → send merged data +4. Record result +``` + +- Reuses `ResolveIssueNumbers` and `BatchSummary` +- Own iteration loop due to GET-before-PUT pattern + +### 3.5 `+batch-assign` + +Assign issues to assignees. + +```bash +# Mode A: Uniform assignment +gitlink-cli issue +batch-assign --numbers 1,2,3 --assignee zhangsan + +# Mode B: CSV-driven per-issue assignment +gitlink-cli issue +batch-assign --from assign.csv +``` + +**CSV format (Mode B):** +``` +number,assignee +1,zhangsan +2,lisi +``` + +**Precedence rule:** If both `--assignee` and a CSV `assignee` column are present, the CSV value wins (per-item specificity over global default). + +- API: `PUT /v1/{owner}/{repo}/issues/{number}` with `assigned_to_id` +- Username → user ID lookup +- Reuses `ResolveIssueNumbers`, `ReadCSV`, `FindColumn`, `BatchSummary` + +### 3.6 `+batch-label` + +Manage labels on multiple issues. + +```bash +# Add labels +gitlink-cli issue +batch-label --numbers 1,2,3 --action add --labels bug,urgent + +# Remove labels +gitlink-cli issue +batch-label --from issues.csv --action remove --labels wontfix + +# Replace all labels +gitlink-cli issue +batch-label --search "refactor" --action set --labels enhancement,v2.0 +``` + +**Label specification:** Both name and ID are supported. +- `--labels bug,urgent` → look up label IDs by name +- `--label-ids 42,17` → use IDs directly + +Name lookup: fetch `GET /v1/{owner}/{repo}/labels` once, cache name→ID mapping for the batch. + +**Execution flow per issue:** +``` +1. GET /v1/{owner}/{repo}/issues/{number} → fetch subject + existing label_ids +2. Compute new label_ids based on --action: + add: existing ∪ new + remove: existing − specified + set: new +3. PUT /v1/{owner}/{repo}/issues/{number} → send updated labels +4. Record result +``` + +- Reuses `ResolveIssueNumbers`, `ReadCSV`, `FindColumn`, `BatchSummary` +- Own iteration loop + +--- + +## 4. Relations to Shared Engine + +| Command | Uses `ResolveIssueNumbers`? | Uses `RunBatch`? | Uses `ReadCSV`/`FindColumn`? | Uses `BatchSummary`? | +|---|---|---|---|---| +| `batch-open` | Yes | Yes | No (via ResolveIssueNumbers) | Yes | +| `batch-close` | Yes | Yes | No (via ResolveIssueNumbers) | Yes | +| `batch-create` | No | No | Yes | Yes | +| `batch-update` | Yes | No | Yes | Yes | +| `batch-assign` | Yes | No | Yes | Yes | +| `batch-label` | Yes | No | Yes | Yes | + +--- + +## 5. Error Handling + +### Per-item errors (Continue on Error) + +Each API call failure is captured per-item. The batch continues through remaining items. Final output includes the `BatchSummary` with per-item error details. + +### Safety gates + +- `--dry-run`: Resolves all issue numbers, prints what would be processed, exits without API calls +- `--confirm`: When NOT in dry-run mode, this flag is required. Omitting it causes an error: `"Add --confirm to proceed, or use --dry-run to preview"` +- `--max` (default 100): Truncates input list with a warning if exceeded + +### API rate limiting + +- `--delay` flag (milliseconds) inserts a `time.Sleep` between iterations +- Default 0 (no delay); users can tune based on their API quota + +--- + +## 6. Output Format + +All batch commands output a `BatchSummary` wrapped in the standard `Envelope` format: + +```json +{ + "ok": true, + "data": { + "repository": "owner/repo", + "dry_run": false, + "total": 5, + "succeeded": 4, + "failed": 1, + "results": [ + {"id": "1", "action": "close", "status": "success"}, + {"id": "2", "action": "close", "status": "success"}, + {"id": "3", "action": "close", "status": "success"}, + {"id": "4", "action": "close", "status": "failed", "error": "already closed"}, + {"id": "5", "action": "close", "status": "success"} + ] + } +} +``` + +Supports `--format json` (default), `--format yaml`, and `--format table`. + +--- + +## 7. File Structure + +``` +shortcuts/issue/ +├── issue.go # Existing single-issue operations +├── batch.go # +batch-close (refactored) +├── batch_common.go # NEW: shared engine +├── batch_open.go # NEW: +batch-open +├── batch_create.go # NEW: +batch-create +├── batch_update.go # NEW: +batch-update +├── batch_assign.go # NEW: +batch-assign +├── batch_label.go # NEW: +batch-label +├── issue_test.go # Existing tests +└── batch_test.go # Existing + expanded tests +``` + +--- + +## 8. Implementation Order + +1. Extract `batch_common.go` from existing `batch.go` — validate abstraction with `batch-close` as sole consumer +2. Implement `batch-close` using new shared functions +3. Add `batch-open` — simplest new command, symmetric to close +4. Add `batch-assign` (uniform mode) — validates engine for second consumer +5. Add `batch-label` — introduces label lookup and three action modes +6. Add `batch-update` — most complex (field mapping, merge logic) +7. Add `batch-create` — separate loop, CSV-driven only + +--- + +## 9. Testing Strategy + +- Unit tests for shared engine functions: `ReadCSV`, `FindColumn`, `ResolveIssueNumbers`, `RunBatch` +- Per-command integration tests using existing `common.NewTestServer()` / `common.NewTestContext()` pattern +- Test dry-run, confirm gate, max truncation, verbose output +- Test label name→ID caching +- Test CSV edge cases: empty columns, duplicate headers, missing required columns