forked from Gitlink/gitlink-cli
docs: add batch issue operations design spec
This commit is contained in:
parent
fde9a5d7e5
commit
dd98d7ddc5
|
|
@ -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
|
||||
Loading…
Reference in New Issue