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.
- package/README.md +943 -0
- package/dist/bin/pingcode.js +17001 -0
- package/dist/bin/pingcode.js.map +1 -0
- package/package.json +51 -0
- package/skills/pingcode/SKILL.md +431 -0
- package/skills/pingcode/modules/api.md +116 -0
- package/skills/pingcode/modules/cicd.md +222 -0
- package/skills/pingcode/modules/crosscutting.md +168 -0
- package/skills/pingcode/modules/pjm.md +385 -0
- package/skills/pingcode/modules/scm.md +432 -0
- package/skills/pingcode/modules/ship.md +197 -0
- package/skills/pingcode/modules/testhub.md +386 -0
package/README.md
ADDED
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
# pingcode-cli
|
|
2
|
+
|
|
3
|
+
[](https://github.com/anine09/pingcode-cli-unofficial/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
A command-line client for the [PingCode Open API](https://open.pingcode.com/), plus a single
|
|
6
|
+
`pingcode` skill that teaches AI agents how to drive it.
|
|
7
|
+
|
|
8
|
+
Scope, in two tiers: **every one of the 459 documented `/v1` endpoints is reachable** through the
|
|
9
|
+
generic executor `pingcode api`, and **158 of them also have named commands** with flag validation,
|
|
10
|
+
name→id resolution and tables — projects and work items, ship products/ideas/tickets, testhub
|
|
11
|
+
libraries/cases/plans/runs, the SCM and CI/CD write-back surface, and the cross-object
|
|
12
|
+
relations/comments/attachments/activities that link them. Wiki has **no** named command (0 of 19
|
|
13
|
+
endpoints), and neither do the org chart beyond `settings users`, worklogs, permission views or
|
|
14
|
+
Nexus; they are reachable through `pingcode api` only. Flow and Insight have no REST API at all.
|
|
15
|
+
See [Coverage](#coverage-reach-vs-ergonomics) for the per-module split.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Requires **Node.js >= 20**. The package is not published to npm; release zips
|
|
22
|
+
ship their own `node_modules/` (`commander`, `picocolors`), so an installed
|
|
23
|
+
binary needs only Node — no `npm` on the client. Build from a checkout for development.
|
|
24
|
+
|
|
25
|
+
### One-click (recommended)
|
|
26
|
+
|
|
27
|
+
From the repo checkout, one command installs deps, builds, and links `pingcode` onto your
|
|
28
|
+
`PATH` — compatible with Linux, macOS, and Windows. Re-run it after `git pull` to rebuild +
|
|
29
|
+
relink the latest code:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
./install.sh # Linux / macOS (or: npm run install:cli)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
.\install.ps1 # Windows PowerShell (or: npm run install:cli)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The Windows variant needs PowerShell (`pwsh` or Windows PowerShell); the `install:cli` npm
|
|
40
|
+
script is the cross-platform fallback (`node scripts/install.mjs`).
|
|
41
|
+
|
|
42
|
+
### Manual
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install
|
|
46
|
+
npm run build # → dist/bin/pingcode.js
|
|
47
|
+
npm link # then just: pingcode --help
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Development commands:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run typecheck # tsc --noEmit
|
|
54
|
+
npm test # vitest run — no network, ever
|
|
55
|
+
npm run dev # tsup --watch
|
|
56
|
+
npm run skill:install # copy skills/pingcode/SKILL.md to the agent skill dirs
|
|
57
|
+
npm run scan:secrets # credential / tenant-identifier scan
|
|
58
|
+
npm run check:commits # commit-message gate
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Get credentials
|
|
64
|
+
|
|
65
|
+
The CLI authenticates as an **application**, not as a user, using the OAuth
|
|
66
|
+
`client_credentials` grant.
|
|
67
|
+
|
|
68
|
+
1. In the PingCode enterprise console open **后台管理 (企业后台) → 凭据管理** ("Credential
|
|
69
|
+
Management") and create an application.
|
|
70
|
+
2. Set 鉴权方式 (grant type) to **Client Credentials**.
|
|
71
|
+
3. Grant the scopes the commands you intend to use need. This table is the same list
|
|
72
|
+
`skills/pingcode/SKILL.md` §1 gives an agent; the first four cover the smallest useful surface:
|
|
73
|
+
|
|
74
|
+
| Scope | Needed for |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `pcp:read:pjm:project` | `project list` / `get` / `progress`, `project member …`, and every project-name lookup |
|
|
77
|
+
| `pcp:write:pjm:project` | `project create` / `update`, `project member add`. Grant deliberately: **a project can never be deleted or archived** through this API |
|
|
78
|
+
| `pcp:read:pjm:workitem` | `project work-item list` / `get` / `history …`, `project meta types` / `states` / `priorities` / `relation-types` / `tags` |
|
|
79
|
+
| `pcp:write:pjm:workitem` | `project work-item create` / `update` / `transition` / `bulk-update` / `delete`, plus `link …` and `tag …` |
|
|
80
|
+
| `pcp:read:global:team` | `settings users`, and every `--assignee` / `--executor` that resolves against the org directory |
|
|
81
|
+
| `pcp:read:pjm:sprint` | `project meta sprints` (which is the sprint *list*) and `project sprint get` |
|
|
82
|
+
| `pcp:write:pjm:sprint` | `project sprint create` / `update` / `bulk-create`. **A sprint can never be deleted** |
|
|
83
|
+
| `pcp:read:pjm:release` | `project version list` / `get`. Note the mismatch: the scope says *release*, the command says *version* |
|
|
84
|
+
| `pcp:write:pjm:release` | `project version create` / `update` / `delete` / `bulk-create` |
|
|
85
|
+
| `pcp:read:ship:product` | `product list` / `get`, `product plan …`, `product meta members`, and every product-name lookup |
|
|
86
|
+
| `pcp:read:ship:idea` | `product idea list` / `get` / `history …`, `product meta idea-*` |
|
|
87
|
+
| `pcp:write:ship:idea` | `product idea create` / `update` |
|
|
88
|
+
| `pcp:read:ship:ticket` | `product ticket list` / `get`, `product meta ticket-*` |
|
|
89
|
+
| `pcp:write:ship:ticket` | `product ticket create` / `update` / `transition` |
|
|
90
|
+
| `pcp:read:ship:configuration` | optional — only the state-plan *explanation* in `product ticket transition`; without it the CLI warns and lets the server judge |
|
|
91
|
+
| `pcp:read:testhub:library` | `testhub libraries list` / `get`, `testhub meta suites`, and the case-module (模块) tree behind `--suite` |
|
|
92
|
+
| `pcp:write:testhub:library` | `testhub libraries create` — grant it only if you mean to create libraries; they cannot be deleted |
|
|
93
|
+
| `pcp:read:testhub:testcase` | `testhub cases list` / `get` / `history list`, `testhub meta case-types` / `case-properties` |
|
|
94
|
+
| `pcp:write:testhub:testcase` | `testhub cases create` / `update` / `bulk-create` / `bulk-update` / `delete` |
|
|
95
|
+
| `pcp:read:testhub:testplan` | `testhub plans list` / `get`, `testhub runs list` / `history …`, `testhub meta plan-types` / `plan-states` |
|
|
96
|
+
| `pcp:write:testhub:testplan` | `testhub plans create` / `update`, `testhub runs create` / `patch` / `bulk*` |
|
|
97
|
+
| `pcp:read:testhub:configuration` | **not optional** — `testhub meta case-states` / `run-statuses` / `important-levels`, i.e. every `state_id`, `status_id` and `important_level_id` |
|
|
98
|
+
| `pcp:read:devops:code` | `scm platform` / `platform-user` / `repo` / `branch` / `commit` / `ref` / `pr` / `review` reads, and every platform/repo name lookup |
|
|
99
|
+
| `pcp:write:devops:code` | every `scm … create` / `update`, and `scm branch delete` |
|
|
100
|
+
| `pcp:read:devops:build` | `build list` / `get` |
|
|
101
|
+
| `pcp:write:devops:build` | `build create` / `update` / `delete`. **Separate from `devops:code`** — a token that can write commits cannot write builds, and the only symptom is exit 4 |
|
|
102
|
+
| `pcp:read:devops:deploy` | `release env list` / `get` **and** `release deploy list` / `get` — one pair covers both subgroups |
|
|
103
|
+
| `pcp:write:devops:deploy` | `release env create` / `update`, `release deploy create` / `update` |
|
|
104
|
+
|
|
105
|
+
Every ship command begins by resolving a product name, so `pcp:read:ship:product` is required
|
|
106
|
+
even for a pure `product idea list`. The product-scoped metadata endpoints (`/v1/ship/idea/*`,
|
|
107
|
+
`/v1/ship/ticket/*`) sit under the ordinary read scopes above, **not** under `configuration`.
|
|
108
|
+
|
|
109
|
+
Testhub is the same story with a sharper edge: every testhub command begins by resolving a test
|
|
110
|
+
library, so `pcp:read:testhub:library` is required even for a pure `testhub cases list` — and
|
|
111
|
+
`pcp:read:testhub:configuration` is *not* optional despite the name, because `case/states` and
|
|
112
|
+
`run/statuses` live behind it while their sibling `case/types` does not. A token without it can
|
|
113
|
+
list cases, plans and runs but cannot resolve a `status_id`, and `PATCH /runs/{id}` requires one,
|
|
114
|
+
so it cannot write a run at all. `--executor` on a run and `--assignee` on a plan both resolve
|
|
115
|
+
through the organisation directory, so they also need `pcp:read:global:team`.
|
|
116
|
+
|
|
117
|
+
The whole DevOps area (`scm`, `build`, `release`) is **企业令牌 only**, which is exactly what
|
|
118
|
+
`client_credentials` yields — no extra grant type is needed, only the six `devops:*` scopes.
|
|
119
|
+
|
|
120
|
+
The 15 cross-object endpoints behind `relation` / `comment` / `attachment` / `activity` declare
|
|
121
|
+
**no scope at all** in the vendor docs, and they work with the scopes above; a 403 from one of
|
|
122
|
+
them would be a documentation bug, not a missing grant.
|
|
123
|
+
|
|
124
|
+
`pingcode api describe <id>` prints the scope the docs declare for any endpoint, so a 403 through
|
|
125
|
+
the generic layer names the scope it wants instead of leaving you guessing.
|
|
126
|
+
|
|
127
|
+
4. Copy the `client_id` and `client_secret`.
|
|
128
|
+
|
|
129
|
+
> **A `client_credentials` token carries organisation-wide system-administrator authority** and is
|
|
130
|
+
> not tied to any user. Treat it, and `~/.pingcode/config.json`, as a secret.
|
|
131
|
+
|
|
132
|
+
## Log in
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# public cloud (default host: https://open.pingcode.com)
|
|
136
|
+
pingcode auth login --client-id <id> --client-secret <secret> --save
|
|
137
|
+
|
|
138
|
+
# self-hosted: pass your instance host, the API is served from <host>/open
|
|
139
|
+
pingcode auth login --host https://pingcode.example.com \
|
|
140
|
+
--client-id <id> --client-secret <secret> --save
|
|
141
|
+
|
|
142
|
+
pingcode auth status --check # adds one live call: GET /v1/pjm/projects?page_size=1
|
|
143
|
+
pingcode auth logout # drops the token, the credentials and the metadata cache
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Credentials resolve in this precedence order: **CLI flags → environment
|
|
147
|
+
(`PINGCODE_CLIENT_ID`, `PINGCODE_CLIENT_SECRET`, `PINGCODE_HOST`) → `~/.pingcode/config.json`**.
|
|
148
|
+
With a TTY attached, `auth login` prompts for anything missing.
|
|
149
|
+
|
|
150
|
+
- `--save` is what persists the client id/secret. Without it only the token is stored, so you must
|
|
151
|
+
log in again when it expires.
|
|
152
|
+
- Storage is `~/.pingcode/config.json`, mode `0600` inside a `0700` directory (a no-op on Windows).
|
|
153
|
+
`PINGCODE_CONFIG_DIR` relocates it.
|
|
154
|
+
- Tokens are valid ~30 days and are re-acquired **transparently**: proactively 120 s before expiry,
|
|
155
|
+
and reactively once on a `401`, replaying the original request. You never have to re-run `login`
|
|
156
|
+
while the credentials are stored.
|
|
157
|
+
- Repeated logins do **not** invalidate previously issued tokens, so parallel invocations are safe.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Coverage: reach vs ergonomics
|
|
162
|
+
|
|
163
|
+
The API has **459** documented `/v1` endpoints and no OpenAPI spec. This CLI answers that with two
|
|
164
|
+
layers whose costs are completely different, and it is worth knowing which one you are standing on.
|
|
165
|
+
|
|
166
|
+
| Layer | What you get | Coverage | Cost of adding an endpoint |
|
|
167
|
+
|---|---|---|---|
|
|
168
|
+
| **Reach** — `pingcode api` | one generic executor over a vendored endpoint catalog: real auth, paging, `--dry-run`, redaction, exit codes, pre-flight validation | **459 / 459** | zero — it is already there |
|
|
169
|
+
| **Ergonomics** — the refined layer | `--flags` instead of raw JSON, name→id resolution, width-aware tables, per-endpoint traps recorded in `--help` | **158 / 459** | one live-verified slice each |
|
|
170
|
+
|
|
171
|
+
"Complete" (完全体) refers to **Reach**, and Reach is finished: every documented endpoint is
|
|
172
|
+
invocable today. Seven of the 459 are refused *before any request* because they need a user token
|
|
173
|
+
this CLI cannot obtain (`/v1/myself`, `/v1/permission/my/*`, `/v1/permission/check/*` — the
|
|
174
|
+
authorization-code flow is not implemented), which leaves 452 actually callable. Ergonomics is a
|
|
175
|
+
**curation backlog, not a finish line**: an endpoint earns a named command by being run against a
|
|
176
|
+
live tenant, having its error codes either mapped with evidence or explicitly left alone, and
|
|
177
|
+
keeping `--json` pure and `--dry-run` silent. Endpoints that nobody drives interactively are better
|
|
178
|
+
served by the generic layer than by a hand-written command nobody has exercised.
|
|
179
|
+
|
|
180
|
+
### Per module
|
|
181
|
+
|
|
182
|
+
Counted as `(method, path)` pairs: how many of a module's endpoints the refined layer calls, out of
|
|
183
|
+
how many the catalog documents. The module names are the ones `pingcode api list --module <m>` takes.
|
|
184
|
+
|
|
185
|
+
| Module | Refined | Total | Notes |
|
|
186
|
+
|---|---|---|---|
|
|
187
|
+
| `pjm` 项目管理 | 40 | 145 | projects, work items, sprints, releases, members. The 105 remaining are mostly configuration — 工作项配置 schemes (42) and 项目配置 (7) — plus 看板 boards (15) |
|
|
188
|
+
| `ship` 产品管理 | 27 | 101 | products, ideas, tickets, requirement schedules. Customers, external users and product configuration writes are generic-layer only |
|
|
189
|
+
| `testhub` 测试管理 | 32 | 65 | libraries, cases, plans, runs and their config lookups |
|
|
190
|
+
| `scm` 源码管理 | 31 | 36 | complete except the 5 `PUT`s — see below |
|
|
191
|
+
| `directory` 组织架构 | 1 | 23 | `settings users` only; departments, groups, roles and jobs are org master data |
|
|
192
|
+
| `wiki` | 0 | 19 | **no named command at all**, by decision: page content is `PUT`-shaped and destructive, and a CLI is a poor editor |
|
|
193
|
+
| `release` 部署 | 8 | 12 | environments + deploys; 2 `DELETE`s and 2 `PUT`s are generic-layer only |
|
|
194
|
+
| `build` 构建 | 5 | 6 | complete except its 1 `PUT` |
|
|
195
|
+
| 跨对象 `relations` `comments` `attachments` `activities` | 14 | 15 | the 15th is the `multipart/form-data` file upload — see the follow-ups |
|
|
196
|
+
| `reviews` `participants` | 0 | 12 | 评审 and 关注人; note `scm review` is a *different* resource |
|
|
197
|
+
| `permission` | 0 | 7 | 6 of the 7 need a user token; `GET /v1/permission/points` is reachable |
|
|
198
|
+
| `workloads` `workload_types` | 0 | 7 | 工时 |
|
|
199
|
+
| `nexus` | 0 | 5 | Nexus/CES app storage |
|
|
200
|
+
| `auth` | 0 | 3 | not user commands: `auth login` calls the `client_credentials` grant internally, and the two user-token grants are not implemented |
|
|
201
|
+
| `security` `myself` | 0 | 3 | login/audit logs, and the user-token `/v1/myself` |
|
|
202
|
+
| **Total** | **158** | **459** | 301 endpoints are reachable through `pingcode api` only |
|
|
203
|
+
|
|
204
|
+
**Two counting traps worth stating, because they make the arithmetic look wrong otherwise.**
|
|
205
|
+
|
|
206
|
+
- The table counts **endpoints**, while `--help` counts **commands**, and the two do not correspond
|
|
207
|
+
one-to-one in either direction. There are **254** command leaves across **10** groups. The four
|
|
208
|
+
cross-object families are implemented once (14 endpoints) and *mounted on five entities* — work
|
|
209
|
+
items, ideas, tickets, test cases and test runs — so they contribute **70** leaves from those 14
|
|
210
|
+
endpoints. And `pingcode resolve` contributes **32** leaves (one per resolvable metadata kind,
|
|
211
|
+
plus `resolve list`) while calling only lookup endpoints already counted in their own module.
|
|
212
|
+
- Conversely one command often covers several endpoints (`project work-item list` is both the simple
|
|
213
|
+
`GET` and `POST …/search`), and **two endpoints have no command at all** yet are counted: `GET
|
|
214
|
+
/v1/ship/ticket_state_plans` and its `…/ticket_state_flows` child are called by the resolver cache,
|
|
215
|
+
to tell `product ticket transition` which states are reachable when the server refuses one. They
|
|
216
|
+
are wired and exercised, just never as a leaf you can type — which is why the layer is labelled
|
|
217
|
+
*the refined layer* rather than *named commands*. So compare the two columns of *this* table, never
|
|
218
|
+
a leaf count against an endpoint count.
|
|
219
|
+
|
|
220
|
+
### How this task's plan compares
|
|
221
|
+
|
|
222
|
+
The task that produced this surface planned three mutually exclusive sets over the 459: **53 already
|
|
223
|
+
covered + 107 to add + 299 left to the generic layer**. Measured after the fact, at
|
|
224
|
+
`(method, path)` granularity:
|
|
225
|
+
|
|
226
|
+
- the **53** baseline is exact (pjm 10 + directory 1 + ship 22 + testhub 20), confirmed by running
|
|
227
|
+
the same count against the pre-task tree;
|
|
228
|
+
- **105 of the 107** landed, so refined coverage is 158 business endpoints and **301** are
|
|
229
|
+
generic-layer only;
|
|
230
|
+
- the two that did not: `POST /v1/attachments` in its `multipart/form-data` form (a file upload
|
|
231
|
+
needs a change to the frozen transport layer, so it was reported rather than forced), and
|
|
232
|
+
`GET /v1/testhub/plan_states/{state_id}` (the get-one; the *list* is wired and is the only thing
|
|
233
|
+
the plan write needs).
|
|
234
|
+
|
|
235
|
+
### Why there is no `scm platform replace`
|
|
236
|
+
|
|
237
|
+
All **10** `PUT` endpoints — 5 in `scm`, 2 in `release`, 1 each in `build`, `wiki` and `testhub` —
|
|
238
|
+
are reachable **only** through `pingcode api`, deliberately. `PUT` is full replacement on this API,
|
|
239
|
+
and the docs never say what an omitted field does; one module was measured *clearing* a field its
|
|
240
|
+
`PATCH` sibling preserves. A named `replace` command would make that trivially easy to do by
|
|
241
|
+
accident, so every refined write is a `PATCH`. If you really mean "replace the whole object":
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
pingcode api list --method PUT # all 10, with the resource each one replaces
|
|
245
|
+
pingcode api describe scm.products.replace # prints the full-replacement warning
|
|
246
|
+
pingcode api PUT /v1/scm/products/<id> --set name="…" --set type=other
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### The escape hatches
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
pingcode api list --module scm # what exists, offline, from the vendored catalog
|
|
253
|
+
pingcode api list --method DELETE # the whole auditable danger surface, 49 rows
|
|
254
|
+
pingcode api describe scm.commits.get # fields, scope, token type, paging, warnings
|
|
255
|
+
pingcode api GET /v1/directory/departments --all
|
|
256
|
+
pingcode resolve list --json # every name→id kind and the parent it needs
|
|
257
|
+
pingcode resolve ship-product SLC --json # ids for the generic layer, since it takes no names
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`api list` / `api describe` read a catalog vendored into the binary and never touch the network. A
|
|
261
|
+
weekly CI job diffs that catalog against the live docs — see [CI/CD](#cicd).
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Command surface
|
|
266
|
+
|
|
267
|
+
The top level mirrors PingCode's own GUI modules: each business module owns its resources *and* its
|
|
268
|
+
id lookups, so one `--help` shows a module's whole surface. **10 groups, 254 leaves**; `--help`
|
|
269
|
+
works at every level, and is the authority — this listing is a map, not a contract.
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
pingcode auth login | status | logout
|
|
273
|
+
pingcode api GET|POST|PATCH|PUT|DELETE <path> · list | describe
|
|
274
|
+
pingcode resolve list | <kind> <name> # 31 id-resolvable kinds
|
|
275
|
+
|
|
276
|
+
# ship (产品管理)
|
|
277
|
+
pingcode product list | get <product>
|
|
278
|
+
pingcode product idea list | get <ref> | create | update <ref> · history list|get
|
|
279
|
+
pingcode product ticket list | get <ref> | create | update <ref> | transition <ref>
|
|
280
|
+
pingcode product plan list | get <ref> # 需求排期, read-only upstream
|
|
281
|
+
pingcode product meta idea-states | idea-priorities | idea-suites | idea-properties | idea-plans
|
|
282
|
+
members | ticket-states | ticket-priorities | ticket-types
|
|
283
|
+
ticket-channels | ticket-properties
|
|
284
|
+
|
|
285
|
+
# pjm (敏捷项目管理)
|
|
286
|
+
pingcode project list | get <project> | create | update <project> | progress <project>
|
|
287
|
+
pingcode project work-item list | get <ref> | create | update <ref> | transition <ref>
|
|
288
|
+
bulk-update | delete <ref>
|
|
289
|
+
link list|get|add|delete · tag add|get|delete · history list|get
|
|
290
|
+
pingcode project sprint get | create | update | bulk-create # list is `project meta sprints`
|
|
291
|
+
pingcode project version list | get | create | update | delete | bulk-create
|
|
292
|
+
pingcode project member list | get | add
|
|
293
|
+
pingcode project meta types | states | priorities | sprints | relation-types | tags
|
|
294
|
+
|
|
295
|
+
# testhub (测试管理)
|
|
296
|
+
pingcode testhub libraries list | get <library> | create
|
|
297
|
+
pingcode testhub cases list | get <ref> | create | update <ref> | delete <ref>
|
|
298
|
+
bulk-create | bulk-update · history list
|
|
299
|
+
pingcode testhub plans list | get <ref> | create | update <ref>
|
|
300
|
+
pingcode testhub runs list | create | patch <run> | bulk | bulk-create | bulk-update
|
|
301
|
+
history list|get
|
|
302
|
+
pingcode testhub meta case-states | case-types | case-properties | important-levels
|
|
303
|
+
run-statuses | plan-types | plan-states | suites
|
|
304
|
+
|
|
305
|
+
# scm (源码管理) — DevOps write-back, 企业令牌 only
|
|
306
|
+
pingcode scm platform | platform-user | repo list | get | create | update
|
|
307
|
+
pingcode scm branch list | get | create | update | delete
|
|
308
|
+
pingcode scm commit | ref list | get | create
|
|
309
|
+
pingcode scm pr | review list | get | create | update
|
|
310
|
+
|
|
311
|
+
# 构建与部署
|
|
312
|
+
pingcode build list | get | create | update | delete
|
|
313
|
+
pingcode release env list | get | create | update
|
|
314
|
+
pingcode release deploy list | get | create | update
|
|
315
|
+
|
|
316
|
+
# 后台设置
|
|
317
|
+
pingcode settings users
|
|
318
|
+
|
|
319
|
+
# cross-object, mounted on five entities:
|
|
320
|
+
# product idea · product ticket · project work-item · testhub cases · testhub runs
|
|
321
|
+
pingcode <entity> relation list | get | add | delete
|
|
322
|
+
pingcode <entity> comment list | get | add | delete
|
|
323
|
+
pingcode <entity> attachment list | get | add-snippet | delete
|
|
324
|
+
pingcode <entity> activity list | get
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Global flags — valid **before or after** the subcommand: `--host <url>`, `--json`, `--dry-run`,
|
|
328
|
+
`--no-cache`, `--verbose`, `--version`, `--help`. `--help` works at every level
|
|
329
|
+
(`pingcode project work-item update --help`).
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
pingcode project list --json
|
|
333
|
+
pingcode project get "Mobile App" --json
|
|
334
|
+
|
|
335
|
+
# metadata first — ids are project-scoped
|
|
336
|
+
pingcode project meta types --project "Mobile App" --json
|
|
337
|
+
pingcode project meta states --project "Mobile App" --type task --json
|
|
338
|
+
pingcode project meta priorities --project "Mobile App" --json
|
|
339
|
+
pingcode settings users --keywords wang --json
|
|
340
|
+
|
|
341
|
+
pingcode project work-item list --project "Mobile App" --type task --state "In Progress" --json
|
|
342
|
+
pingcode project work-item list --project "Mobile App" --all --limit 200 --json
|
|
343
|
+
pingcode project work-item get SCR-5 --json # also: id, short_id, or a pasted work-item URL
|
|
344
|
+
|
|
345
|
+
pingcode project work-item create --project "Mobile App" --type task --title "Fix login retry" --dry-run --json
|
|
346
|
+
pingcode project work-item create --project "Mobile App" --type task --title "Fix login retry" \
|
|
347
|
+
--assignee wangxiao --priority High --end-at 2026-02-15 --json
|
|
348
|
+
|
|
349
|
+
pingcode project work-item update SCR-5 --title "Fix login retry (v2)" --json
|
|
350
|
+
pingcode project work-item transition SCR-5 --type task --state Done --json
|
|
351
|
+
pingcode project work-item transition SCR-5 --state-id 5eb623f6a70571487ea47000 --json
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
# ship: resolve the product first — every other ship id hangs off it
|
|
356
|
+
pingcode product list --json
|
|
357
|
+
pingcode product get SLC --json
|
|
358
|
+
|
|
359
|
+
pingcode product meta idea-states --product SLC --json
|
|
360
|
+
pingcode product meta members --product SLC --json # the only valid --assignee values
|
|
361
|
+
pingcode product meta ticket-types --product SLC --json # required to create a ticket
|
|
362
|
+
|
|
363
|
+
pingcode product idea list --product SLC --state 待评审 --assignee zhangsan --json
|
|
364
|
+
pingcode product idea get SLC-1 --json
|
|
365
|
+
pingcode product idea create --product SLC --title "Single sign-on" --dry-run --json
|
|
366
|
+
pingcode product idea update SLC-1 --state 开发中 --json
|
|
367
|
+
|
|
368
|
+
pingcode product ticket list --product SLC --type 故障 --json
|
|
369
|
+
pingcode product ticket create --product SLC --type 故障 --title "Cannot log in" --json
|
|
370
|
+
pingcode product ticket transition SLC-7 --state 处理中 --json
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
# testhub: resolve the test library first — states, types, statuses, modules and plans hang off it
|
|
375
|
+
pingcode testhub libraries list --json
|
|
376
|
+
pingcode testhub libraries get LIB --json
|
|
377
|
+
pingcode testhub libraries create --name "Payments" --identifier PAY --json # permanent: no DELETE
|
|
378
|
+
|
|
379
|
+
pingcode testhub meta case-states --library LIB --json # --state / state_id
|
|
380
|
+
pingcode testhub meta case-types --library LIB --json # --type / type_id
|
|
381
|
+
pingcode testhub meta run-statuses --library LIB --json # --status / status_id
|
|
382
|
+
pingcode testhub meta plan-types --library LIB --json # --type on `plans create`
|
|
383
|
+
pingcode testhub meta suites --library LIB --json # --suite; the PATH column is the key
|
|
384
|
+
pingcode testhub meta suites --library LIB --parent-id root --json # top level only
|
|
385
|
+
pingcode testhub meta important-levels --json # org-wide: takes no --library
|
|
386
|
+
|
|
387
|
+
pingcode testhub cases list --library LIB --state 已评审 --json
|
|
388
|
+
pingcode testhub cases get aB3dEf9h --json # an id or a short_id
|
|
389
|
+
pingcode testhub cases create --library LIB --title "SSO login" --dry-run --json
|
|
390
|
+
pingcode testhub cases update aB3dEf9h --state 已评审 --json
|
|
391
|
+
|
|
392
|
+
pingcode testhub plans list --library LIB --json
|
|
393
|
+
pingcode testhub plans get "2026 S1 回归" --library LIB --json
|
|
394
|
+
pingcode testhub plans create --library LIB --name "2026 S2 回归" \
|
|
395
|
+
--type 普通 --start 2026-08-10 --end 2026-08-31 --assignee 张三 --dry-run --json
|
|
396
|
+
|
|
397
|
+
pingcode testhub runs list --library LIB --plan "2026 S1 回归" --json
|
|
398
|
+
pingcode testhub runs update 7hK2mQ9x --status 通过 --remark "retested on iOS" --json
|
|
399
|
+
pingcode testhub runs bulk --library LIB --plan "2026 S1 回归" --remove-run 7hK2mQ9x --json
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
# DevOps write-back: a CI job telling PingCode what happened. Nothing here reads your
|
|
404
|
+
# git server or your pipeline — every command writes a record PingCode links to work items.
|
|
405
|
+
pingcode scm platform list --json
|
|
406
|
+
pingcode scm repo list --platform "GitHub" --json
|
|
407
|
+
pingcode scm commit create --sha 9f3c1ab0000000000000000000000000000000ab \
|
|
408
|
+
--message "fix login retry" --committer ci-bot --work-item PLM-1 --dry-run --json
|
|
409
|
+
pingcode scm pr create --platform "GitHub" --repo acme/web --title "Fix login retry" \
|
|
410
|
+
--number 42 --status open --creator ci-bot \
|
|
411
|
+
--source-branch-id <id> --target-branch-id <id> --json
|
|
412
|
+
|
|
413
|
+
pingcode build create --name nightly --identifier 1042 --provider jenkins --status success \
|
|
414
|
+
--start-at 2026-08-05T01:00:00Z --end-at 2026-08-05T01:07:30Z --duration 450 \
|
|
415
|
+
--work-item PLM-1 --json
|
|
416
|
+
pingcode release env list --json
|
|
417
|
+
pingcode release deploy create --env staging --status deployed --release-name 1.4.0 \
|
|
418
|
+
--start-at 2026-08-05T02:00:00Z --end-at 2026-08-05T02:03:00Z --duration 180 --json
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
# cross-object: the same four families on any of the five entities
|
|
423
|
+
pingcode project work-item comment add SCR-5 --text "blocked by the SSO rollout" --json
|
|
424
|
+
pingcode project work-item relation add SCR-5 --target-type test_case --target-id <id> --json
|
|
425
|
+
pingcode product idea activity list SLC-1 --json
|
|
426
|
+
pingcode testhub runs attachment list 7hK2mQ9x --json
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
`--dry-run` on a mutating command prints the request it *would* have sent and exits 0 without
|
|
430
|
+
sending it. Read requests still run, so ids are genuinely resolved first.
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## The `--json` contract
|
|
435
|
+
|
|
436
|
+
- **stdout carries JSON only.** Tables, logs, warnings, dry-run notes and errors go to stderr.
|
|
437
|
+
- Timestamps stay raw **unix seconds** in `--json`; human mode renders local time.
|
|
438
|
+
- Three list shapes, by command family:
|
|
439
|
+
- one page of any refined `list` (`project work-item list`, `product idea list`,
|
|
440
|
+
`testhub cases list`, `scm repo list`, `build list`, `release deploy list`, …) →
|
|
441
|
+
`{"page_index":0,"page_size":30,"total":123,"values":[…]}`
|
|
442
|
+
- any list with `--all` → `{"values":[…],"count":42,"all":true}`
|
|
443
|
+
- every `meta` lookup (`product meta …`, `project meta …`, `testhub meta …`, `settings users`) → `{"values":[…],"count":20}`
|
|
444
|
+
- Single-resource commands (`get`, `create`, `update`, `transition`) print the resource object.
|
|
445
|
+
- `--dry-run` prints `{"dry_run":true,"request":{"method":…,"url":…,"headers":…,"body":…}}` — with
|
|
446
|
+
`Authorization` and any `client_secret` masked.
|
|
447
|
+
- Errors print to **stderr** as `{"error":{"kind":…,"message":…,"code":…,"exit":…}}`.
|
|
448
|
+
- **`pingcode api` is different: stdout is always the API's raw JSON, so `--json` is a no-op on the
|
|
449
|
+
five verbs.** There is no table to switch off. Its own `api list` / `api describe` are local
|
|
450
|
+
catalog views and do honour `--json`.
|
|
451
|
+
- **Read keys defensively — an absent key means null *or* empty.** `api/parse.ts` normalises both
|
|
452
|
+
`null` and `""` to "not present", so they are simply missing from the output and cannot be told
|
|
453
|
+
apart. See the follow-ups; this is the one output change queued as breaking.
|
|
454
|
+
|
|
455
|
+
### Exit codes
|
|
456
|
+
|
|
457
|
+
| Exit | Kind | Meaning |
|
|
458
|
+
|---|---|---|
|
|
459
|
+
| 0 | — | success, including a printed dry-run plan |
|
|
460
|
+
| 1 | `unexpected` | unhandled internal error |
|
|
461
|
+
| 2 | `usage` | bad/missing flags, ambiguous or unresolvable name, empty update |
|
|
462
|
+
| 3 | `auth` | no or invalid credentials |
|
|
463
|
+
| 4 | `permission` | 403, or a scope the app was never granted |
|
|
464
|
+
| 5 | `not_found` | the work item, state or other resource does not exist |
|
|
465
|
+
| 6 | `rate_limit` | 429 — the limit is 200 requests/minute per token |
|
|
466
|
+
| 7 | `api` | any other non-2xx, carrying the API's `{code, message}` |
|
|
467
|
+
| 8 | `transport` | DNS / TCP / TLS / timeout / unparseable body |
|
|
468
|
+
|
|
469
|
+
This API answers **HTTP 400 where REST convention would use 401 or 404**, so a table of observed API
|
|
470
|
+
`code` values is mapped by code rather than by status. It currently holds **32 rows** — 1 → exit 3
|
|
471
|
+
and 31 → exit 5 — and the authoritative copy, with the live observation behind every single row, is
|
|
472
|
+
`ERROR_CODE_OVERRIDES` in `src/core/wire.ts`. A sample:
|
|
473
|
+
|
|
474
|
+
| API `code` | HTTP | Observed on | → exit |
|
|
475
|
+
|---|---|---|---|
|
|
476
|
+
| `100024` | 400 | `GET /v1/auth/token` with a wrong client id/secret | 3 (`auth`) |
|
|
477
|
+
| `100317` | 400 | `GET /v1/pjm/work_items/{unknown id}` | 5 (`not_found`) |
|
|
478
|
+
| `100725` / `100711` | 400 | unknown ship idea / ticket | 5 (`not_found`) |
|
|
479
|
+
| `100601` / `100603` / `100600` | 400 | unknown testhub case / run / library | 5 (`not_found`) |
|
|
480
|
+
| `100051` / `100045` / `100801` / `100077` | 400 | unknown comment / attachment / relation / activity | 5 (`not_found`) |
|
|
481
|
+
| `100200` / `100202` / `100209` | 400 | unknown scm platform / repository / git identity | 5 (`not_found`) |
|
|
482
|
+
|
|
483
|
+
The rule for growing that table is in
|
|
484
|
+
[`.trellis/spec/backend/error-handling.md`](.trellis/spec/backend/error-handling.md): match on the
|
|
485
|
+
`code` string only (the API is Chinese-only and its wording is not a contract), and add a row only
|
|
486
|
+
with a recorded live observation cited next to it.
|
|
487
|
+
|
|
488
|
+
Any other code keeps the status-first mapping and is surfaced verbatim on exit 7 — read `code`
|
|
489
|
+
before drawing conclusions. (An invalid *bearer* token on a resource endpoint does return a real
|
|
490
|
+
401, so the 401 branch is still live.) Note what is deliberately **absent**, and why the absences
|
|
491
|
+
matter as much as the rows: ship's `100719` / `100702` ("state does not exist") also fire for a state
|
|
492
|
+
that plainly exists but is unreachable under the state plan, so mapping them to `not_found` would be
|
|
493
|
+
a lie; testhub's `100619` rejects a *whole* bulk batch, so exit 5 would name one run while implying
|
|
494
|
+
the others landed; and `100000` is a real HTTP 500 that must keep it.
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Caveats that matter in practice
|
|
499
|
+
|
|
500
|
+
The exhaustive per-module traps live in `skills/pingcode/modules/*.md` — one file per module, and
|
|
501
|
+
they are written for an agent, which makes them the most detailed reference in the repository. What
|
|
502
|
+
follows is only what applies everywhere.
|
|
503
|
+
|
|
504
|
+
- **Ids are parent-scoped — run the module's `meta` lookups first.** The parent is a **project** in
|
|
505
|
+
pjm, a **product** in ship, a **test library** in testhub and a **hosting platform** in scm. The
|
|
506
|
+
same state name has a different id under a different parent. System work-item types are bare slugs
|
|
507
|
+
(`task`, `story`, `bug`); custom types, states and priorities are 24-hex ids; users are 32-hex.
|
|
508
|
+
Never reuse an id across parents, and never let a script validate an id's shape.
|
|
509
|
+
- **`update` replaces, it does not merge.** Every field you pass overwrites the stored value, and
|
|
510
|
+
arrays plus `properties` objects are replaced wholesale. Read the item first if you need to keep
|
|
511
|
+
anything. There is no way to clear a field, and an update with no fields is exit 2, not a no-op.
|
|
512
|
+
- **Every `delete` needs `--yes`, and the refusal echoes the resolved name**, not just the id — the
|
|
513
|
+
confirmation costs one extra GET and buys back the one class of mistake that cannot be undone.
|
|
514
|
+
`pingcode api list --method DELETE` enumerates all 49 deletable endpoints.
|
|
515
|
+
- **`PATCH` only. No refined command issues a `PUT`** — see
|
|
516
|
+
[why there is no `scm platform replace`](#why-there-is-no-scm-platform-replace).
|
|
517
|
+
- **`--all` is best effort, not a snapshot.** It walks 0-based pages (`page_size` ≤ 100),
|
|
518
|
+
de-duplicates by id, stops at `--limit` (default 500) and bails if the server stops honouring
|
|
519
|
+
`page_index`. **No endpoint supports sorting**, so offset paging over changing data can duplicate
|
|
520
|
+
or skip rows. Sort what you collected yourself.
|
|
521
|
+
- **Rate limit: 200 requests/minute per token.** 2xx responses carry no rate-limit headers, so the
|
|
522
|
+
budget is invisible until a 429 arrives. Prefer one `--page-size 100` call over many small ones,
|
|
523
|
+
and let the cache work.
|
|
524
|
+
- **Timestamps are unix seconds everywhere.** Date flags accept `1730000000` or a calendar date;
|
|
525
|
+
read the flag's own `--help`, because the two families differ deliberately: `project --start-at`
|
|
526
|
+
stores the instant verbatim, while `project sprint` / `project version` / `testhub plans` snap
|
|
527
|
+
`--start` to `00:00:00` and `--end` to `23:59:59` of the date.
|
|
528
|
+
- **A 200 is not proof the field landed.** This API accepts unknown body fields, several read-only
|
|
529
|
+
fields and (in `work-item bulk-update`) whole unsupported properties with a 200 and no warning.
|
|
530
|
+
Where a command knows about one, it refuses locally or warns on stderr; where it cannot know, read
|
|
531
|
+
the object back.
|
|
532
|
+
- **Metadata is cached for 24 h** under `~/.pingcode/cache/` (mode `0600`, hashed filenames), keyed
|
|
533
|
+
by `(apiBase, clientId, parentId, kind)`. Pass `--no-cache` if a parent was reconfigured and an id
|
|
534
|
+
looks stale; a write rejected on a cached id invalidates that entry and retries **once**, and only
|
|
535
|
+
if re-resolving actually changed an id — the CLI never sends the same mutating body twice.
|
|
536
|
+
`auth login` and `auth logout` both clear the cache.
|
|
537
|
+
- **`pingcode resolve` is the same lookup as a hand-typed name**, exposed as one id on stdout so it
|
|
538
|
+
can feed `pingcode api`, which takes ids only.
|
|
539
|
+
- **Two flag shapes, split by module, both accepting a name or an id.** `testhub`, `scm` and
|
|
540
|
+
`release` use **pairs** — `--library` / `--library-id`, `--platform` / `--platform-id`, `--repo` /
|
|
541
|
+
`--repo-id`, `--env` / `--env-id` — where `--x` looks the name up and `--x-id` is sent verbatim with
|
|
542
|
+
no lookup; the two are mutually exclusive (exit 2). `pjm` and `ship` use a **single** flag
|
|
543
|
+
(`--project`, `--sprint`, `--release`, `--product`) that decides for you and offers no way to skip
|
|
544
|
+
the lookup. Neither shape ever validates an id's format. `SKILL.md` has the table plus the three
|
|
545
|
+
deliberate exceptions (`testhub runs list --case-id`, `scm … list --work-item-id`, and
|
|
546
|
+
`project update --state-id`, which has no `--state` because no resolver kind covers project states).
|
|
547
|
+
|
|
548
|
+
### pjm-specific caveats
|
|
549
|
+
|
|
550
|
+
- **`--state <name>` always needs `--type`.** States live in a `(project, work item type)` pair and
|
|
551
|
+
the API never reports a work item's type, so the CLI cannot infer it — not on `list`, and not on
|
|
552
|
+
`update`/`transition` (`create` already requires `--type`). Pass `--type <name|id>`, or skip the
|
|
553
|
+
lookup with `--state-id <id>`. On `update`/`transition`, `--type` is *only* a lookup aid: it is
|
|
554
|
+
never written to the work item. `--state` and `--state-id` are mutually exclusive.
|
|
555
|
+
- **State changes are workflow-validated server-side.** On rejection the CLI prints the server
|
|
556
|
+
message plus the states configured for that type — but only if you passed `--type`.
|
|
557
|
+
- **A project can never be deleted or archived**, and a **sprint can never be deleted at all**.
|
|
558
|
+
`project create`, `project sprint create` and `sprint bulk-create` are irreversible; `--dry-run` first.
|
|
559
|
+
- **`link` and `relation` are different families.** `link` is work item ↔ work item with a required
|
|
560
|
+
type; `relation` is work item ↔ anything *else* and refuses two work items outright.
|
|
561
|
+
- **There is no `sprint list` or `work-item tag list` leaf.** The sprint list is `project meta
|
|
562
|
+
sprints` (it doubles as the `--sprint` lookup); a work item's tags are the `tags[]` field of
|
|
563
|
+
`work-item get`, because upstream publishes no collection GET for them.
|
|
564
|
+
- **A work item's assignee cannot be cleared via the API.** `PATCH`'s `assignee_id` is a plain
|
|
565
|
+
string with no `nullable`: `null` is a silent HTTP 200 no-op and `""` is an HTTP 400. So
|
|
566
|
+
`work-item update <id> --assignee ""` fails fast (exit 2) rather than sending a request that would
|
|
567
|
+
look like success. To unassign, use the PingCode web UI.
|
|
568
|
+
|
|
569
|
+
### Ship-specific caveats
|
|
570
|
+
|
|
571
|
+
Everything above still applies; [`modules/ship.md`](skills/pingcode/modules/ship.md) is the full
|
|
572
|
+
version. These are the differences that will cost you time:
|
|
573
|
+
|
|
574
|
+
- **A product is ship's project.** `state_id`, `priority_id`, `suite_id`, `type_id`, `channel_id`,
|
|
575
|
+
the writable `properties` keys and the assignable people are all **product-scoped**, even though
|
|
576
|
+
several of them look org-global (the same `P0` priority id appears under multiple products). The
|
|
577
|
+
API demands `product_id` on every lookup; never reuse an id across products.
|
|
578
|
+
- **`--assignee` resolves against product members**, not `/v1/directory/users`. A user who is not a
|
|
579
|
+
member of the product cannot be assigned, so `product meta members` is the candidate set.
|
|
580
|
+
- **`product idea list` and `product ticket list` are `POST …/search`.** The plain list endpoints cannot filter by
|
|
581
|
+
assignee, date or custom property. The DSL allows **one operator per field and no `$and`/`$or`**;
|
|
582
|
+
several filters are AND-ed. Body pagination puts the cursor in `payload.page_index`, and the CLI
|
|
583
|
+
applies the same `--page` / `--page-size` (≤100) / `--all` / `--limit` semantics as elsewhere.
|
|
584
|
+
- **State changes are decided by the server; ticket refusals are explained.** Ship publishes the
|
|
585
|
+
legal transitions of a ticket state plan, and the CLI reads them — but only to *explain* a
|
|
586
|
+
refusal, never to pre-empt one. `product ticket transition` sends the PATCH; if the server refuses, the
|
|
587
|
+
error `message` carries the configured states, the current state and the states reachable from
|
|
588
|
+
it. `product ticket transition --dry-run` previews that reachable set on stderr without writing. Ideas
|
|
589
|
+
have **no state-flow endpoint at all**, so `product idea update --state` gets the configured states on
|
|
590
|
+
rejection and nothing more. The only local refusal is moving a ticket to the state it is already
|
|
591
|
+
in. Rationale: the server refuses atomically, so nothing is saved by checking first, while a
|
|
592
|
+
mis-identified plan would block a legal move outright (`s7-smoke.md` F5).
|
|
593
|
+
- **Locating a ticket's state plan is a scan, and only ever advisory.** The ticket payload carries
|
|
594
|
+
no plan reference and the plan list has no `product_id` filter, so the CLI lists every plan and
|
|
595
|
+
matches the embedded `product.id`, falling back to the org-default (`product: null`) plan when
|
|
596
|
+
there is exactly one — which live is the common case. Cached per product. Since the answer only
|
|
597
|
+
feeds an explanation, a wrong guess costs a wrong suggestion, never a blocked write.
|
|
598
|
+
- **`--set key=value` sends the value verbatim, and select-type properties want the option `_id`,
|
|
599
|
+
not its label.** `product meta idea-properties` / `product meta ticket-properties` print both, and are also the
|
|
600
|
+
authoritative list of writable keys. `properties` replaces wholesale.
|
|
601
|
+
- **Nothing in ship can be deleted.** There is no DELETE for products, ideas or tickets, and
|
|
602
|
+
`is_archived` / `is_deleted` are read-only. Anything you create during a test is permanent —
|
|
603
|
+
prefix the title before creating it.
|
|
604
|
+
- **An identifier works on the resource, and nowhere below it.** `product idea get` / `ticket get`
|
|
605
|
+
accept the id, the 8-char `short_id` a pasted URL ends in **and** the human `SLC-1` — all three
|
|
606
|
+
answer 200 live. A sub-resource (a comment, an attachment) is addressed by the parent's real id, so
|
|
607
|
+
every write resolves the reference first.
|
|
608
|
+
- **`--suite` filtering on `product idea list` is undocumented** — the API lists `suite.id` as neither
|
|
609
|
+
filterable nor unfilterable, so an empty result proves nothing. The CLI warns when you use it.
|
|
610
|
+
- **`ticket.channel` is an object or the bare string `"internal"`**, and `--channel` can only be set
|
|
611
|
+
at create time. Tags cannot be written at all, and a ticket's `submitter_id` is silently ignored
|
|
612
|
+
under a client-credentials token — neither is exposed.
|
|
613
|
+
- **"Plan" is three unrelated resources**, and mixing them up produces a not-found nobody can
|
|
614
|
+
explain: `product plan` is a 需求排期 (requirement schedule, read-only upstream — a write answers
|
|
615
|
+
HTTP 405), `testhub plans` is a test plan, and `ticket_state_plans` is a configuration scheme
|
|
616
|
+
reachable only through `pingcode api`.
|
|
617
|
+
- **`product idea history` is state changes only.** A title or assignee edit is not there; that is
|
|
618
|
+
`product idea activity list`, the free-form feed.
|
|
619
|
+
|
|
620
|
+
### Testhub-specific caveats
|
|
621
|
+
|
|
622
|
+
Everything above still applies. Testhub's parent scope is a **test library**, and its write path is
|
|
623
|
+
the sharpest in the CLI. [`skills/pingcode/modules/testhub.md`](skills/pingcode/modules/testhub.md)
|
|
624
|
+
is the exhaustive version, including the `--set` traps and the two bulk families' opposite failure
|
|
625
|
+
modes.
|
|
626
|
+
|
|
627
|
+
- **A test library is testhub's project.** `state_id`, `type_id`, `status_id`, `suite_id` and the
|
|
628
|
+
plan list are all library-scoped — two libraries never share a state, type or status id, even when
|
|
629
|
+
the names match. Six `meta` leaves are library-scoped (`case-states`, `case-types`,
|
|
630
|
+
`case-properties`, `run-statuses`, `plan-types`, `suites`) and two are organisation-level
|
|
631
|
+
(`important-levels`, `plan-states`); a missing `--library` on a library-scoped command is exit 2.
|
|
632
|
+
`cases get|update|delete`, `plans get|update` and `runs update` read the resource first and inherit
|
|
633
|
+
its library; `runs list` needs one only to resolve `--plan` / `--status` by name.
|
|
634
|
+
- **`cases list` and `runs list` are `POST …/search`.** The plain `GET` lists are never used —
|
|
635
|
+
unfiltered, `GET /v1/testhub/cases` scans every library the token can see. Same DSL limits as
|
|
636
|
+
ship: one operator per field, no `$and`/`$or`, no sorting.
|
|
637
|
+
- **`--step` is all-or-nothing, because `steps[]` replaces.** A run's step array is overwritten
|
|
638
|
+
wholesale and a step sent without its `step_id` is re-created with a new id, orphaning its
|
|
639
|
+
history. Re-emitting an untouched step is impossible: a run step reports a status **slug** while
|
|
640
|
+
the write needs a status **id**, and only the localized (renameable) name joins them. So the CLI
|
|
641
|
+
refuses a partial step edit and prints the full list of step ids. `--set` / `properties` on a case
|
|
642
|
+
replace wholesale too.
|
|
643
|
+
- **`runs update` always sends `status_id`, and carries the executor over.** `status_id` is required
|
|
644
|
+
by the API even on PATCH, so the CLI pre-reads the run and re-sends its current result — and its
|
|
645
|
+
current executor — when you do not name one. If the run has no executor and you name none,
|
|
646
|
+
`executor_id` is omitted and the CLI warns that the run stays unassigned (omitting it is a
|
|
647
|
+
verified no-op on PATCH: it neither clears the field nor reassigns the run). With no recorded
|
|
648
|
+
result at all it asks for `--status` (exit 2) rather than sending a half-formed body.
|
|
649
|
+
- **`runs bulk` is the only way to *delete* a run** — there is no run DELETE endpoint at all. Runs
|
|
650
|
+
can be *created* three ways (`runs create`, `runs bulk-create`, or `runs bulk --add-case`), and the
|
|
651
|
+
caps differ: `cases bulk-*` and `runs bulk-*` are capped at **100 by the server**, while
|
|
652
|
+
`runs bulk` enforces nothing upstream and the CLI caps each of its three arrays at **50** locally.
|
|
653
|
+
A bulk response is **counts only**: re-list the plan to see the new ids.
|
|
654
|
+
- **The two bulk families fail in opposite ways.** `runs bulk` is per-element best effort under a
|
|
655
|
+
200, `cases bulk-*` is atomic. Do not generalise one to the other — always read the counts.
|
|
656
|
+
- **`runs list` cannot filter by `library.id`** — it is on the API's exclusion list for run search,
|
|
657
|
+
so scope runs with `--plan`. The CLI warns when `--library` is given without one.
|
|
658
|
+
- **`meta important-levels` takes no `--library`.** Importance levels are organisation-wide, the one
|
|
659
|
+
testhub lookup with no per-library variant, so the flag is refused with exit 2 instead of being
|
|
660
|
+
ignored. The refusal is hidden from `--help`, which is why it is written down here.
|
|
661
|
+
- **`pcp:read:testhub:configuration` is a trap, not an option.** `meta case-states` and
|
|
662
|
+
`meta run-statuses` need it; their sibling `meta case-types` does not. Without it a token gets a
|
|
663
|
+
bare 403 from exactly the two lookups that produce a `state_id` and a `status_id` — so it cannot
|
|
664
|
+
write a run at all. The CLI rewrites that 403 to say so.
|
|
665
|
+
- **`cases create` sends the library as `test_library_id`** (not `library_id`), and `state_id` is
|
|
666
|
+
**PATCH-only**: a case is created in the library's initial state and moved with `cases update`.
|
|
667
|
+
- **`cases delete` takes the case's runs with it**, soft-deleted alongside it. It is gated behind
|
|
668
|
+
`--yes` and the confirmation names the case, because the blast radius is invisible from the
|
|
669
|
+
reference you typed.
|
|
670
|
+
- **`short_id` is read-only.** Reads accept an id or a `short_id`; every write documents `id` only,
|
|
671
|
+
so the write paths resolve it through a pre-read.
|
|
672
|
+
- **Dates: the end of a range is inclusive.** `--start` / `--end` on `plans create` and
|
|
673
|
+
`plans update` take a zero-padded `YYYY-MM-DD` or a 10-digit unix **seconds** integer. A calendar
|
|
674
|
+
date resolves to **00:00:00 local** for `--start` and **23:59:59 local** for `--end`; a raw integer
|
|
675
|
+
is passed through **verbatim** on both. The asymmetry is deliberate — a range runs *through* its
|
|
676
|
+
end date. Rejected with exit 2 **before any request**: an unpadded `2026-8-1`, slashes, an ISO
|
|
677
|
+
string carrying a time, a 13-digit milliseconds value, an impossible date like `2026-02-30`, and
|
|
678
|
+
`--end` before `--start`.
|
|
679
|
+
- **`plans create` requires all six flags and `--assignee` has no default.** `--library`, `--name`
|
|
680
|
+
(unique within the library), `--type`, `--start`, `--end`, `--assignee`. There is no "assign to me"
|
|
681
|
+
because a client-credentials token acts as the **bot user**, so a default would quietly make a bot
|
|
682
|
+
the 负责人 of every plan the CLI creates.
|
|
683
|
+
- **A plan type carries no `kind`, so the CLI cannot classify it.** Iteration and release types also
|
|
684
|
+
need `sprint_id` / `version_id` (and the `project_id` those make mandatory), but the plan-type
|
|
685
|
+
resource exposes only `id` / `name` / `url` / `library` — and tenants rename these, so the name is
|
|
686
|
+
not a safe discriminator. `plans create` sends what you gave it and surfaces the **server's**
|
|
687
|
+
refusal for a type that needs more. Use the plain (普通) type unless you know the tenant's setup.
|
|
688
|
+
- **A library can be created but never deleted.** `--identifier` is unique across the organisation
|
|
689
|
+
and the server enforces it. There is **no library DELETE**; a library PATCH *does* exist upstream
|
|
690
|
+
and is reachable generically —
|
|
691
|
+
`pingcode api PATCH /v1/testhub/libraries/<id> --set description="…"` — but it cannot clear a
|
|
692
|
+
field, so name a library right the first time. The CLI prints that warning after every create.
|
|
693
|
+
- **Still not exposed, on purpose:** library members, case-module (suite) writes, plan **delete**,
|
|
694
|
+
configuration writes, and `PUT /runs/{id}` (documented to blank the executor when the field is
|
|
695
|
+
omitted — unverified, and `runs update` covers the same ground safely). All are reachable through
|
|
696
|
+
`pingcode api` if you really need them.
|
|
697
|
+
|
|
698
|
+
### SCM, build and release caveats
|
|
699
|
+
|
|
700
|
+
These three groups are the DevOps **write-back** surface: a CI/CD job tells PingCode what happened,
|
|
701
|
+
and PingCode links it to work items. None of them reads your git server or your pipeline.
|
|
702
|
+
[`modules/scm.md`](skills/pingcode/modules/scm.md) and
|
|
703
|
+
[`modules/cicd.md`](skills/pingcode/modules/cicd.md) are the full versions.
|
|
704
|
+
|
|
705
|
+
- **All 企业令牌 only**, which is exactly what `client_credentials` yields — but under three
|
|
706
|
+
*separate* scopes (`devops:code`, `devops:build`, `devops:deploy`). A token that can write commits
|
|
707
|
+
cannot write builds, and the only symptom is exit 4.
|
|
708
|
+
- **`/v1/scm/products` is a hosting platform (托管平台), not a ship product.** Every scm command
|
|
709
|
+
except the commit family starts by resolving a platform; commits are organisation-level.
|
|
710
|
+
- **A "platform user" is a git author identity, not a PingCode member** — it carries no `user_id`,
|
|
711
|
+
and naming an unknown one on a write **creates** it. Since scm has **no DELETE anywhere** except
|
|
712
|
+
branches, a typo in `--sender` or `--creator` is a permanent row.
|
|
713
|
+
- **`full_name` (`owner/name`) is a repository's unique key**, and `?name=` is ignored upstream —
|
|
714
|
+
hence `scm repo list --full-name`, an exact filter.
|
|
715
|
+
- **`build list` has no filters at all** (five plausible ones were probed live and silently ignored),
|
|
716
|
+
and a build `identifier` is not unique, so it is not a lookup key either.
|
|
717
|
+
- **`release deploy list` hides an unknown environment behind an empty list**, so an empty result is
|
|
718
|
+
not evidence the environment exists. Resolve it first.
|
|
719
|
+
- **Work items are linked by `--work-item <identifier>`** (`PLM-1`), not by id, and an unknown
|
|
720
|
+
identifier is **silently dropped** by the API under a 200.
|
|
721
|
+
|
|
722
|
+
---
|
|
723
|
+
|
|
724
|
+
## The `pingcode` skill
|
|
725
|
+
|
|
726
|
+
`skills/pingcode/` is the source of truth for the agent-facing docs. It is layered the same way this
|
|
727
|
+
README now is: `SKILL.md` carries only what does not scale with the surface — the authentication
|
|
728
|
+
gate, the `--json` / `--dry-run` contracts, the exit-code table, the escape hatches and a map — and
|
|
729
|
+
one file per module carries that module's flags and traps:
|
|
730
|
+
|
|
731
|
+
| File | Covers |
|
|
732
|
+
|---|---|
|
|
733
|
+
| [`modules/pjm.md`](skills/pingcode/modules/pjm.md) | projects, work items, sprints, releases, members |
|
|
734
|
+
| [`modules/ship.md`](skills/pingcode/modules/ship.md) | products, ideas, tickets, requirement schedules |
|
|
735
|
+
| [`modules/testhub.md`](skills/pingcode/modules/testhub.md) | libraries, cases, plans, runs |
|
|
736
|
+
| [`modules/scm.md`](skills/pingcode/modules/scm.md) | platforms, git identities, repos, branches, commits, refs, PRs, reviews |
|
|
737
|
+
| [`modules/cicd.md`](skills/pingcode/modules/cicd.md) | build records, environments, deployments |
|
|
738
|
+
| [`modules/crosscutting.md`](skills/pingcode/modules/crosscutting.md) | relations, comments, attachments, activities and their five mounts |
|
|
739
|
+
| [`modules/api.md`](skills/pingcode/modules/api.md) | the generic executor and `api list` / `api describe` |
|
|
740
|
+
|
|
741
|
+
`test/help/skill.test.ts` asserts that **every `pingcode …` path mentioned in any of those files
|
|
742
|
+
resolves in the real commander tree**, so a documented command that does not exist fails the suite.
|
|
743
|
+
The reverse is deliberately *not* asserted: at 254 leaves, requiring every leaf to be documented
|
|
744
|
+
would make the docs a merge point for every parallel change. Sync them to your agent skill
|
|
745
|
+
directories:
|
|
746
|
+
|
|
747
|
+
```bash
|
|
748
|
+
npm run skill:install -- --dry-run # show the destinations, write nothing
|
|
749
|
+
npm run skill:install # pick a target (prompts on a TTY, else installs both)
|
|
750
|
+
npm run skill:install -- --target claude # Claude Code only
|
|
751
|
+
npm run skill:install -- --target opencode # OpenCode only
|
|
752
|
+
npm run skill:install -- --target claude,opencode # or --target all
|
|
753
|
+
npm run skill:install -- --force # overwrite existing copies
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
Installs are **global (user-level)** only, and copy the `modules/` directory alongside `SKILL.md`:
|
|
757
|
+
|
|
758
|
+
| Target | Destination |
|
|
759
|
+
| --- | --- |
|
|
760
|
+
| `claude` | `~/.claude/skills/pingcode/SKILL.md` |
|
|
761
|
+
| `opencode` | `$XDG_CONFIG_HOME/opencode/skills/pingcode/SKILL.md` (default `~/.config/opencode/…`) |
|
|
762
|
+
|
|
763
|
+
`--target` is repeatable, comma-separated and case-insensitive. With no `--target` the script
|
|
764
|
+
prompts when stdin is a TTY (prompt on stderr, `q` aborts without writing) and installs **both**
|
|
765
|
+
targets when it isn't, so CI and pipes keep their old behaviour. An unknown target exits `2`.
|
|
766
|
+
|
|
767
|
+
---
|
|
768
|
+
|
|
769
|
+
## CI/CD
|
|
770
|
+
|
|
771
|
+
Three GitHub Actions workflows, all dependency-free: every gate is an npm script you can run
|
|
772
|
+
locally with the identical command, so a red run never needs a "push and see" loop.
|
|
773
|
+
|
|
774
|
+
**`.github/workflows/ci.yml`** — on every push to `main` and every pull request. Superseded runs
|
|
775
|
+
for the same ref are cancelled.
|
|
776
|
+
|
|
777
|
+
| Job | What it does |
|
|
778
|
+
| --- | --- |
|
|
779
|
+
| `node 20` / `node 22` / `node 24` | `npm ci` → `typecheck` → `test` → `build` → run the built bundle's `--version` and `--help` → `skill:install --dry-run` |
|
|
780
|
+
| `secret scan and commit gate` | `scan:secrets` and `check:commits` over the pushed/PR commit range, once per run |
|
|
781
|
+
|
|
782
|
+
**`.github/workflows/catalog-check.yml`** — a **weekly** (Mondays 03:17 UTC) and on-demand watch that
|
|
783
|
+
diffs the vendored endpoint catalog against the live apiDoc bundle. It is deliberately **not** a PR
|
|
784
|
+
gate: it depends on a third-party host, and an upstream documentation edit has nothing to do with
|
|
785
|
+
whichever pull request happens to be open when it lands. On drift it files or refreshes a **single**
|
|
786
|
+
`catalog-drift` issue and stays green; it closes that issue when upstream matches again, and fails
|
|
787
|
+
only when the check itself could not run. What to do with the issue is
|
|
788
|
+
[`.trellis/spec/backend/catalog-drift.md`](.trellis/spec/backend/catalog-drift.md) — in particular,
|
|
789
|
+
live behaviour outranks the catalog, and a command is never deleted because an endpoint vanished for
|
|
790
|
+
one cycle.
|
|
791
|
+
|
|
792
|
+
Permissions are `contents: read` at workflow level; the release job and the drift watch each elevate
|
|
793
|
+
exactly one scope. No secrets are used or needed — the test suite injects `fetch` and never opens a
|
|
794
|
+
socket, and there are no PingCode credentials in CI.
|
|
795
|
+
|
|
796
|
+
Run the same gates locally:
|
|
797
|
+
|
|
798
|
+
```bash
|
|
799
|
+
npm run typecheck && npm test && npm run build
|
|
800
|
+
node dist/bin/pingcode.js --version && node dist/bin/pingcode.js --help
|
|
801
|
+
npm run skill:install -- --dry-run
|
|
802
|
+
|
|
803
|
+
npm run catalog:check # diff the vendored catalog against the live docs
|
|
804
|
+
npm run scan:secrets # tracked files
|
|
805
|
+
npm run scan:secrets -- origin/main..HEAD # + those commit messages
|
|
806
|
+
npm run check:commits # whole history
|
|
807
|
+
npm run check:commits -- origin/main..HEAD # just your branch
|
|
808
|
+
npm run check:commits -- --file .git/COMMIT_EDITMSG # one message file (what the hook runs)
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
`scan:secrets` (`scripts/scan-secrets.ts`) looks for `client_secret=…` assignments,
|
|
812
|
+
`PINGCODE_CLIENT_ID` / `PINGCODE_CLIENT_SECRET` assignments with a real-looking value, `Bearer`
|
|
813
|
+
token literals, and tenant hosts (a `*.pingcode.com` subdomain containing a digit). The patterns
|
|
814
|
+
are deliberately keyword-anchored: a generic "hex id" rule would match every git sha and every
|
|
815
|
+
work-item id. Documented placeholders are ignored, and a line carrying `scan-secrets:allow` is
|
|
816
|
+
never reported.
|
|
817
|
+
|
|
818
|
+
`check:commits` (`scripts/check-commits.ts`) enforces
|
|
819
|
+
[`.trellis/spec/guides/commit-conventions.md`](.trellis/spec/guides/commit-conventions.md): the
|
|
820
|
+
`type(scope): subject` shape, the type table, a lowercase non-empty subject with no trailing period
|
|
821
|
+
and at most 72 characters. Merge commits are exempt, and on a pull request the PR title is checked
|
|
822
|
+
too because a squash merge turns it into the commit subject.
|
|
823
|
+
|
|
824
|
+
> **Node version note.** `skill:install`, `scan:secrets` and `check:commits` are TypeScript run
|
|
825
|
+
> through `node --experimental-strip-types`, which exists from Node **22.6** only. On the Node 20
|
|
826
|
+
> matrix leg the `skill:install --dry-run` step is therefore skipped, and the hygiene job runs on
|
|
827
|
+
> Node 24. `engines` still says `>=20` because the *published bundle* is built for Node 20 and is
|
|
828
|
+
> smoke-tested there; the restriction is on the repository's own scripts, not on the CLI.
|
|
829
|
+
|
|
830
|
+
### Git hooks
|
|
831
|
+
|
|
832
|
+
Native git hooks in [`.githooks/`](.githooks), pointed at by `core.hooksPath`. `npm install` wires
|
|
833
|
+
them up through `prepare`; to do it by hand:
|
|
834
|
+
|
|
835
|
+
```bash
|
|
836
|
+
npm run hooks:install # git config core.hooksPath .githooks
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
| Stage | What it runs |
|
|
840
|
+
| --- | --- |
|
|
841
|
+
| `pre-commit` | `npm run scan:secrets`, `npm run typecheck` |
|
|
842
|
+
| `commit-msg` | `npm run check:commits -- --file "$1"` |
|
|
843
|
+
| `pre-push` | `npm test`, `npm run build`, then the built bundle's `--version` and `--help` |
|
|
844
|
+
|
|
845
|
+
**A hook only ever runs a command CI also runs.** No rule lives in a hook alone, so `git commit
|
|
846
|
+
--no-verify` / `git push --no-verify` *defers* feedback to CI rather than skipping a check — which is
|
|
847
|
+
exactly what you want when you are mid-thought and the fix is one commit away. The split follows how
|
|
848
|
+
expensive a mistake is to undo: a leaked credential is the only irreversible one here (the
|
|
849
|
+
`client_secret` travels in a query string, and history already needed one sanitisation pass), so the
|
|
850
|
+
secret scan sits in the cheapest, most frequent gate. `check:commits` gained a `--file` mode for the
|
|
851
|
+
hook, because in CI it can only look at commits that already exist — a bad message is found after the
|
|
852
|
+
commit is written and the fix is a rebase, whereas `commit-msg` catches it before the commit is born.
|
|
853
|
+
The slow suite waits for `pre-push`, the point at which code starts reaching other people.
|
|
854
|
+
|
|
855
|
+
> **Caveat worth knowing: `pre-commit` validates the working tree, not the staged snapshot.** With
|
|
856
|
+
> unstaged changes present, `typecheck` and `scan:secrets` check something other than what is being
|
|
857
|
+
> committed. Stashing around the hook (`git stash --keep-index`) would fix that and is deliberately
|
|
858
|
+
> *not* implemented: an interrupted hook can then lose work, and a documented limitation beats an
|
|
859
|
+
> unexplainable failure mode. `scan:secrets` does enumerate `git ls-files`, so newly staged files are
|
|
860
|
+
> included — it just reads their contents from disk. CI has the last word either way.
|
|
861
|
+
|
|
862
|
+
The installer (`scripts/install-hooks.mjs`) no-ops when `CI` is set or when it is not inside a git
|
|
863
|
+
work tree, so it can never fail an install. It is plain `.mjs` rather than `.ts` like everything else
|
|
864
|
+
in `scripts/` for one reason: `prepare` runs during `npm ci`, including on the Node 20 leg, where
|
|
865
|
+
`--experimental-strip-types` does not exist.
|
|
866
|
+
|
|
867
|
+
**`.github/workflows/release.yml`** — on tags matching `v*`. To cut a release:
|
|
868
|
+
|
|
869
|
+
```bash
|
|
870
|
+
# 1. bump the version in package.json (src/version.ts is asserted to match by test/version.test.ts)
|
|
871
|
+
# 2. commit it, and make sure main is green
|
|
872
|
+
git tag -a v0.2.0 -m 'v0.2.0'
|
|
873
|
+
git push origin v0.2.0
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
The job first asserts `v<version>` equals `package.json`'s version and fails immediately on a
|
|
877
|
+
mismatch, then re-runs typecheck/test/build plus the binary smoke on Node 20, `npm pack`s the
|
|
878
|
+
tarball, and creates a GitHub Release with auto-generated notes and the tarball attached. It does
|
|
879
|
+
**not** push to the npm registry: the package name is unclaimed, so that stays a non-goal.
|
|
880
|
+
|
|
881
|
+
---
|
|
882
|
+
|
|
883
|
+
## Security notes
|
|
884
|
+
|
|
885
|
+
- Credentials and token live in `~/.pingcode/config.json` (mode `0600`), never in the repository.
|
|
886
|
+
- The `client_secret` travels in the **URL query string** on the token endpoint, so every printable
|
|
887
|
+
URL — `--verbose` logs, dry-run plans, error messages, body snippets — goes through `redactUrl()`.
|
|
888
|
+
`Authorization` headers and `access_token` / `client_secret` JSON values are masked too.
|
|
889
|
+
- `--verbose` prints request URLs with the full `client_id` visible, while `auth status` shows it
|
|
890
|
+
masked (`abcd…wxyz`). That asymmetry is intentional: the `client_id` is an identifier, not a
|
|
891
|
+
secret — only the `client_secret` and the access token are, and both are redacted everywhere.
|
|
892
|
+
|
|
893
|
+
---
|
|
894
|
+
|
|
895
|
+
## Known limitations / follow-ups
|
|
896
|
+
|
|
897
|
+
Recorded rather than forgotten. Three items that used to sit here are **done** and have been
|
|
898
|
+
retired: codegen from the apiDoc bundle (there is now a vendored 459-entry catalog, a generator and a
|
|
899
|
+
weekly drift watch), `POST /v1/pjm/work_items/search` (wired and live-verified — six flags switch
|
|
900
|
+
`project work-item list` to it), and bulk `PATCH /v1/pjm/work_items` (`project work-item bulk-update`).
|
|
901
|
+
|
|
902
|
+
- **Ergonomics is 158 of 459, and that is a backlog rather than a bug.** 301 endpoints are reachable
|
|
903
|
+
only through `pingcode api`. The gap is largest in `pjm` configuration (schemes and boards),
|
|
904
|
+
`directory` (departments, groups, roles, jobs) and `wiki` (0 of 19, by decision). Nothing is
|
|
905
|
+
*unreachable* because of it; see [Coverage](#coverage-reach-vs-ergonomics).
|
|
906
|
+
- **7 endpoints cannot be reached at all**: `/v1/myself`, `/v1/permission/my/*` and
|
|
907
|
+
`/v1/permission/check/*` need a *user* token, i.e. the OAuth2 authorization-code flow, which is not
|
|
908
|
+
implemented. They are refused before any request with an explanation. `GET /v1/permission/points`,
|
|
909
|
+
which looks like it belongs to that set, does work.
|
|
910
|
+
- **File attachments cannot be uploaded.** `POST /v1/attachments` has two documented forms: JSON for
|
|
911
|
+
a code snippet and `multipart/form-data` for a real file. Only the snippet form exists, by name
|
|
912
|
+
(`<entity> attachment add-snippet`) and generically (`pingcode api POST /v1/attachments`), because
|
|
913
|
+
a multipart body needs a change to the frozen transport layer that this work was not allowed to
|
|
914
|
+
make. So the *path* is reachable but the *file* form is not expressible in either layer — the one
|
|
915
|
+
place where "459 / 459" is about endpoints rather than about every documented request shape.
|
|
916
|
+
- **Keychain storage.** Credentials sit in a `0600` file; an OS keychain (Keychain Access,
|
|
917
|
+
libsecret, DPAPI) would be stronger.
|
|
918
|
+
- **`--json` drops `null` and `""` fields.** `api/parse.ts` normalises both to `undefined`, so they
|
|
919
|
+
vanish from the output; an absent key currently means "null, empty, or genuinely missing". `null`
|
|
920
|
+
→ absent is defensible, `""` → absent is not (an empty string is a value someone chose). The fix
|
|
921
|
+
— preserve both and reserve `undefined` for genuinely missing — is a **breaking output change**
|
|
922
|
+
and wants its own commit before there are consumers. Note this applies to refined commands only:
|
|
923
|
+
`pingcode api` passes the API's JSON through untouched.
|
|
924
|
+
- **`state_flows` pre-validation — tried on ship, and deliberately rolled back.** Reading the state
|
|
925
|
+
flow up front to reject illegal transitions locally sounds better than it is: live evidence
|
|
926
|
+
(`08-01-ship-cli/research/s7-smoke.md` F5) showed the server refuses atomically anyway, so
|
|
927
|
+
nothing is saved, while plan discovery is a scan, depends on an optional scope, and can
|
|
928
|
+
mis-identify the plan — turning a legal move into a terminal local refusal with no override. Ship
|
|
929
|
+
now reads the flows only to *explain* a refusal and to answer `--dry-run`. If pjm ever grows the
|
|
930
|
+
same feature, it should be advisory in the same way.
|
|
931
|
+
- **Self-hosted `--host` verification.** The `<host>/open` derivation is unit-tested only; it has
|
|
932
|
+
never been exercised against a real self-hosted instance.
|
|
933
|
+
- **429 and 403 paths are unit-tested only.** Provoking a real 429 means ~200 requests/minute
|
|
934
|
+
against a production org, and the token used for the live verification was org-admin-scoped, so
|
|
935
|
+
nothing ever denied it with a 403.
|
|
936
|
+
- **Smoke data cannot be cleaned up.** Ship exposes no DELETE at all, and neither do projects,
|
|
937
|
+
sprints or test libraries, so anything created while verifying against a live tenant is permanent.
|
|
938
|
+
Prefix it before you create it.
|
|
939
|
+
|
|
940
|
+
Requirements, design and the live-API findings live under `.trellis/tasks/` — the original MVP in
|
|
941
|
+
`07-31-pingcode-cli-mvp/` (`prd.md`, `design.md`, `research/pingcode-api.md`, `research/s8-smoke.md`)
|
|
942
|
+
and the full-coverage work in `08-02-full-api-coverage/`, whose `research/open-api-surface-460.md` is
|
|
943
|
+
the endpoint-by-endpoint map of all 459 and whose `design.md` records every live finding per module.
|