azdo-cli 0.4.0 → 0.5.0-develop.148
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -11
- package/dist/index.js +39 -10
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
12
12
|
- Update work item state (`set-state`)
|
|
13
13
|
- Assign and unassign work items (`assign`)
|
|
14
14
|
- Set any work item field by reference name (`set-field`)
|
|
15
|
-
- Create or update
|
|
15
|
+
- Create or update work items from markdown documents (`upsert`)
|
|
16
16
|
- Read rich-text fields as markdown (`get-md-field`)
|
|
17
17
|
- Set rich-text fields as markdown from inline text, file, or stdin (`set-md-field`)
|
|
18
18
|
- Check branch pull request status, open PRs to `develop`, and review active comments (`pr`)
|
|
@@ -26,6 +26,20 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
26
26
|
npm install -g azdo-cli
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
## Utility Scripts
|
|
30
|
+
|
|
31
|
+
The repository also includes a helper script for syncing local `.env` entries into GitHub Actions secrets for the current repository:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
./scripts/sync-env-to-gh-secrets.zsh
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
It walks upward from the current directory until it finds a `.env`, then sets each valid `KEY=VALUE` entry with `gh secret set`. You can also limit the sync to selected keys:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
./scripts/sync-env-to-gh-secrets.zsh FOO BAR
|
|
41
|
+
```
|
|
42
|
+
|
|
29
43
|
## Authentication and Context Resolution
|
|
30
44
|
|
|
31
45
|
PAT resolution order:
|
|
@@ -51,8 +65,8 @@ azdo get-item 12345
|
|
|
51
65
|
# 3) Update state
|
|
52
66
|
azdo set-state 12345 "Active"
|
|
53
67
|
|
|
54
|
-
# 4) Create or update a
|
|
55
|
-
azdo upsert --content $'---\nTitle: Improve markdown import UX\nState: New\n---'
|
|
68
|
+
# 4) Create or update a work item from markdown
|
|
69
|
+
azdo upsert --type "User Story" --content $'---\nTitle: Improve markdown import UX\nState: New\n---'
|
|
56
70
|
```
|
|
57
71
|
|
|
58
72
|
## Command Cheat Sheet
|
|
@@ -63,7 +77,7 @@ azdo upsert --content $'---\nTitle: Improve markdown import UX\nState: New\n---'
|
|
|
63
77
|
| `azdo set-state <id> <state>` | Change work item state | `--json`, `--org`, `--project` |
|
|
64
78
|
| `azdo assign <id> [name]` | Assign or unassign owner | `--unassign`, `--json`, `--org`, `--project` |
|
|
65
79
|
| `azdo set-field <id> <field> <value>` | Update any field | `--json`, `--org`, `--project` |
|
|
66
|
-
| `azdo upsert [id]` | Create or update a
|
|
80
|
+
| `azdo upsert [id]` | Create or update a work item from markdown | `--content`, `--file`, `--type`, `--json`, `--org`, `--project` |
|
|
67
81
|
| `azdo get-md-field <id> <field>` | Get field as markdown | `--org`, `--project` |
|
|
68
82
|
| `azdo set-md-field <id> <field> [content]` | Set markdown field | `--file`, `--json`, `--org`, `--project` |
|
|
69
83
|
| `azdo list-fields <id>` | List all fields of a work item | `--json`, `--org`, `--project` |
|
|
@@ -88,8 +102,6 @@ azdo get-item 12345 --fields "System.Tags,Microsoft.VSTS.Common.Priority"
|
|
|
88
102
|
# Convert rich text fields to markdown
|
|
89
103
|
azdo get-item 12345 --markdown
|
|
90
104
|
|
|
91
|
-
# Disable markdown even if config is on
|
|
92
|
-
azdo get-item 12345 --no-markdown
|
|
93
105
|
```
|
|
94
106
|
|
|
95
107
|
```bash
|
|
@@ -118,7 +130,7 @@ azdo list-fields 12345 --json
|
|
|
118
130
|
|
|
119
131
|
The `get-item` command can convert HTML rich-text fields to readable markdown. Resolution order:
|
|
120
132
|
|
|
121
|
-
1. `--markdown`
|
|
133
|
+
1. `--markdown` flag enables markdown for the current call
|
|
122
134
|
2. Config setting: `azdo config set markdown true`
|
|
123
135
|
3. Default: off (HTML stripped to plain text)
|
|
124
136
|
|
|
@@ -155,7 +167,7 @@ azdo pr comments
|
|
|
155
167
|
|
|
156
168
|
`azdo pr status`
|
|
157
169
|
|
|
158
|
-
- Lists
|
|
170
|
+
- Lists pull requests for the current branch
|
|
159
171
|
- Prints `No pull requests found for branch <branch>.` when no PRs exist
|
|
160
172
|
- Supports `--json` for machine-readable output
|
|
161
173
|
|
|
@@ -177,11 +189,14 @@ azdo pr comments
|
|
|
177
189
|
|
|
178
190
|
## azdo upsert
|
|
179
191
|
|
|
180
|
-
`azdo upsert` accepts a single markdown
|
|
192
|
+
`azdo upsert` accepts a single markdown work-item document and either creates a new Azure DevOps work item or updates an existing one. Omit `[id]` to create; pass `[id]` to update that work item in place. Create mode defaults to `Task`, and `--type <work item type>` lets you create `Bug`, `User Story`, `Feature`, `Epic`, and other Azure DevOps work item types.
|
|
181
193
|
|
|
182
194
|
```bash
|
|
183
|
-
# Create from inline content
|
|
184
|
-
azdo upsert --content $'---\nTitle: Improve markdown import UX\nAssigned To: user@example.com\nState: New\n---'
|
|
195
|
+
# Create a Bug from inline content
|
|
196
|
+
azdo upsert --type Bug --content $'---\nTitle: Improve markdown import UX\nAssigned To: user@example.com\nState: New\n---'
|
|
197
|
+
|
|
198
|
+
# Preserve the default Task create behavior
|
|
199
|
+
azdo upsert --content $'---\nTitle: Follow-up task\nAssigned To: user@example.com\nState: New\n---'
|
|
185
200
|
|
|
186
201
|
# Update from a file
|
|
187
202
|
azdo upsert 12345 --file ./task-import.md
|
|
@@ -194,9 +209,16 @@ The command requires exactly one source flag:
|
|
|
194
209
|
|
|
195
210
|
- `azdo upsert [id] --content <markdown>`
|
|
196
211
|
- `azdo upsert [id] --file <path>`
|
|
212
|
+
- `azdo upsert --type <work-item-type> --content <markdown>`
|
|
197
213
|
|
|
198
214
|
If `--file` succeeds, the source file is deleted after the Azure DevOps write completes. If parsing, validation, or the API call fails, the file is preserved. If deletion fails after a successful write, the command still succeeds and prints a warning.
|
|
199
215
|
|
|
216
|
+
Type rules:
|
|
217
|
+
|
|
218
|
+
- `--type` is optional for create and defaults to `Task`.
|
|
219
|
+
- `--type` is only valid when creating a new work item.
|
|
220
|
+
- Human-readable and JSON success output include the resulting work item type.
|
|
221
|
+
|
|
200
222
|
### Task Document Format
|
|
201
223
|
|
|
202
224
|
The document starts with YAML front matter for scalar fields, followed by optional `##` heading sections for markdown rich-text fields.
|
|
@@ -245,6 +267,7 @@ Clear semantics:
|
|
|
245
267
|
{
|
|
246
268
|
"action": "created",
|
|
247
269
|
"id": 12345,
|
|
270
|
+
"workItemType": "User Story",
|
|
248
271
|
"fields": {
|
|
249
272
|
"System.Title": "Improve markdown import UX",
|
|
250
273
|
"System.Description": "Implement a single-command task import flow."
|
|
@@ -288,6 +311,8 @@ These commands support `--json` for machine-readable output:
|
|
|
288
311
|
- `assign`
|
|
289
312
|
- `set-field`
|
|
290
313
|
- `set-md-field`
|
|
314
|
+
- `upsert`
|
|
315
|
+
- `pr status|open|comments`
|
|
291
316
|
- `config set|get|list|unset`
|
|
292
317
|
|
|
293
318
|
## Development
|
package/dist/index.js
CHANGED
|
@@ -39,7 +39,15 @@ async function fetchWithErrors(url, init) {
|
|
|
39
39
|
}
|
|
40
40
|
if (response.status === 401) throw new Error("AUTH_FAILED");
|
|
41
41
|
if (response.status === 403) throw new Error("PERMISSION_DENIED");
|
|
42
|
-
if (response.status === 404)
|
|
42
|
+
if (response.status === 404) {
|
|
43
|
+
let detail = "";
|
|
44
|
+
try {
|
|
45
|
+
const body = await response.text();
|
|
46
|
+
detail = ` | url=${url} | body=${body}`;
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`NOT_FOUND${detail}`);
|
|
50
|
+
}
|
|
43
51
|
return response;
|
|
44
52
|
}
|
|
45
53
|
async function readResponseMessage(response) {
|
|
@@ -633,7 +641,7 @@ function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
|
633
641
|
`Error: Access denied. Your PAT may lack ${scope} permissions for project "${context?.project}".
|
|
634
642
|
`
|
|
635
643
|
);
|
|
636
|
-
} else if (msg
|
|
644
|
+
} else if (msg.startsWith("NOT_FOUND")) {
|
|
637
645
|
process.stderr.write(
|
|
638
646
|
`Error: Work item ${id} not found in ${context?.org}/${context?.project}.
|
|
639
647
|
`
|
|
@@ -1383,7 +1391,7 @@ function buildAppliedFields(fields) {
|
|
|
1383
1391
|
function ensureTitleForCreate(fields) {
|
|
1384
1392
|
const titleField = fields.find((field) => field.refName === "System.Title");
|
|
1385
1393
|
if (!titleField || titleField.op === "clear" || titleField.value === null || titleField.value.trim() === "") {
|
|
1386
|
-
fail2("Title is required when creating a
|
|
1394
|
+
fail2("Title is required when creating a work item.");
|
|
1387
1395
|
}
|
|
1388
1396
|
}
|
|
1389
1397
|
function writeSuccess(result, options) {
|
|
@@ -1395,7 +1403,7 @@ function writeSuccess(result, options) {
|
|
|
1395
1403
|
const verb = result.action === "created" ? "Created" : "Updated";
|
|
1396
1404
|
const fields = Object.keys(result.fields).join(", ");
|
|
1397
1405
|
const suffix = fields ? ` (${fields})` : "";
|
|
1398
|
-
process.stdout.write(`${verb}
|
|
1406
|
+
process.stdout.write(`${verb} ${result.workItemType} #${result.id}${suffix}
|
|
1399
1407
|
`);
|
|
1400
1408
|
}
|
|
1401
1409
|
function cleanupSourceFile(sourceFile) {
|
|
@@ -1409,16 +1417,31 @@ function cleanupSourceFile(sourceFile) {
|
|
|
1409
1417
|
`);
|
|
1410
1418
|
}
|
|
1411
1419
|
}
|
|
1412
|
-
function
|
|
1420
|
+
function resolveCreateType(id, options) {
|
|
1421
|
+
if (options.type === void 0) {
|
|
1422
|
+
return "Task";
|
|
1423
|
+
}
|
|
1424
|
+
if (id !== void 0) {
|
|
1425
|
+
fail2("--type can only be used when creating a work item.");
|
|
1426
|
+
}
|
|
1427
|
+
const trimmedType = options.type.trim();
|
|
1428
|
+
if (trimmedType === "") {
|
|
1429
|
+
fail2("--type must be a non-empty work item type.");
|
|
1430
|
+
}
|
|
1431
|
+
return trimmedType;
|
|
1432
|
+
}
|
|
1433
|
+
function buildUpsertResult(action, writeResult, fields, fallbackWorkItemType) {
|
|
1413
1434
|
const appliedFields = buildAppliedFields(fields);
|
|
1435
|
+
const workItemType = writeResult.fields["System.WorkItemType"];
|
|
1414
1436
|
return {
|
|
1415
1437
|
action,
|
|
1416
1438
|
id: writeResult.id,
|
|
1439
|
+
workItemType: typeof workItemType === "string" && workItemType.trim() !== "" ? workItemType : fallbackWorkItemType,
|
|
1417
1440
|
fields: appliedFields
|
|
1418
1441
|
};
|
|
1419
1442
|
}
|
|
1420
1443
|
function isUpdateWriteError(err) {
|
|
1421
|
-
return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message
|
|
1444
|
+
return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message.startsWith("NOT_FOUND") || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("UPDATE_REJECTED:");
|
|
1422
1445
|
}
|
|
1423
1446
|
function isCreateWriteError(err) {
|
|
1424
1447
|
return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("HTTP_");
|
|
@@ -1448,10 +1471,11 @@ function handleUpsertError(err, id, context) {
|
|
|
1448
1471
|
}
|
|
1449
1472
|
function createUpsertCommand() {
|
|
1450
1473
|
const command = new Command9("upsert");
|
|
1451
|
-
command.description("Create or update a
|
|
1474
|
+
command.description("Create or update a work item from a markdown document").argument("[id]", "work item ID to update; omit to create a new work item").option("--content <markdown>", "task document content").option("--file <path>", "read task document from file").option("--type <workItemType>", "create mode work item type (defaults to Task)").option("--json", "output result as JSON").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(async (idStr, options) => {
|
|
1452
1475
|
validateOrgProjectPair(options);
|
|
1453
1476
|
const id = idStr === void 0 ? void 0 : parseWorkItemId(idStr);
|
|
1454
1477
|
const { content, sourceFile } = loadSourceContent(options);
|
|
1478
|
+
const createType = resolveCreateType(id, options);
|
|
1455
1479
|
let context;
|
|
1456
1480
|
try {
|
|
1457
1481
|
context = resolveContext(options);
|
|
@@ -1464,14 +1488,19 @@ function createUpsertCommand() {
|
|
|
1464
1488
|
const credential = await resolvePat();
|
|
1465
1489
|
let writeResult;
|
|
1466
1490
|
if (action === "created") {
|
|
1467
|
-
writeResult = await createWorkItem(context,
|
|
1491
|
+
writeResult = await createWorkItem(context, createType, credential.pat, operations);
|
|
1468
1492
|
} else {
|
|
1469
1493
|
if (id === void 0) {
|
|
1470
1494
|
fail2("Work item ID is required for updates.");
|
|
1471
1495
|
}
|
|
1472
1496
|
writeResult = await applyWorkItemPatch(context, id, credential.pat, operations);
|
|
1473
1497
|
}
|
|
1474
|
-
const result = buildUpsertResult(
|
|
1498
|
+
const result = buildUpsertResult(
|
|
1499
|
+
action,
|
|
1500
|
+
writeResult,
|
|
1501
|
+
document.fields,
|
|
1502
|
+
action === "created" ? createType : "Work item"
|
|
1503
|
+
);
|
|
1475
1504
|
writeSuccess(result, options);
|
|
1476
1505
|
cleanupSourceFile(sourceFile);
|
|
1477
1506
|
} catch (err) {
|
|
@@ -1684,7 +1713,7 @@ function handlePrCommandError(err, context, mode = "read") {
|
|
|
1684
1713
|
if (error.message === "NETWORK_ERROR") {
|
|
1685
1714
|
writeError("Could not connect to Azure DevOps. Check your network connection.");
|
|
1686
1715
|
}
|
|
1687
|
-
if (error.message
|
|
1716
|
+
if (error.message.startsWith("NOT_FOUND")) {
|
|
1688
1717
|
writeError(`Azure DevOps repository not found in ${context?.org}/${context?.project}.`);
|
|
1689
1718
|
}
|
|
1690
1719
|
if (error.message.startsWith("HTTP_")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "azdo-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-develop.148",
|
|
4
4
|
"description": "Azure DevOps CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"typecheck": "tsc --noEmit",
|
|
16
16
|
"format": "prettier --check src/",
|
|
17
17
|
"test": "npm run build && vitest run tests/unit",
|
|
18
|
-
"test:integration": "npm run build && vitest run tests/integration"
|
|
18
|
+
"test:integration": "npm run build && vitest run tests/integration",
|
|
19
|
+
"test:integration:full": "bash scripts/setup-keyring.sh && npm run test:integration"
|
|
19
20
|
},
|
|
20
21
|
"repository": {
|
|
21
22
|
"type": "git",
|