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,222 @@
1
+ # 构建与部署 (build / release) — `build`, `release`
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 build and deployment write-back surface and
6
+ > the ids it needs.
7
+
8
+ **What these two modules are for.** They are the other half of the *write-back* API
9
+ [`scm.md`](scm.md) describes: a CI/CD job tells PingCode "this build ran" and "this release
10
+ was deployed", and PingCode shows those facts on the work items involved. Nothing here runs
11
+ a pipeline, watches one, or reads anything from your CI server — every row you create is a
12
+ record inside PingCode that you wrote.
13
+
14
+ **Two separate groups, because they are two separate APIs** with different scopes:
15
+
16
+ ```
17
+ build 构建记录 /v1/build/builds pcp:*:devops:build
18
+ release 环境 /v1/release/environments ┐
19
+ 部署 /v1/release/deploys ┘ pcp:*:devops:deploy
20
+ ```
21
+
22
+ **The write-back path.** Both groups are flat — there is no platform, project or product to
23
+ resolve first, unlike scm:
24
+
25
+ ```
26
+ build create ─────────────────────┐
27
+ ├→ --work-item PLM-001 (that is the only link)
28
+ release env create → deploy create ┘
29
+ ```
30
+
31
+ An environment is standing configuration (create it once); a build and a deploy are events
32
+ (one row per run).
33
+
34
+ **Three group-wide facts.**
35
+
36
+ - Both groups are **企业令牌 only** — which is exactly the token this CLI holds, so they work
37
+ out of the box. `build` needs `pcp:read:devops:build` / `pcp:write:devops:build`;
38
+ `release` needs `pcp:read:devops:deploy` / `pcp:write:devops:deploy` for **both** its
39
+ subgroups. These are *separate* from scm's `devops:code`: a token that can write commits
40
+ cannot write builds, and the only symptom is exit 4.
41
+ - **`--work-item` is the entire integration.** Neither group has any other reference to
42
+ PingCode data — no project, no product, no repository. A build or deploy with no
43
+ `--work-item` is invisible outside its own list.
44
+ - **Everything is organisation-level.** No `--platform`, no `--project`, no `--product` on
45
+ any leaf. Do not look for one.
46
+
47
+ ### Build records — `build`
48
+
49
+ ```bash
50
+ pingcode build list --json
51
+ pingcode build list --page 1 --page-size 20 --json
52
+ pingcode build get 6a70c1eb919cce9794f01acb --json
53
+ pingcode build create --name unit-test --identifier 131 --provider jenkins --status success \
54
+ --start-at 1785700000 --end-at 1785700038 --duration 38 --dry-run --json
55
+ pingcode build create --name unit-test --identifier 131 --provider jenkins --status success \
56
+ --start-at 2026-08-04T09:00:00Z --end-at 2026-08-04T09:00:38Z --duration 38 \
57
+ --job-url https://ci.example.com/job/131 --result-overview "1000 test cases pass" \
58
+ --work-item PLM-001 --json
59
+ pingcode build update 6a70c1eb919cce9794f01acb --status failure --json
60
+ pingcode build delete 6a70c1eb919cce9794f01acb --yes --json
61
+ ```
62
+
63
+ **`build list` has no filters. None.** The endpoint documents no query parameter, and
64
+ `identifier`, `name`, `status`, `provider` and `work_item_id` were each tried against the
65
+ live API and **silently ignored** — every row came back every time. So the CLI offers only
66
+ `--page` / `--page-size` / `--all`, and any list is a whole-organisation scan. **Keep the
67
+ `id` that `build create --json` returns**; it is the only cheap way back to a build.
68
+
69
+ **The build number (`--identifier`) is not a key.** Two builds may carry `131` and the API
70
+ accepts both. `build get` therefore takes an **id**, never a number, and there is no name
71
+ resolution to fall back on.
72
+
73
+ Seven flags are required on `create` because the API requires all seven: `--name`,
74
+ `--identifier`, `--provider`, `--status`, `--start-at`, `--end-at`, `--duration`. In
75
+ particular **`--duration` is not derived** from the two timestamps — the server never
76
+ computes it, so report whatever your pipeline measured.
77
+
78
+ - `--provider` is a closed enum: `bamboo | bitbucket | jenkins | other`. Anything else is
79
+ exit 7 from the server, not exit 2 from the CLI. There is no `github-actions` or
80
+ `gitlab-ci` value; use `other`.
81
+ - `--status` is `success | failure`. There is no "running", "cancelled" or "unstable" — a
82
+ build in flight is usually recorded once it finishes, or created and then moved with
83
+ `build update --status`.
84
+ - Timestamps take unix **seconds** or a date string. Milliseconds are rejected by the server
85
+ (exit 7, `数值不是有效的时间戳`), so pass seconds or let the CLI convert a date for you.
86
+
87
+ `build delete` exists — the only delete in the whole DevOps surface apart from
88
+ `scm branch delete` — because a build record can simply be written again. It requires
89
+ `--yes`, it reads the record first so the confirmation names it (`the build #131
90
+ "unit-test"`), and it takes no `--all`. It is a hard delete: a following `get` is exit 5.
91
+ Unlike deleting an scm branch, it takes nothing else with it.
92
+
93
+ There is **no `build replace`**. `PUT /v1/build/builds/{id}` exists upstream but would blank
94
+ every field you did not send, so it is reachable only as
95
+ `pingcode api PUT /v1/build/builds/<id>` — where you are explicitly asking for a full
96
+ replacement.
97
+
98
+ ### Deploy environments — `release env`
99
+
100
+ ```bash
101
+ pingcode release env list --json
102
+ pingcode release env list --name production --json
103
+ pingcode release env get production --json
104
+ pingcode release env create --name production --html-url https://app.example.com --json
105
+ pingcode release env update production --html-url https://app.example.com/status --json
106
+ pingcode resolve release-env production --json | jq -r .id
107
+ ```
108
+
109
+ **An environment name is a complete address.** Names are unique per organisation and
110
+ `--name` is an exact (case-insensitive) filter that upstream really honours — so unlike
111
+ most ids in this API, you can pass the name everywhere an environment is expected, and
112
+ `pingcode resolve release-env <name>` turns it into the id `pingcode api` wants. A duplicate
113
+ create is exit 7 (`环境已经存在`).
114
+
115
+ Two things to know before scripting an update:
116
+
117
+ - **`--html-url` cannot be cleared.** The server validates it as a URL and rejects an empty
118
+ value, so a link can be replaced but never removed.
119
+ - The resource is only `{id, name, html_url}` — there is nothing else to patch.
120
+
121
+ There is **no `release env delete` leaf**, and this is *not* because the API lacks one: the
122
+ endpoint exists and works. It is simply not wrapped yet. Until it is:
123
+
124
+ ```bash
125
+ pingcode api DELETE /v1/release/environments/<id> --yes
126
+ ```
127
+
128
+ The server **refuses** that while any deploy still references the environment (exit 7,
129
+ `'environment'正在使用,不能被删除`) — delete those deploys first. That refusal is good news:
130
+ unlike an scm branch, whose deletion orphans its commit refs permanently, nothing in
131
+ `release` can be left dangling.
132
+
133
+ ### Deployment records — `release deploy`
134
+
135
+ ```bash
136
+ pingcode release deploy list --json
137
+ pingcode release deploy list --env production --json
138
+ pingcode release deploy get 6a70c153919cce9794f01aca --json
139
+ pingcode release deploy create --env production --status deployed --release-name 1.4.0 \
140
+ --start-at 1785700000 --end-at 1785700200 --duration 200 --dry-run --json
141
+ pingcode release deploy create --env production --status deployed --release-name 1.4.0 \
142
+ --start-at 2026-08-04T09:00:00Z --end-at 2026-08-04T09:03:20Z --duration 200 \
143
+ --release-url https://github.com/acme/app/releases/tag/1.4.0 --work-item PLM-001 --json
144
+ pingcode release deploy update 6a70c153919cce9794f01aca --status deployed --json
145
+ ```
146
+
147
+ `--env <name|id>` and `--env-id <id>` are the usual pair: the name resolves (and a typo is
148
+ exit 2 that lists the real environments), the id is sent unchanged. One of them is required
149
+ on `create`; on `list` it is the **only** filter that works — `status`, `release_name` and
150
+ `work_item_id` were tried live and silently ignored.
151
+
152
+ ⚠️ **On `list`, an unknown `--env-id` gives you an empty list, not an error.** The API answers
153
+ 200 with zero rows, so silence does not distinguish "nothing deployed there" from "no such
154
+ environment". Use `--env <name>` instead and a bad name fails loudly. (`release deploy
155
+ create` does report a missing environment properly: exit 5.)
156
+
157
+ Six flags are required on `create`, for the same reason as `build`: `--status`,
158
+ `--release-name`, `--start-at`, `--end-at`, `--duration`, plus the environment.
159
+
160
+ - `--status` has exactly two values: `not_deployed | deployed`. There is **no failed or
161
+ rolled-back state** — record a rollback as another deploy of the previous release.
162
+ - `--release-name` is free text (`1.4.0`, a tag, a commit subject) and is **not unique**, so
163
+ it is not a lookup key. `release deploy get` takes an id.
164
+
165
+ ⚠️ **A deploy cannot be moved between environments, and the API pretends otherwise.**
166
+ `release deploy update` has no `--env` on purpose: the endpoint documents `env_id` as
167
+ updatable and *accepts* it — it returns 200 **and echoes the new environment back in the
168
+ response** — but the change is never stored, and a following `get` shows the original
169
+ environment. So the usual "read it back to be sure" habit does not protect you here; the
170
+ response itself is wrong. Record a new deploy on the right environment instead, and remove
171
+ the stray one with `pingcode api DELETE /v1/release/deploys/<id> --yes`.
172
+
173
+ ⚠️ **Moving a deploy's time window needs two calls, `--end-at` first.** A new `--start-at` is
174
+ validated against the **stored** `--end-at`, not against one sent in the same request, so
175
+ pushing a window forward in a single update is exit 7
176
+ (`开始时间必须小于等于已存在的结束时间`). Extend the end, then move the start. Reversing the two
177
+ on a `create` is exit 7 as well (`开始时间必须小于等于结束时间`).
178
+
179
+ No `release deploy replace` (the `PUT` would blank omitted fields — use
180
+ `pingcode api PUT /v1/release/deploys/<id>` deliberately) and no `release deploy delete`
181
+ leaf, though the endpoint exists and works:
182
+ `pingcode api DELETE /v1/release/deploys/<id> --yes`.
183
+
184
+ ### Linking to work items — the trap both groups share
185
+
186
+ `--work-item` takes a work item **identifier** (`PLM-001`), repeatable, and it is the only
187
+ thing that connects a build or a deploy to anything in PingCode.
188
+
189
+ ⚠️ **An identifier that does not exist is silently dropped and the call still returns 200.**
190
+ Mixed input links the ones that exist and ignores the rest, so **the exit code cannot tell
191
+ you whether the link happened**. The CLI compares what you asked for against the response
192
+ and prints a warning on stderr naming the identifiers that did not land — the exit code
193
+ stays 0, because the write itself succeeded. Under `--json` the authoritative `work_items`
194
+ array is on stdout: read it if you care.
195
+
196
+ On `update`, `--work-item` **replaces** the whole link set, it does not merge. Passing none
197
+ leaves the existing links alone.
198
+
199
+ Two contrasts with [`scm.md`](scm.md) worth stating, because they are the kind of thing
200
+ that gets copied across modules by mistake:
201
+
202
+ - **Nothing in these two groups upserts an identity.** scm's `--sender`, `--owner-name`,
203
+ `--creator`, `--merged-by` and `--reviewer` each create a permanent 托管平台用户 from a
204
+ typo. No field here is a name reference of that kind, so there is no ghost-identity
205
+ hazard to warn about.
206
+ - **A missing parent is reported honestly on a create** (`release deploy create --env-id
207
+ <unknown>` is exit 5), unlike `scm review list` under an unknown pull request. The one
208
+ place silence hides an absence here is `release deploy list --env-id`, called out above.
209
+
210
+ ### What is not here
211
+
212
+ | Wanted | Reality |
213
+ |---|---|
214
+ | filter builds by status, work item, provider… | the endpoint honours no filter at all; page through `build list` or keep the id |
215
+ | look a build up by its build number | numbers are not unique; use the id |
216
+ | a "running"/"cancelled" build status | the enum is `success | failure` only |
217
+ | a failed or rolled-back deploy status | the enum is `not_deployed | deployed` only |
218
+ | `build replace` / `release … replace` | `PUT` is generic-layer only: `pingcode api PUT <path>` |
219
+ | `release env delete` / `release deploy delete` | the endpoints exist and work, but are not wrapped yet: `pingcode api DELETE <path> --yes` |
220
+ | move a deploy to another environment | not possible: `env_id` is accepted on PATCH, echoed back, and ignored — create a new deploy |
221
+ | attach a build to a project, sprint or release version | not in this API; `--work-item` is the only link |
222
+ | trigger, cancel or poll a pipeline | not in this API at all — these are records, not controls |
@@ -0,0 +1,168 @@
1
+ # 跨对象资源 — relations / comments / attachments / activities
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 cross-object surface.
6
+
7
+ Four families are polymorphic over the object they hang on: **关联 relations**, **评论 comments**,
8
+ **附件 attachments** and **活动记录 activities**. They are the glue between the four modules — a
9
+ requirement linked to a work item linked to a test case linked to a defect — and they are the
10
+ only write-back channel an automated flow has for prose.
11
+
12
+ They live **under the entity they belong to**, so the entity's own command path supplies
13
+ `principal_type` and you never type it:
14
+
15
+ ```bash
16
+ pingcode project work-item comment add <work-item> --text "CI #123 failed, filed BUG-45"
17
+ pingcode product idea relation list <idea> --target-type work_item
18
+ pingcode testhub cases relation add <case> --target-type work_item --target-id <id>
19
+ pingcode testhub runs activity list <run>
20
+ pingcode product ticket attachment list <ticket>
21
+ ```
22
+
23
+ There is deliberately **no top-level `comment` group**: it would have to ask you for
24
+ `--principal-type`, which is a value you can get wrong. If you need the generic form anyway, it
25
+ already exists: `pingcode api GET /v1/comments --query principal_type=work_item --query
26
+ principal_id=<id>`.
27
+
28
+ ## 1. Where they are mounted, and where they are not
29
+
30
+ | mount | `principal_type` | relation | comment | attachment | activity |
31
+ |---|---|---|---|---|---|
32
+ | `pingcode project work-item …` | `work_item` | yes | yes | yes | yes |
33
+ | `pingcode product idea …` | `idea` | yes | yes | yes | yes |
34
+ | `pingcode product ticket …` | `ticket` | yes | yes | yes | yes |
35
+ | `pingcode testhub cases …` | `test_case` | yes | yes | yes | yes |
36
+ | `pingcode testhub runs …` | `test_run` | yes | yes | yes | yes |
37
+
38
+ **A test plan is not an object these families accept.** There is no
39
+ `pingcode testhub plans comment`, and there is no generic way to fake one: the API rejects
40
+ `principal_type=test_plan` outright, and `/v1/activities` answers it with an HTTP 500 rather than
41
+ a 4xx. Comment on the plan's runs instead.
42
+
43
+ A wiki page (`principal_type=page`) is accepted by comments and attachments but **not** by
44
+ activities, and 知识库 wiki has no command group of its own, so reach it through
45
+ `pingcode api … --query principal_type=page`.
46
+
47
+ ## 2. Every command takes the parent object first
48
+
49
+ ```bash
50
+ pingcode project work-item comment get <work-item> <comment-id>
51
+ pingcode project work-item comment delete <work-item> <comment-id> --yes
52
+ ```
53
+
54
+ Two positional arguments, always in that order, for all four families. The reason is the API's,
55
+ not ours: `GET /v1/comments/{id}` and `GET /v1/attachments/{id}` **require** the principal in the
56
+ query — a comment id alone is not addressable. `relation get` / `relation delete` are the one
57
+ exception under the hood (a relation id is globally addressable), but they still take the parent
58
+ reference so the four families have one shape; it is accepted and not sent, and costs no request.
59
+
60
+ The parent reference accepts whatever that entity's own `get` accepts — an id, a `short_id`, an
61
+ identifier such as `SCR-5`, a pasted URL — and is resolved to a real id first, because **no
62
+ cross-object endpoint accepts anything but an id**.
63
+
64
+ ## 3. relations: cross-*kind* only, and the pairs are asymmetric
65
+
66
+ `POST /v1/relations` takes no relation type of any kind. What it does take is a pair of kinds,
67
+ and it refuses some pairs — including every same-kind work-item pair:
68
+
69
+ | from ↓ | `work_item` | `idea` | `ticket` | `test_case` | `test_run` | `page` |
70
+ |---|---|---|---|---|---|---|
71
+ | `work_item` | **no** | yes | yes | yes | no | yes |
72
+ | `idea` | yes | yes | yes | yes | no | yes |
73
+ | `ticket` | yes | yes | yes | **no** | no | yes |
74
+ | `test_case` | yes | yes | **no** | no | no | yes |
75
+ | `test_run` | yes | no | no | no | no | no |
76
+
77
+ Read that table as *observed*, not *documented*: the API declares no vocabulary for either field,
78
+ so this is what a live tenant accepted. The CLI refuses nothing locally — it sends what you ask
79
+ and explains the rejection.
80
+
81
+ **There is a second filter the table cannot show: the work item's *type*.** Creating a link
82
+ *from* a test case, only a 需求 (story) or 缺陷 (bug) target is accepted; *from* a test run, only
83
+ a 缺陷. An epic, feature or task target is rejected with `不支持的工作项类型` (`100107`) — while the
84
+ same link created *from the work-item side* succeeds for any type. So if you are linking a case or
85
+ a run to a feature, create the link from the work item instead:
86
+
87
+ ```bash
88
+ # refused (100107): testhub cases relation add <case> --target-type work_item --target-id <feature>
89
+ pingcode project work-item relation add <feature> --target-type test_case --target-id <case>
90
+ ```
91
+
92
+ Three consequences worth internalising:
93
+
94
+ 1. **work item ↔ work item is a different family.** Those links are typed (阻塞 / 重复 / 关联 …)
95
+ and live on `POST /v1/pjm/work_items/{id}/relations` with a `relation_type`. Reach them with
96
+ `pingcode api POST /v1/pjm/work_items/<id>/relations --set target_work_item_id=<id> --set
97
+ relation_type=<type>`, and list the types with
98
+ `pingcode api GET /v1/pjm/work_item/relation_types`.
99
+ 2. **Direction matters even though the link does not.** `test_run → work_item` is accepted;
100
+ `work_item → test_run` is not. Once created, the link is stored as a mirrored pair and shows up
101
+ from both ends — with a *different* id on each side. Deleting either id removes both.
102
+ 3. **`--target-type` is mandatory on `relation list`.** It reads like a filter and is not: omit it
103
+ and the API rejects the call.
104
+
105
+ When a pair is refused, the API answers `不支持的'principal_type'` (code `100049`) **whatever was
106
+ actually wrong** — a bad target kind, or no target kind at all. The CLI prints the pair it tried
107
+ and the kinds this principal does link to; trust that line over the server's message.
108
+
109
+ ## 4. comments: delete is a soft delete
110
+
111
+ ```bash
112
+ pingcode project work-item comment add <work-item> --text "…" --reply-to <comment-id>
113
+ pingcode project work-item comment list <work-item> --all
114
+ ```
115
+
116
+ `comment delete` **does not remove the row.** It stays in `comment list` with `is_deleted` set, and
117
+ the table's `STATE` column shows `deleted`. Read that column and nothing else: whether the text
118
+ survives depends on the module — a work-item comment comes back empty, a ticket comment keeps its
119
+ body — so neither "has content" nor "has none" tells you whether a comment is live.
120
+
121
+ ## 5. attachments: snippets only, and always under a comment
122
+
123
+ There are two upload shapes in the API and the CLI can only send one of them:
124
+
125
+ - **files** are `multipart/form-data` in a single request. This CLI cannot produce that body, so
126
+ `attachment add` does not exist. Read, list and delete work normally on files uploaded through
127
+ the web UI.
128
+ - **code snippets** are JSON, and `attachment add-snippet` sends them.
129
+
130
+ ```bash
131
+ pingcode project work-item comment add <work-item> --text "build log"
132
+ pingcode project work-item attachment add-snippet <work-item> \
133
+ --comment-id <comment-id> --title main.go --format go --content-file ./main.go
134
+ ```
135
+
136
+ **`--comment-id` is required**, on the write and on every subsequent read. The docs call it
137
+ optional; live, a snippet posted without it is rejected (`100039`), and a snippet that exists is
138
+ reported as *not found* (`100045`, exit 5) if you read or delete it without the scope. Create the
139
+ comment first, keep its id.
140
+
141
+ `--format` is a closed vocabulary — `clike css dart django dockerfile go markdown nginx python php
142
+ shell sql swift html javascript jsx pascal sass stylus vue yaml haskell` — and anything else is
143
+ rejected as a malformed request. `--content` takes the text inline, `--content-file` reads a file
144
+ verbatim; exactly one of the two.
145
+
146
+ Unlike a comment, `attachment delete` is permanent.
147
+
148
+ ## 6. activities: the only change feed there is
149
+
150
+ Read-only, and per-object. This API has **no webhook API and no global activity stream**, so
151
+ polling `activity list` on the objects you care about is the only way to notice a change.
152
+
153
+ `EVENT` is the machine-readable name (`unrelate-test-case`) and its verb (`unrelate`); `SUMMARY`
154
+ is Chinese prose written for humans and is **not** a contract — never match on it. The `content`
155
+ object differs per event type and is passed through untouched, so read it from `--json`.
156
+
157
+ ## 7. Exit codes you will actually see here
158
+
159
+ | what happened | code | exit |
160
+ |---|---|---|
161
+ | parent reference could not be resolved | — | 2 |
162
+ | `--yes` missing on a delete | — | 2 |
163
+ | unsupported `principal_type`, or a refused pair, or a missing `--target-type` | `100049` | 7 |
164
+ | snippet without `--comment-id`, or an unknown `--format` | `100039` | 7 |
165
+ | work item on the other end has a type this direction refuses | `100107` | 7 |
166
+ | comment / attachment / activity / relation id does not exist | `100051` / `100045` / `100077` / `100801` | 5 |
167
+ | parent object does not exist (work item) | `100317` | 5 |
168
+ | `principal_type=test_plan` on `activities` | — | 8-ish: HTTP 500, do not do this |