docs: add batch issue operations design spec

This commit is contained in:
wauxing 2026-05-28 19:01:01 +08:00
parent fde9a5d7e5
commit dd98d7ddc5
1 changed files with 350 additions and 0 deletions

View File

@ -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