pingcode-cli-unofficial 1.7.1

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.
@@ -0,0 +1,432 @@
1
+ # 源码管理 (scm) — `scm`
2
+
3
+ > Part of the `pingcode` skill. Read [`../SKILL.md`](../SKILL.md) first — it carries the
4
+ > authentication gate, the stdout/stderr contract, the exit-code table and the rules that
5
+ > apply to every module. This file is only the scm command surface and the ids it needs.
6
+
7
+ **What this module is for.** It is the *write-back* API a CI system uses: you tell PingCode
8
+ which hosting platform, git identities, repositories, branches and commits exist, and
9
+ PingCode links them to work items. It does **not** read your git server — nothing here
10
+ clones, pushes or inspects a repository. Every row you create is a record inside PingCode.
11
+
12
+ **The CI write-back path, in order.** Each step needs the one before it:
13
+
14
+ ```
15
+ platform → repo → branch ─┬→ pr → review
16
+ ├→ ref (commit ↔ branch)
17
+ commit ──────┘
18
+ ```
19
+
20
+ A commit is created *without* a repository (it is organisation-level), so `scm ref create`
21
+ is what attaches it to a branch. Do not look for a `--repo` on `scm commit`.
22
+
23
+ A pull request needs its **target branch id** first, and a code review needs its **pull
24
+ request id** — both are ids, not names or numbers, so create the branch and the PR before
25
+ you try to hang anything off them.
26
+
27
+ **Two group-wide facts.**
28
+
29
+ - Every scm endpoint is **企业令牌 only** — which is exactly the token this CLI holds, so the
30
+ whole group works out of the box. Required scopes: `pcp:read:devops:code` and
31
+ `pcp:write:devops:code`. Without them you get exit 4.
32
+ - An scm **platform** (托管平台) is *not* a ship product, even though both sit under a
33
+ `products` URL segment. `pingcode product …` is 产品管理; `pingcode scm platform …` is a
34
+ GitHub/GitLab/SVN server record.
35
+
36
+ ### Hosting platforms — `scm platform`
37
+
38
+ Everything else in scm is addressed under a platform, so this is always the first call.
39
+
40
+ ```bash
41
+ pingcode scm platform list --json
42
+ pingcode scm platform get Github --json
43
+ pingcode scm platform create --name "Gitea (internal)" --type other --dry-run --json
44
+ pingcode scm platform create --name "Gitea (internal)" --type other --description "self-hosted" --json
45
+ pingcode scm platform update Github --description "github.com" --json
46
+ ```
47
+
48
+ `--type` is a closed enum used to pick an icon:
49
+ `github | gitlab | bitbucket | coding.net | gogs | git | svn | gerrit | other`. A value outside
50
+ it is rejected by the server (exit 7), not by the CLI.
51
+
52
+ A platform **name is unique per organisation**; creating a duplicate is exit 7 with
53
+ `'product'已经存在`.
54
+
55
+ ### Git identities — `scm platform-user`
56
+
57
+ ```bash
58
+ pingcode scm platform-user list --platform Github --json
59
+ pingcode scm platform-user list --platform Github --name octocat --json
60
+ pingcode scm platform-user get 685c6ca42974f854bb4979ac --platform Github --json
61
+ pingcode scm platform-user create --platform Github --name octocat --display-name "Octo Cat" \
62
+ --html-url https://github.com/octocat --json
63
+ pingcode scm platform-user update 685c6ca42974f854bb4979ac --platform Github --display-name "Octo" --json
64
+ ```
65
+
66
+ **A platform user is a git author identity, not a PingCode member, and there is no field that
67
+ links it to one.** The resource is exactly `{id, name, display_name, html_url, avatar_url}` —
68
+ no `user`, no `user_id`, no `email`, on read or on write. Attribution is by the **name
69
+ string**: a commit's `committer_name` and a branch's `sender_name` are matched against these
70
+ rows. So do not promise a user that the CLI can "assign a commit to a person"; what it can do
71
+ is make sure the git username exists as an identity with a readable display name and avatar.
72
+
73
+ ⚠️ **Five write fields silently CREATE one of these rows when the name is unknown**, and
74
+ **there is no DELETE for a platform user anywhere in scm**, so every typo is permanent:
75
+
76
+ | field | flag | upserts? |
77
+ |---|---|---|
78
+ | repository `owner_name` | `scm repo create --owner-name` | **yes** |
79
+ | branch `sender_name` | `scm branch create --sender` | **yes** |
80
+ | pull request `creator_name` | `scm pr create/update --creator` | **yes** |
81
+ | pull request `merged_by_name` | `scm pr create/update --merged-by` | **yes** |
82
+ | code review `reviewer_name` | `scm review create/update --reviewer` | **yes** |
83
+ | commit `committer_name` | `scm commit create --committer` | **no** — that path has no platform to create one in |
84
+
85
+ All five upserts are verified live. Check the name before a write:
86
+ `scm platform-user list --platform <p> --name <username> --json` returns zero rows if it
87
+ does not exist yet.
88
+
89
+ `get` and `update` take an **id**, because ids in this API have three shapes and are never
90
+ guessed. Turn a git username into an id with the exact-match filter above
91
+ (`list --name octocat`), which is why no `resolve` kind exists for this resource.
92
+
93
+ ### Repositories — `scm repo`
94
+
95
+ ```bash
96
+ pingcode scm repo list --platform Github --json
97
+ pingcode scm repo list --platform Github --full-name acme/pingcode-cli --json
98
+ pingcode scm repo list --platform Github --all --limit 200 --json
99
+
100
+ pingcode scm repo get pingcode-cli --platform Github --json
101
+ pingcode scm repo get acme/pingcode-cli --platform Github --json
102
+
103
+ pingcode scm repo create --platform Github --name pingcode-cli --full-name acme/pingcode-cli \
104
+ --owner-name octocat --private true \
105
+ --html-url https://github.com/acme/pingcode-cli \
106
+ --branches-url 'https://github.com/acme/pingcode-cli/tree/{branch}' \
107
+ --commits-url 'https://github.com/acme/pingcode-cli/commit/{sha}' --json
108
+
109
+ pingcode scm repo update acme/pingcode-cli --platform Github --private false --json
110
+ ```
111
+
112
+ Rules that will bite otherwise:
113
+
114
+ - **`full_name` (`owner/name`) is the unique key, `name` is not.** Two repositories in one
115
+ platform may share a name (a fork and its upstream); `scm repo get <name>` then exits 2 and
116
+ lists both ids, and the `full_name` is what disambiguates it.
117
+ - **`--full-name` is the only list filter.** The API ignores a `name` query parameter and
118
+ returns every repository, so the CLI does not offer one.
119
+ - **`--owner-name` creates the identity if it does not exist.** An unknown git username is not
120
+ rejected: the server makes a new platform user for it and points `owner` at it. A typo
121
+ therefore silently produces a ghost identity, and nothing in this API can delete it.
122
+ - **`--private` and `--fork` take `true` / `false`**, not bare switches, so a repository can be
123
+ made public again. Omit the flag to leave the field untouched.
124
+ - **The `*_url` values are templates stored verbatim** (`{branch}`, `{sha}`,
125
+ `{base}...{head}`, `{number}`). PingCode substitutes them when it renders a link; the CLI
126
+ never does. **Quote them** in a shell, or the braces may be eaten.
127
+
128
+ ### Branches — `scm branch`
129
+
130
+ ```bash
131
+ pingcode scm branch list --platform Github --repo acme/pingcode-cli --json
132
+ pingcode scm branch list --platform Github --repo acme/pingcode-cli --name feature/PLM-001 --json
133
+ pingcode scm branch list --platform Github --repo acme/pingcode-cli --work-item-id 5edca524cad2fa112b06105c --json
134
+
135
+ pingcode scm branch get feature/PLM-001 --platform Github --repo acme/pingcode-cli --json
136
+
137
+ pingcode scm branch create --platform Github --repo acme/pingcode-cli \
138
+ --name feature/PLM-001-login --sender octocat --work-item PLM-001 --json
139
+
140
+ pingcode scm branch update feature/PLM-001-login --platform Github --repo acme/pingcode-cli \
141
+ --work-item PLM-001 --work-item PLM-002 --json
142
+ pingcode scm branch update feature/PLM-001-login --platform Github --repo acme/pingcode-cli --default --json
143
+
144
+ pingcode scm branch delete feature/PLM-001-login --platform Github --repo acme/pingcode-cli --yes --json
145
+ ```
146
+
147
+ Rules that will bite otherwise:
148
+
149
+ - **`--sender` creates the git identity if it does not exist**, exactly like `repo
150
+ --owner-name`. An unknown username is not rejected — a platform user is made for it, and
151
+ **nothing in this API can delete a platform user**. Create it deliberately first
152
+ (`scm platform-user create`) and check the spelling.
153
+ - **`--default` is a switch, and there is no `--default false`.** On a patch the server
154
+ accepts *only* `true` — the field is really the action "make this the default". It also
155
+ **clears the flag on whichever branch currently holds it**, so one call changes two
156
+ branches. (Contrast `scm repo --private true|false`, which is genuinely three-state.)
157
+ - **The first branch in an empty repository becomes the default automatically**, even
158
+ though you did not ask.
159
+ - **`--work-item` takes an identifier (`PLM-001`), not an id — and an unknown one is
160
+ silently ignored.** The API returns 200 either way. The CLI compares what came back
161
+ against what you asked for and prints a `warning:` on stderr naming the identifiers that
162
+ did not link; the exit code stays 0, and under `--json` the authoritative answer is the
163
+ `work_items` array on stdout. **Read it.**
164
+ - **`--work-item` REPLACES the link set on update**, it does not add to it. Pass every
165
+ identifier you want to keep. There is no "clear" flag — repeat the command with the
166
+ links you want.
167
+ - **`--name` is a real filter here** (exact, case-insensitive), unlike on `repo list`.
168
+ Branch names are unique per repository, which is also why `scm branch get <name>` works
169
+ in one request and needs no `resolve` kind.
170
+
171
+ ### Deleting a branch — the one destructive command in scm
172
+
173
+ ```bash
174
+ # refuses without --yes, and names the branch it would delete
175
+ pingcode scm branch delete feature/old --platform Github --repo acme/pingcode-cli
176
+ pingcode scm branch delete feature/old --platform Github --repo acme/pingcode-cli --yes --dry-run --json
177
+ pingcode scm branch delete feature/old --platform Github --repo acme/pingcode-cli --yes --json
178
+ ```
179
+
180
+ **This is the only `DELETE` in the whole scm module** — 代码分支 is the one family shaped with
181
+ a `DELETE` and no `PUT`; the other five are the reverse. Three things to know before you
182
+ run it:
183
+
184
+ - **`--yes` is mandatory** and the refusal echoes the *resolved branch name*, not just what
185
+ you typed. There is deliberately **no `--all`**: bulk branch deletion is not offered.
186
+ - **The default branch cannot be deleted at all** (exit 7, `默认分支不能被删除`). Make another
187
+ branch the default first. A repository whose only branch is the default therefore has no
188
+ deletable branch.
189
+ - **Deleting a branch orphans its commit refs, permanently.** The refs keep resolving by
190
+ id, but `scm ref list --branch-id <the deleted branch>` then fails with a server error
191
+ (HTTP 500), and **refs have no delete**, so the broken state cannot be cleaned up. Delete
192
+ a branch only when you are sure nothing references it.
193
+
194
+ ### Commits — `scm commit`
195
+
196
+ ```bash
197
+ # by SHA — this is the point of the family: a pipeline has a SHA, not a PingCode id
198
+ pingcode scm commit get 96a024347146ebdc5f481f45e6e6871e0c43af5f --json
199
+ pingcode scm commit get 5e3bb2128cfda459bbafa3fb --json
200
+
201
+ pingcode scm commit list --sha 96a024347146ebdc5f481f45e6e6871e0c43af5f --json
202
+ pingcode scm commit list --work-item-id 5edca524cad2fa112b06105c --json
203
+
204
+ pingcode scm commit create --sha 96a024347146ebdc5f481f45e6e6871e0c43af5f \
205
+ --message "feat(auth): #PLM-001 add login" --committer octocat \
206
+ --committed-at 2026-08-03T09:00:00Z \
207
+ --added src/login.ts --modified README.md --removed src/old.ts \
208
+ --work-item PLM-001 --json
209
+ ```
210
+
211
+ - **These leaves take no `--platform` and no `--repo`.** A commit is an
212
+ organisation-level record; that is the API's shape, not an omission. Consequently
213
+ `commit list` with no filter **scans every commit in the organisation** — always pass
214
+ `--sha` or `--work-item-id`.
215
+ - **`get` accepts a full 40-character SHA or a PingCode id.** An **abbreviated SHA does not
216
+ work** (the server answers "resource path error", exit 5), even though every git tool
217
+ accepts one. Pass the full hash.
218
+ - **`--sha` is the one value this API validates for you**: a malformed SHA is exit 7 on
219
+ create, not a silent acceptance.
220
+ - **`--committer` does NOT create an identity.** This is the opposite of branch `--sender`:
221
+ the commit stores the name as a plain string and no platform user is made, so a typo
222
+ leaves the commit attributed to nobody rather than creating a ghost row. Fix it by
223
+ recreating the commit — but note a duplicate SHA is rejected, so the wrong one persists.
224
+ - **`--added` / `--removed` / `--modified` are repeatable** and all three are sent even when
225
+ empty. `file_changed_count` is computed by the server.
226
+ - There is **no `update` and no `delete`** for a commit.
227
+
228
+ ### Commit refs — `scm ref`
229
+
230
+ A ref is the record that says *this commit is on this branch*. Create the commit first.
231
+
232
+ ```bash
233
+ pingcode scm ref create --platform Github --repo acme/pingcode-cli \
234
+ --sha 96a024347146ebdc5f481f45e6e6871e0c43af5f --branch-id 564587fe700d43b81b080767 --json
235
+
236
+ pingcode scm ref list --platform Github --repo acme/pingcode-cli --branch-id 564587fe700d43b81b080767 --json
237
+ pingcode scm ref get 5e451b7dd704c212f7de8b4f --platform Github --repo acme/pingcode-cli --json
238
+ ```
239
+
240
+ - **`--branch-id` is required on `list`, and it takes an id, not a name.** The API's list
241
+ requires the referenced entity, so **there is no way to list every ref in a repository**
242
+ — you enumerate one branch at a time. Get the id from `scm branch list --json`.
243
+ - **`--sha` must name a commit that already exists** in PingCode, or you get exit 5
244
+ (`'commit'资源不存在`). Order: `scm commit create` → `scm ref create`.
245
+ - Only branches can be referenced; the CLI sends `meta_type=branch` for you.
246
+ - There is **no `update` and no `delete`**, and a ref outlives the branch it points at (see
247
+ the branch delete warning above).
248
+
249
+ ### Pull requests — `scm pr`
250
+
251
+ ```bash
252
+ pingcode scm pr list --platform Github --repo acme/pingcode-cli --json
253
+ pingcode scm pr list --platform Github --repo acme/pingcode-cli --number 42 --json
254
+ pingcode scm pr list --platform Github --repo acme/pingcode-cli --work-item-id 5edca524cad2fa112b06105c --json
255
+ pingcode scm pr list --platform Github --repo acme/pingcode-cli --all --limit 200 --json
256
+
257
+ pingcode scm pr get 594587fe700d43b81b080789 --platform Github --repo acme/pingcode-cli --json
258
+
259
+ pingcode scm pr create --platform Github --repo acme/pingcode-cli \
260
+ --title "feat(auth): #PLM-001 add login" --number 42 --creator octocat \
261
+ --target-branch-id 564587fe700d43b81b080776 --source-branch-id 564587fe700d43b81b080767 \
262
+ --status open --work-item PLM-001 --json
263
+
264
+ pingcode scm pr update 594587fe700d43b81b080789 --platform Github --repo acme/pingcode-cli \
265
+ --status merged --merged-at 2026-08-03T10:00:00Z \
266
+ --merged-commit-sha 96a024347146ebdc5f481f45e6e6871e0c43af5f --merged-by octocat --json
267
+ ```
268
+
269
+ Rules that will bite otherwise:
270
+
271
+ - **A pull request is addressed by its id, and *found* by its number.** scm has no
272
+ `identifier` and no `short_id` anywhere, and the detail path takes the 24-hex id only.
273
+ So `--number` is a **list filter**, and `pr get|update` take the id: run
274
+ `pingcode scm pr list --number 42 --json` and read `values[0].id`. The CLI does not
275
+ guess whether what you typed "looks like a number". `--number` is an **exact filter and
276
+ it really works** (verified live) — unlike `repo list`'s ignored `?name=`, so this
277
+ discovery path is reliable; an unused number returns zero rows.
278
+ - **`--status` is required by the API on every patch**, not just when you want to change
279
+ it. Omit it and the CLI reads the pull request first and re-sends its *current* status,
280
+ which costs one extra GET; pass `--status` and it does not. There is no way to send a
281
+ patch without a status (verified live: `100008 'status'是必填字段`). The patch is
282
+ genuinely partial otherwise — counts and work-item links you do not mention survive it.
283
+ - **Both `--target-branch-id` and `--source-branch-id` are required on create**, and both
284
+ take **branch ids**, from `scm branch list --json`. They must also **differ** (the
285
+ server refuses `source == target`). The published API reference marks the source branch
286
+ optional; the live API rejects a create without it, so the CLI requires it and you get
287
+ exit 2 naming the flag instead of a server error.
288
+ - **`--merged-at`, `--merged-commit-sha` and `--merged-by` become required when
289
+ `--status merged`.** The server enforces that, not the CLI, so expect exit 7 rather than
290
+ exit 2 if you forget one (verified live: `100212`).
291
+ - ⚠️ **`--creator` and `--merged-by` CREATE a 托管平台用户 if the name is unknown.** They are
292
+ git usernames matched against 托管平台用户 rows by name, and an unrecognised one is
293
+ **upserted**, exactly as branch `--sender` is. scm has **no user DELETE**, so a typo
294
+ leaves a permanent ghost identity in the tenant. Create or confirm the identity first
295
+ with `scm platform-user list --name <username>`. (A commit's `--committer` is the one
296
+ exception — it creates nothing.)
297
+ - **The six `--*-count` flags are yours to report.** Nothing server-side recomputes them.
298
+ Note that the server stores `0` for any count you do not send, so unlike `merged_at`
299
+ (which stays absent) an omitted count is indistinguishable from `0` once written.
300
+ - **`--work-item` takes an identifier (`PLM-001`) and an unknown one is silently
301
+ ignored**, exactly as on a branch: the response's `work_items` array is the only
302
+ evidence, the CLI warns on stderr about identifiers that did not link, and the exit code
303
+ stays 0. On `update` it **REPLACES** the whole link set. `--work-item-id` on `list` is a
304
+ different thing — a work item **id**, and an unknown one is an error (exit 5), not an
305
+ empty result.
306
+ - **`number` must be unique in the repository**; a duplicate is a server-side conflict
307
+ (exit 7), not a silent overwrite.
308
+ - **A missing pull request exits 5**, on `get`, on `update`, and on `review create`.
309
+
310
+ ### Code reviews — `scm review`
311
+
312
+ ⚠️ **`scm review` is not `pingcode api … /v1/reviews`.** Two unrelated resources share
313
+ the word:
314
+
315
+ | | `scm review` (this section) | `/v1/reviews` (generic layer only) |
316
+ |---|---|---|
317
+ | what | 代码评审: one review event on one pull request | 评审: a polymorphic review object with contents |
318
+ | addressed by | platform + repository + pull request | `principal_type` + `pilot_id` |
319
+ | used by | git / CI write-back | 需求 and 用例 review flows |
320
+ | reachable as | `pingcode scm review …` | `pingcode api GET /v1/reviews …` |
321
+
322
+ Their ids are not interchangeable. If you are recording "someone approved PR #42", you
323
+ want this section.
324
+
325
+ ```bash
326
+ pingcode scm review list --platform Github --repo acme/pingcode-cli \
327
+ --pr-id 594587fe700d43b81b080789 --json
328
+
329
+ pingcode scm review get 524587fe700d43b81b080988 --platform Github --repo acme/pingcode-cli \
330
+ --pr-id 594587fe700d43b81b080789 --json
331
+
332
+ pingcode scm review create --platform Github --repo acme/pingcode-cli \
333
+ --pr-id 594587fe700d43b81b080789 \
334
+ --status approved --reviewer octocat --submitted-at 2026-08-03T10:00:00Z \
335
+ --description "Review has approved" \
336
+ --html-url 'https://github.com/acme/pingcode-cli/pull/42#pullrequestreview-384383294' --json
337
+
338
+ pingcode scm review update 524587fe700d43b81b080988 --platform Github --repo acme/pingcode-cli \
339
+ --pr-id 594587fe700d43b81b080789 --status request_changes --json
340
+ ```
341
+
342
+ - **`--pr-id` is required on every leaf, and it takes an id, not a number.** A review is
343
+ addressed three parents deep, and **there is no repository-wide or organisation-wide
344
+ review list** — you enumerate one pull request at a time, exactly as with `scm ref` and
345
+ branches. Get the id from `scm pr list --number <n> --json`.
346
+ - ⚠️ **A wrong `--pr-id` reads as "no reviews", not as an error.** `review list` against a
347
+ pull request that does not exist returns an **empty list with exit 0** — the pull request
348
+ is the one scm parent whose absence a child *list* does not report (a bad `--platform` or
349
+ `--repo` does fail). So an empty result means "either no reviews, or that pull request is
350
+ not there". Confirm with `scm pr get <id>` if it matters. `review get` and
351
+ `review create` do fail properly, with exit 5.
352
+ - ⚠️ **`--reviewer` CREATES a 托管平台用户 if the name is unknown**, exactly like
353
+ `pr create --creator` and `branch create --sender`, and platform users cannot be
354
+ deleted. Confirm the username first with `scm platform-user list --name <username>`.
355
+ - **A missing review exits 5** — including a real review id passed with the wrong
356
+ `--pr-id`, because the review genuinely is not at that address.
357
+ - **`--submitted-at` is required on create.** A review carries **no server-assigned
358
+ timestamp at all** — no `created_at`, no `updated_at` — so the time is yours to supply.
359
+ On `update` it is optional like everything else: this PATCH has no mandatory field
360
+ (unlike `scm pr update`).
361
+ - **`--status` is `comment` / `approved` / `request_changes`.** A value outside the enum
362
+ is rejected by the server (exit 7), not by the CLI.
363
+ - **`--reviewer` is a git username**, attribution by name again.
364
+ - **`--html-url` is optional**; without it PingCode shows no jump link back to the
365
+ hosting platform. **Quote it** in a shell — review URLs contain a `#`.
366
+
367
+ ### Name → id
368
+
369
+ ```bash
370
+ pingcode resolve scm-platform Github --json
371
+ pingcode resolve scm-repo pingcode-cli --parent 68393e8b47512a5d5d4e5b55 --json
372
+ ```
373
+
374
+ Both are cached for 24 h under `~/.pingcode/cache/`; pass `--no-cache` when a platform was
375
+ reconfigured. `--platform <name|id>` resolves by name; `--platform-id <id>` is sent verbatim
376
+ with no lookup, and the two are mutually exclusive (exit 2).
377
+
378
+ ### What cannot be deleted, and why nothing is `replace`d
379
+
380
+ **`scm branch delete` is the only delete in this module.** For everything else — platforms,
381
+ git identities, repositories, commits, refs, **pull requests and code reviews** —
382
+ **no DELETE exists upstream**. Nothing you create there can ever be removed through the API, so
383
+ mark test data clearly and check spellings before you write. A mistyped pull request title
384
+ can be patched; a pull request created against the wrong repository or with the wrong
385
+ `--number` **cannot be withdrawn**. `--owner-name` and branch `--sender` are the two flags
386
+ that can create a row by accident.
387
+
388
+ **`PUT` is deliberately not offered anywhere.** Five scm families document a `PUT` that
389
+ replaces the whole record and blanks every field you did not send, and this API never
390
+ documents what clearing a field does. Use `update` (PATCH). **Full replacement of a pull
391
+ request or a code review therefore goes through `pingcode api PUT`** — and there, omitting
392
+ a field is not "leave it alone", it is a wipe. If you truly want a full replacement, ask
393
+ for it explicitly through the escape hatch:
394
+
395
+ ```bash
396
+ pingcode api PUT /v1/scm/products/68393e8b47512a5d5d4e5b55 \
397
+ --set name=Github --set type=github
398
+
399
+ pingcode api PUT /v1/scm/products/<platform>/repositories/<repo>/pull_requests/<pr> \
400
+ --set title=… --set creator_name=… --set source_branch_id=… --set target_branch_id=… --set status=open
401
+ ```
402
+
403
+ See [`api.md`](api.md).
404
+
405
+ ⚠️ **代码分支 has no `PUT` upstream at all** — its fifth verb is `DELETE`, which is why
406
+ `scm branch delete` exists while its siblings have none. So there is nothing missing to
407
+ "complete": do not add a `scm branch replace`, and do not expect `scm platform delete` to
408
+ appear. All six documented scm families are now refined commands; the only part of
409
+ `/v1/scm/**` that is generic-layer-only is those five `PUT`s.
410
+
411
+ ### Errors you should expect
412
+
413
+ | Situation | Exit |
414
+ |---|---|
415
+ | `--platform` missing, or given together with `--platform-id` | 2 |
416
+ | a name matches nothing, or matches two repositories | 2 |
417
+ | `update` with no field to change | 2 |
418
+ | `--private maybe` | 2 |
419
+ | the token lacks `pcp:*:devops:code` | 4 |
420
+ | a platform / repository / identity / branch / commit / ref id does not exist | 5 |
421
+ | `scm ref create` names a `--sha` or `--branch-id` that does not exist | 5 |
422
+ | an abbreviated SHA passed to `scm commit get` | 5 |
423
+ | `--branch-id` missing on `scm ref list` | 2 |
424
+ | `scm branch delete` without `--yes` | 2 |
425
+ | deleting the **default** branch | 7 |
426
+ | a `--type` outside the enum, or a duplicate name / SHA / ref | 7 |
427
+ | `scm ref list` for a branch that was deleted (server error) | 7 |
428
+ | `--pr-id` missing on any `scm review` leaf | 2 |
429
+ | a non-numeric `--number`, or a non-date `--merged-at` / `--submitted-at` | 2 |
430
+ | a pull request or code review id that does not exist | 5 |
431
+ | `--status merged` without `--merged-at` / `--merged-commit-sha` / `--merged-by` | 7 |
432
+ | a `--status` outside its enum, or a duplicate pull request `--number` | 7 |
@@ -0,0 +1,197 @@
1
+ # 产品管理 (ship) — `product`
2
+
3
+ > Part of the `pingcode` skill. Read [`../SKILL.md`](../SKILL.md) first — it carries the
4
+ > authentication gate, the stdout/stderr contract, the exit-code table and the rules that
5
+ > apply to every module. This file is only the ship command surface plus the ship-specific traps.
6
+
7
+ ### Products — 产品管理
8
+
9
+ A **product** (产品) is ship's parent scope, the way a project is pjm's. Resolve it first: every other
10
+ ship id is scoped to it.
11
+
12
+ ```bash
13
+ pingcode product list --json
14
+ pingcode product list --keywords sales --json
15
+ pingcode product get SLC --json # name, identifier such as SLC, or id
16
+ ```
17
+
18
+ `--keywords` searches the **name only** — the identifier is not searchable server-side, so the CLI
19
+ matches it client-side over the full list. There is no `product create`/`update`/`delete`: ship has no
20
+ product DELETE at all, and `PATCH` only edits three cosmetic fields.
21
+
22
+ ### `product meta` — mandatory before writing an idea or ticket
23
+
24
+ ```bash
25
+ pingcode product meta idea-states --product SLC --json
26
+ pingcode product meta idea-priorities --product SLC --json
27
+ pingcode product meta idea-suites --product SLC --json
28
+ pingcode product meta idea-properties --product SLC --json
29
+ pingcode product meta idea-plans --product SLC --json
30
+ pingcode product meta members --product SLC --json
31
+ pingcode product meta ticket-states --product SLC --json
32
+ pingcode product meta ticket-priorities --product SLC --json
33
+ pingcode product meta ticket-types --product SLC --json
34
+ pingcode product meta ticket-channels --product SLC --json
35
+ pingcode product meta ticket-properties --product SLC --json
36
+ ```
37
+
38
+ `product meta members` is the **only** valid source of `--assignee` values for ideas and tickets —
39
+ the organisation directory is not, because a user who is not a member of the product cannot be
40
+ assigned. `product meta idea-properties` / `product meta ticket-properties` are the only source of `--set` keys and,
41
+ for select-typed properties, of the option ids you must send instead of the display label.
42
+ `product meta idea-plans` is the only source of `plan_id` values — read the 需求排期 section below
43
+ before you use the word "plan" anywhere near this API.
44
+
45
+ ### Requirements 需求 — `product idea`
46
+
47
+ ```bash
48
+ pingcode product idea list --product SLC --json
49
+ pingcode product idea list --product SLC --state 待评审 --assignee zhangsan --json
50
+ pingcode product idea list --product SLC --keywords sso --page-size 20 --page 0 --json
51
+ pingcode product idea list --product SLC --all --limit 200 --json
52
+
53
+ pingcode product idea get SLC-1 --json # identifier, id, or a pasted idea URL
54
+
55
+ pingcode product idea create --product SLC --title "Single sign-on" --dry-run --json
56
+ pingcode product idea create --product SLC --title "Single sign-on" \
57
+ --assignee zhangsan --priority P1 --suite "客户端 / 登录" --json
58
+
59
+ pingcode product idea update SLC-1 --title "Single sign-on (v2)" --json
60
+ pingcode product idea update SLC-1 --state 开发中 --json
61
+ pingcode product idea update SLC-1 --set 需求类型=5cb7e763fda1ce4ca0010002 --json
62
+ ```
63
+
64
+ `product idea list` is `POST /v1/ship/ideas/search` — the plain list endpoint cannot filter by assignee, date
65
+ or custom property, so the CLI never uses it. Note there is **no `--type`** anywhere on `idea`: ship
66
+ states are scoped to the product alone, which `--product` (or, on `update`, the idea itself) already
67
+ supplies.
68
+
69
+ ### State history 流转记录 — `product idea history`
70
+
71
+ ```bash
72
+ pingcode product idea history list SLC-1 --json
73
+ pingcode product idea history list SLC-1 --all --json
74
+ pingcode product idea history get SLC-1 6a1cd3670faf359d7447bf37 --json
75
+ ```
76
+
77
+ **State changes only.** A title, assignee, priority or 排期 change is *not* here — that is
78
+ `pingcode product idea activity`, the free-form audit feed. Every requirement has exactly one row
79
+ from creation, printed with `FROM` as `(new)`. Read-only upstream (a POST or DELETE on this path
80
+ answers HTTP 405), and the endpoint accepts `?name=`, `?state_id=` and `?keywords=` while **ignoring
81
+ all three** — which is why the CLI offers no filter flag here. Filter client-side after `--all`.
82
+
83
+ Unlike some sibling lists in this API, this one validates its parent: an empty result really means
84
+ "this requirement has no rows", and a bad reference exits 5. A history id that belongs to a
85
+ *different* requirement also exits 5 — the (requirement, record) pair is the address, and the CLI
86
+ resolves your `SLC-1` to an id first because the raw endpoint accepts nothing else.
87
+
88
+ ### Requirement schedules 需求排期 — `product plan`
89
+
90
+ ```bash
91
+ pingcode product plan list --product SLC --json
92
+ pingcode product plan list --product SLC --all --limit 200 --json
93
+ pingcode product plan get 6a1c53580faf359d7447b68e --product SLC --json
94
+ ```
95
+
96
+ **Read-only, and permanently so**: `POST`, `PATCH` and `DELETE` on the schedule path all answer
97
+ HTTP 405, so there is nothing to reach through `pingcode api` either — schedules are created in the
98
+ web UI. There is **no filter flag**, for the same reason as above: the endpoint documents none and an
99
+ undeclared `?name=` changed nothing when tried.
100
+
101
+ `product plan list` returns the full record (name, assignee, start, end); `product meta idea-plans`
102
+ returns the same rows as `{id, name}` only, because ship answers **two structures for one resource**
103
+ depending on the endpoint. Use `plan list` to read a schedule, `meta idea-plans` to pick an id for
104
+ `product idea update --plan-id`.
105
+
106
+ A schedule id that does not exist exits **7**, not 5 — read the message rather than the code. The
107
+ vendor code is the same one an idea PATCH answers for an unknown `--plan-id`, and whether it can
108
+ additionally mean "exists, but in another product" has not been measurable on any tenant reached so
109
+ far (no tenant has had a single 排期 yet).
110
+
111
+ ### Tickets 工单 — `product ticket`
112
+
113
+ ```bash
114
+ pingcode product ticket list --product SLC --json
115
+ pingcode product ticket list --product SLC --type 故障 --state 待处理 --json
116
+ pingcode product ticket list --product SLC --channel 邮件 --all --limit 200 --json
117
+
118
+ pingcode product ticket get SLC-7 --json
119
+
120
+ pingcode product ticket create --product SLC --type 故障 --title "Cannot log in" --dry-run --json
121
+ pingcode product ticket create --product SLC --type 故障 --title "Cannot log in" \
122
+ --assignee zhangsan --priority P1 --channel 邮件 --json
123
+
124
+ pingcode product ticket update SLC-7 --title "Cannot log in (iOS)" --json
125
+ pingcode product ticket transition SLC-7 --state 处理中 --json
126
+ ```
127
+
128
+ `--type` is **required** on `product ticket create` — `type_id` is a required body field, which is the one
129
+ place ship demands a lookup (`pingcode product meta ticket-types --product SLC`) before a write can even be
130
+ attempted. `--channel` can only be set at create time; there is no way to change it afterwards.
131
+
132
+ ## 4b. Ship rules that will bite you
133
+
134
+ These are on top of §4, which still applies. Ship is a different module with the same machinery, and
135
+ almost every difference is a trap.
136
+
137
+ 1. **Resolve the product first, and scope everything to it.** A product is to ship what a project is
138
+ to pjm. `state_id`, `priority_id`, `suite_id`, `type_id`, `channel_id`, the `properties` keys and
139
+ the assignable people are **all product-scoped**. They frequently *look* org-global — the same
140
+ priority id `P0` appears under several products — but the API requires `product_id` on every
141
+ lookup, so never carry an id from one product to another.
142
+ 2. **`--assignee` must be a product member.** `pingcode product meta members --product <p>` is the
143
+ only valid candidate set; the organisation directory (`settings users`) is not, and a non-member is
144
+ rejected.
145
+ 3. **`--state <name>` needs no companion flag here.** Unlike pjm, ship states hang off the product
146
+ alone, so there is no `--type` on `idea` at all, and `--type` on `ticket` is a real field being
147
+ written, not a lookup aid. `--state` and `--state-id` remain mutually exclusive.
148
+ 4. **Reads go through `search`.** `product idea list` and `product ticket list` are `POST …/search`. The plain list
149
+ endpoints exist but cannot filter by assignee, date or custom property, so the CLI never uses
150
+ them. Search takes **one operator per field and has no `$and`/`$or`**; multiple filters are
151
+ AND-ed. There is still no sorting anywhere.
152
+ 5. **No state change is refused locally — the server decides, and a ticket refusal is explained.**
153
+ - `pingcode product ticket transition` and `product ticket update --state` send the PATCH. If the server refuses
154
+ it, the error `message` names the product's configured states, the current state and — when
155
+ the state plan can be read — **the states reachable from the current one**. Read it from
156
+ `message`: `--json` errors are `{kind,message,code,exit}` and carry no hint.
157
+ - Want to know before you write? `product ticket transition <t> --state <s> --dry-run` prints the
158
+ reachable set on stderr and sends nothing.
159
+ - `pingcode product idea update --state` gets the configured states on rejection but never a reachable
160
+ set: ship publishes no idea state-flow endpoint at all.
161
+ - The CLI does **not** refuse a transition on its own (the one exception: moving a ticket to the
162
+ state it is already in, which is exit 2). The server refuses atomically with no state change,
163
+ so a local check saves nothing — and a mis-identified state plan would otherwise block a legal
164
+ move with no escape hatch. Expect the server's exit code, not exit 2, for an illegal target.
165
+ 6. **`--set key=value` sends the value verbatim, and select properties want option ids.** For a
166
+ `select`-typed property the API expects the option's `_id`, not the label you see in the UI —
167
+ the docs' own examples only show text properties, which is the trap. Run
168
+ `pingcode product meta idea-properties --product <p>` (or `ticket-properties`): it prints each key and
169
+ its `label=option_id` pairs. `properties` **replaces**, it never merges.
170
+ 7. **Nothing in ship can be deleted.** There is no DELETE for products, ideas or tickets, and
171
+ `is_archived` / `is_deleted` are read-only. A test artifact you create is permanent — mark it in
172
+ the title (for example `[CLI smoke] …`) before you create it, not after.
173
+ 8. **An identifier works on the resource, and nowhere below it.** `GET /v1/ship/ideas/<x>` accepts
174
+ the id, the 8-char `short_id` a pasted URL ends in **and** the human `SLC-1` — all three answered
175
+ 200 live, correcting an earlier note here that said none of them did. But every *sub-collection* —
176
+ `history`, and the `relation` / `comment` / `attachment` / `activity` families — takes the 24-hex
177
+ id only and answers HTTP 404 `资源路径错误` for anything else. The CLI therefore resolves your
178
+ reference to an id first (one extra request when the identifier has a dashed product prefix, or a
179
+ `search` hop when it does not), so all four forms work at the command layer.
180
+ 9. **`--suite` filtering on `product idea list` is undocumented.** The API lists `suite.id` as neither
181
+ filterable nor unfilterable, so an empty result proves nothing. The CLI warns when you use it.
182
+ 10. **"Plan" is three unrelated things, and only one of them is a 排期.** Getting this wrong is the
183
+ fastest way to hand a valid id to the wrong endpoint and get a not-found you cannot explain:
184
+ | You mean | Command | What it is |
185
+ |---|---|---|
186
+ | 需求排期 requirement schedule | `pingcode product plan list --product <p>` | a named window a requirement is planned into; `idea update --plan-id` takes its id |
187
+ | 测试计划 test plan | `pingcode testhub plans list --library <l>` | a test cycle in a test library, with its own states and runs |
188
+ | 配置方案 configuration scheme | `pingcode api GET /v1/ship/ticket_state_plans` | a *scheme* — a reusable bundle of states/properties/transitions bound to products |
189
+ They share no ids and no vocabulary. The 排期 is the only one that is read-only for the whole
190
+ surface, and the configuration schemes are the only one with **no leaf you can type**. Note that
191
+ "no leaf" is not "not wired": `ticket_state_plans` and its `ticket_state_flows` child *are*
192
+ called, by the resolver cache, so that `ticket transition` can tell you which states are
193
+ reachable when the server refuses one. So they count towards README's 158 refined endpoints even
194
+ though no command bears their name — which is why that table is labelled *the refined layer*.
195
+ 11. **Tags cannot be set through the API** on ideas or tickets, and `submitter_id` on a ticket is
196
+ silently ignored under a client-credentials token — the ticket is attributed to the token owner
197
+ with no error. The CLI exposes neither.