@knpkv/jira-cli 1.2.3 → 1.4.0
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/CHANGELOG.md +133 -0
- package/README.md +64 -0
- package/dist/AttachmentService.d.ts +2 -2
- package/dist/AttachmentService.d.ts.map +1 -1
- package/dist/AttachmentService.js +1 -1
- package/dist/AttachmentService.js.map +1 -1
- package/dist/IssueService.d.ts +82 -3
- package/dist/IssueService.d.ts.map +1 -1
- package/dist/IssueService.js +111 -16
- package/dist/IssueService.js.map +1 -1
- package/dist/JiraAuth.d.ts +3 -3
- package/dist/JiraAuth.d.ts.map +1 -1
- package/dist/JiraAuth.js +87 -23
- package/dist/JiraAuth.js.map +1 -1
- package/dist/MarkdownWriter.d.ts +4 -4
- package/dist/MarkdownWriter.d.ts.map +1 -1
- package/dist/MarkdownWriter.js +2 -2
- package/dist/MarkdownWriter.js.map +1 -1
- package/dist/SyncWorkspace.d.ts +2 -2
- package/dist/SyncWorkspace.d.ts.map +1 -1
- package/dist/SyncWorkspace.js.map +1 -1
- package/dist/VersionService.d.ts +81 -9
- package/dist/VersionService.d.ts.map +1 -1
- package/dist/VersionService.js +113 -17
- package/dist/VersionService.js.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/commands/auth.d.ts +13 -2
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +7 -7
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/edit.d.ts +22 -0
- package/dist/commands/edit.d.ts.map +1 -0
- package/dist/commands/edit.js +49 -0
- package/dist/commands/edit.js.map +1 -0
- package/dist/commands/errorHandler.d.ts.map +1 -1
- package/dist/commands/get.d.ts +6 -1
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +3 -3
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/issue.d.ts +2 -2
- package/dist/commands/issue.d.ts.map +1 -1
- package/dist/commands/issue.js +3 -2
- package/dist/commands/issue.js.map +1 -1
- package/dist/commands/issueAttachment.d.ts +1 -1
- package/dist/commands/issueAttachment.d.ts.map +1 -1
- package/dist/commands/issueAttachment.js +13 -11
- package/dist/commands/issueAttachment.js.map +1 -1
- package/dist/commands/layers.d.ts +3 -3
- package/dist/commands/layers.d.ts.map +1 -1
- package/dist/commands/layers.js +5 -2
- package/dist/commands/layers.js.map +1 -1
- package/dist/commands/search.d.ts +6 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +7 -7
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/version.d.ts +17 -2
- package/dist/commands/version.d.ts.map +1 -1
- package/dist/commands/version.js +131 -19
- package/dist/commands/version.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/NodeLayers.js +1 -1
- package/dist/internal/NodeLayers.js.map +1 -1
- package/dist/internal/attachmentInsertion.d.ts.map +1 -1
- package/dist/internal/frontmatter.d.ts.map +1 -1
- package/dist/internal/frontmatter.js +2 -1
- package/dist/internal/frontmatter.js.map +1 -1
- package/dist/internal/jqlBuilder.d.ts.map +1 -1
- package/dist/internal/oauthServer.d.ts +10 -4
- package/dist/internal/oauthServer.d.ts.map +1 -1
- package/dist/internal/oauthServer.js +13 -8
- package/dist/internal/oauthServer.js.map +1 -1
- package/dist/internal/openBrowser.d.ts +1 -1
- package/dist/internal/openBrowser.d.ts.map +1 -1
- package/dist/internal/openBrowser.js +1 -1
- package/dist/internal/openBrowser.js.map +1 -1
- package/dist/internal/sync/baseline.d.ts.map +1 -1
- package/dist/internal/sync/baseline.js +2 -2
- package/dist/internal/sync/baseline.js.map +1 -1
- package/dist/internal/sync/changes.d.ts.map +1 -1
- package/dist/internal/sync/config.d.ts.map +1 -1
- package/dist/internal/sync/config.js +1 -1
- package/dist/internal/sync/config.js.map +1 -1
- package/dist/internal/sync/document.d.ts.map +1 -1
- package/dist/internal/sync/document.js +8 -8
- package/dist/internal/sync/document.js.map +1 -1
- package/dist/internal/sync/fieldValues.d.ts +9 -9
- package/dist/internal/sync/fieldValues.d.ts.map +1 -1
- package/dist/internal/sync/fieldValues.js +12 -16
- package/dist/internal/sync/fieldValues.js.map +1 -1
- package/dist/internal/sync/manifest.d.ts.map +1 -1
- package/dist/internal/sync/manifest.js +1 -1
- package/dist/internal/sync/manifest.js.map +1 -1
- package/dist/internal/sync/paths.d.ts.map +1 -1
- package/dist/internal/sync/schemas.d.ts +17 -9
- package/dist/internal/sync/schemas.d.ts.map +1 -1
- package/dist/internal/sync/schemas.js +6 -6
- package/dist/internal/sync/schemas.js.map +1 -1
- package/dist/internal/sync/types.d.ts +4 -4
- package/dist/internal/sync/types.d.ts.map +1 -1
- package/dist/internal/sync/types.js +1 -1
- package/dist/internal/sync/types.js.map +1 -1
- package/package.json +15 -11
- package/skills/jira/SKILL.md +17 -1
- package/src/AttachmentService.ts +6 -6
- package/src/IssueService.ts +214 -27
- package/src/JiraAuth.ts +106 -42
- package/src/MarkdownWriter.ts +6 -6
- package/src/SyncWorkspace.ts +12 -11
- package/src/VersionService.ts +199 -27
- package/src/bin.ts +1 -1
- package/src/commands/auth.ts +7 -7
- package/src/commands/edit.ts +88 -0
- package/src/commands/get.ts +3 -3
- package/src/commands/issue.ts +3 -2
- package/src/commands/issueAttachment.ts +21 -20
- package/src/commands/layers.ts +5 -2
- package/src/commands/search.ts +7 -7
- package/src/commands/version.ts +176 -23
- package/src/index.ts +13 -3
- package/src/internal/NodeLayers.ts +1 -1
- package/src/internal/frontmatter.ts +3 -2
- package/src/internal/oauthServer.ts +29 -16
- package/src/internal/openBrowser.ts +1 -1
- package/src/internal/sync/baseline.ts +3 -2
- package/src/internal/sync/config.ts +2 -1
- package/src/internal/sync/document.ts +15 -14
- package/src/internal/sync/fieldValues.ts +21 -22
- package/src/internal/sync/manifest.ts +1 -1
- package/src/internal/sync/schemas.ts +6 -6
- package/src/internal/sync/types.ts +4 -4
- package/test/IssueService.test.ts +2 -2
- package/test/JiraAuthRefresh.test.ts +269 -0
- package/test/VersionService.test.ts +2 -2
- package/test/commandTree.test.ts +158 -2
- package/test/editIssuePayload.test.ts +103 -0
- package/test/frontmatter.test.ts +4 -2
- package/test/integration.test.ts +12 -10
- package/test/oauthServer.test.ts +77 -4
- package/test/publicApi.test.ts +34 -0
- package/test/relatedWorkSync.test.ts +179 -0
- package/test/remoteWrites.test.ts +220 -0
- package/test/syncChanges.test.ts +2 -2
- package/test/syncConfig.test.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,138 @@
|
|
|
1
1
|
# @knpkv/jira-cli
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#452](https://github.com/knpkv/npm/pull/452) [`755eafa`](https://github.com/knpkv/npm/commit/755eafab8c0bc3e82b00e2dd27c68d669a1de87e) Thanks [@konopkov](https://github.com/konopkov)! - Upgrade to Effect 4.0.0 stable. `effect` and every `@effect/*` dependency or peer dependency now require `4.0.0`; imports move from the removed `effect/unstable/*` paths to `effect/*`.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#452](https://github.com/knpkv/npm/pull/452) [`755eafa`](https://github.com/knpkv/npm/commit/755eafab8c0bc3e82b00e2dd27c68d669a1de87e) Thanks [@konopkov](https://github.com/konopkov)! - Keep boolean flags such as `--dry-run`, `--json`, and `--force` optional. Effect 4.0.0 made omitted boolean flags required, so each now defaults to `false`.
|
|
12
|
+
|
|
13
|
+
- [#438](https://github.com/knpkv/npm/pull/438) [`acb8b25`](https://github.com/knpkv/npm/commit/acb8b25772cc188a0cc1299a1b591903240cfc7c) Thanks [@github-actions](https://github.com/apps/github-actions)! - Update the generated Schema-backed Jira API client.
|
|
14
|
+
|
|
15
|
+
Breaking: these exported types now include `null`, so code that reads them must handle it: `ApprovalConfiguration`, `BoardFeaturesPayload`, `BoardsPayload`, `ConditionGroupConfiguration`, `ConditionGroupUpdate`, `CustomFieldPayload`, `FieldCapabilityPayload`, `FieldLayoutPayload`, `FieldLayoutSchemePayload`, `FieldSchemePayload`, `IssueLayoutPayload`, `IssueTypeHierarchyPayload`, `IssueTypePayload`, `IssueTypeProjectCreatePayload`, `IssueTypeScreenSchemePayload`, `NotificationSchemePayload`, `PermissionPayloadDTO`, `PreviewConditionGroupConfiguration`, `PreviewRuleConfiguration`, `ProjectId`, `RolesCapabilityPayload`, `ScopePayload`, `ScreenPayload`, `ScreenSchemePayload`, `SecuritySchemePayload`, `TargetClassification`, `TargetMandatoryFields`, `TargetStatus`, `WorkflowCapabilityPayload`, `WorkflowLayout`, `WorkflowProjectIdScope`, `WorkflowRuleConfiguration`, `WorkflowStatusLayout`, and `WorkflowTransitionLinks`. `ProjectId` and `WorkflowLayout` also appear in responses. No exports are removed; 129 are added.
|
|
16
|
+
|
|
17
|
+
- [#376](https://github.com/knpkv/npm/pull/376) [`379b029`](https://github.com/knpkv/npm/commit/379b02947180d50d4a40cf4b7723851aa68fc183) Thanks [@konopkov](https://github.com/konopkov)! - Depend on the patched `js-yaml` 5.4.2 directly, matching the workspace security override, so installs and packed consumers resolve the version the manifest names.
|
|
18
|
+
- Updated dependencies [[`755eafa`](https://github.com/knpkv/npm/commit/755eafab8c0bc3e82b00e2dd27c68d669a1de87e), [`379b029`](https://github.com/knpkv/npm/commit/379b02947180d50d4a40cf4b7723851aa68fc183), [`acb8b25`](https://github.com/knpkv/npm/commit/acb8b25772cc188a0cc1299a1b591903240cfc7c), [`755eafa`](https://github.com/knpkv/npm/commit/755eafab8c0bc3e82b00e2dd27c68d669a1de87e)]:
|
|
19
|
+
- @knpkv/atlassian-common@1.5.0
|
|
20
|
+
- @knpkv/jira-api-client@2.0.0
|
|
21
|
+
- @knpkv/agent-skills@0.3.2
|
|
22
|
+
|
|
23
|
+
## 1.3.1
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- [#361](https://github.com/knpkv/npm/pull/361) [`676419e`](https://github.com/knpkv/npm/commit/676419e39c395dd4cfea6d9ffaee7d002a3f75e2) Thanks [@konopkov](https://github.com/konopkov)! - Upgrade the workspace to Effect 4.0.0-rc.109, pin the vendored Effect reference to that exact upstream release, guard source/package alignment, and bound Control Center test concurrency for reliable CI execution.
|
|
28
|
+
- Updated dependencies [[`676419e`](https://github.com/knpkv/npm/commit/676419e39c395dd4cfea6d9ffaee7d002a3f75e2)]:
|
|
29
|
+
- @knpkv/agent-skills@0.3.1
|
|
30
|
+
- @knpkv/atlassian-common@1.4.1
|
|
31
|
+
- @knpkv/jira-api-client@1.1.1
|
|
32
|
+
|
|
33
|
+
## 1.3.0
|
|
34
|
+
|
|
35
|
+
### Minor Changes
|
|
36
|
+
|
|
37
|
+
- [#366](https://github.com/knpkv/npm/pull/366) [`b08ca20`](https://github.com/knpkv/npm/commit/b08ca2004b3efcd72a695b44c72b56dae20afdfd) Thanks [@konopkov](https://github.com/konopkov)! - Add `jira version create` and `jira issue edit`, closing the two gaps that forced release scaffolding out of the CLI.
|
|
38
|
+
|
|
39
|
+
`jira version create --project <KEY> --name <NAME>` opens a new unreleased version, optionally with `--description`, `--start-date` and `--release-date`. The project key is resolved to the numeric `projectId` the endpoint requires, and dates are validated as ISO 8601 locally so a bad one names its own flag instead of returning an unattributed 400.
|
|
40
|
+
|
|
41
|
+
`jira issue edit <KEY>` edits fix versions and labels. Both fields are sets, so the incremental flags — `--add-fix-version`, `--remove-fix-version`, `--add-label`, `--remove-label` — are the ones to reach for: they go through Jira's `update` verb, which applies server-side and cannot clobber a concurrent edit. The replacing forms (`--fix-version`, `--label`) are still available and say in their help that they drop anything not listed. Passing both forms for one field is refused up front, because Jira's own error for that case does not name the offending field.
|
|
42
|
+
|
|
43
|
+
- [#354](https://github.com/knpkv/npm/pull/354) [`2e26e30`](https://github.com/knpkv/npm/commit/2e26e3032ce527260a4e4d9fca8af43039f762d6) Thanks [@konopkov](https://github.com/konopkov)! - Add `jira version related-work sync` — reconcile a version's related-work links
|
|
44
|
+
against the given set instead of blindly appending.
|
|
45
|
+
|
|
46
|
+
Repeated `related-work add` calls pile up duplicate "Release notes" links every
|
|
47
|
+
time a release is re-scaffolded. `sync` takes the desired set as repeatable
|
|
48
|
+
`--link title=url` flags and adds only what is missing, matching on URL (the
|
|
49
|
+
only stable identity a link has — Jira assigns the id and the title is
|
|
50
|
+
editable, so a link retitled by hand is still recognised). Scoped to one
|
|
51
|
+
`--category` so reconciling `Communication` cannot disturb `Testing` links.
|
|
52
|
+
`--prune` opts into removing extras, which is off by default because links
|
|
53
|
+
added by hand in the Jira UI are legitimate; it removes surplus copies of a
|
|
54
|
+
desired URL too, since an existing pile-up is the case it exists to clean up.
|
|
55
|
+
Repeated `--link` flags for one URL collapse to a single link.
|
|
56
|
+
|
|
57
|
+
The planning step is exposed as the pure `planRelatedWorkSync` and covered by
|
|
58
|
+
tests, including that a second run is a no-op.
|
|
59
|
+
|
|
60
|
+
- [#370](https://github.com/knpkv/npm/pull/370) [`27d2ca1`](https://github.com/knpkv/npm/commit/27d2ca18b0c0b0f8a252d461c0aaf10eb92e9ffc) Thanks [@konopkov](https://github.com/konopkov)! - Enforce the complete anti-slop rule set with zero accepted diagnostics and update affected APIs and implementations to satisfy the required contracts.
|
|
61
|
+
|
|
62
|
+
### Patch Changes
|
|
63
|
+
|
|
64
|
+
- [#358](https://github.com/knpkv/npm/pull/358) [`503d345`](https://github.com/knpkv/npm/commit/503d3459b419a3c9fd366715d5916e41086f493d) Thanks [@konopkov](https://github.com/konopkov)! - Make the OAuth refresh-token rotation atomic so an interrupted CLI cannot log
|
|
65
|
+
the user out.
|
|
66
|
+
|
|
67
|
+
Atlassian rotates refresh tokens: the refresh response carries a replacement and
|
|
68
|
+
the token that was sent is consumed server-side. `refreshTokenImpl` performed
|
|
69
|
+
the grant and then persisted the result in a separate, interruptible step, so a
|
|
70
|
+
fiber interrupt landing between the two destroyed the credential — the stored
|
|
71
|
+
refresh token was already spent, the replacement was never written, and the next
|
|
72
|
+
refresh failed with a 4xx. `getAccessToken` treats that failure as an expired
|
|
73
|
+
refresh token and deletes the token file, so the user was silently logged out
|
|
74
|
+
and had to run `jira auth login` again.
|
|
75
|
+
|
|
76
|
+
Nothing interrupted it before, which is why this had not surfaced. It becomes
|
|
77
|
+
reachable as soon as a caller kills the process: this runs during layer
|
|
78
|
+
construction on every CLI invocation, and `@knpkv/jira-clockify`'s nvim
|
|
79
|
+
statusline terminates the poll process when the editor closes. The grant and
|
|
80
|
+
the persist now share one `Effect.uninterruptible` region.
|
|
81
|
+
|
|
82
|
+
That region carries its own 30s deadline rather than relying on a caller's, for
|
|
83
|
+
two reasons. A caller's `Effect.timeout` would be inert — `timeout` is a race,
|
|
84
|
+
and racing an uninterruptible loser means waiting for it anyway. And an
|
|
85
|
+
uninterruptible region with no deadline of its own absorbs SIGINT/SIGTERM
|
|
86
|
+
entirely, since `NodeRuntime.runMain`'s signal handlers do nothing but interrupt
|
|
87
|
+
the main fiber — a hung `jira` command would stop responding to Ctrl-C. The
|
|
88
|
+
deadline forked inside the region is itself interruptible, so it does bound it.
|
|
89
|
+
|
|
90
|
+
Abandoning the round-trip still cannot prove the grant did not land — Atlassian
|
|
91
|
+
may consume and rotate the token after we stop listening, and no client-side
|
|
92
|
+
deadline changes that. So the deadline is paired with a second rule:
|
|
93
|
+
`getAccessToken` no longer deletes the stored token on any `step: "refresh"`
|
|
94
|
+
failure. It deletes only on the answers that actually mean the grant is dead —
|
|
95
|
+
the provider explicitly reporting `invalid_grant`, on a `400` or a `403` — using
|
|
96
|
+
the new `OAuthError.status` and `OAuthError.errorCode` from
|
|
97
|
+
`@knpkv/atlassian-common`. Previously a transport error or timeout deleted
|
|
98
|
+
the active profile outright, which turned a bad network window into an
|
|
99
|
+
unattended silent logout: `JiraApiConfigLive` builds on every CLI invocation,
|
|
100
|
+
and jcf's statusline runs one every 30 seconds.
|
|
101
|
+
|
|
102
|
+
The statuses deliberately left alone matter as much. `429` is the one this most
|
|
103
|
+
needs to survive — several `jira`/`jcf` processes on one expired token hit the
|
|
104
|
+
endpoint together, one wins the rotation and the rest are rate-limited. `408`
|
|
105
|
+
and `425` restate the timeout case. `407` and other middlebox replies never came
|
|
106
|
+
from Atlassian at all. `401` and `400 invalid_client` mean the client secret is
|
|
107
|
+
wrong, where the fix is `jira auth configure`, not a re-login that would fail
|
|
108
|
+
the same way. A bare `403` is left alone too: it is as likely to be a proxy or
|
|
109
|
+
WAF as Atlassian revoking anything. An unparseable body is no verdict at all.
|
|
110
|
+
|
|
111
|
+
Now an incomplete refresh normally costs a retry rather than the session. This
|
|
112
|
+
narrows the window rather than closing it: the atomicity is fiber-level, so a
|
|
113
|
+
SIGKILL after Atlassian has already rotated the token still loses the
|
|
114
|
+
replacement, and the next refresh then legitimately reports `invalid_grant`. No
|
|
115
|
+
client-side design can close that window against a hard kill.
|
|
116
|
+
|
|
117
|
+
Covered by tests: a refresh that never answers and one that is rate-limited both
|
|
118
|
+
leave the profile on disk, a `400` removes it, and an interrupt mid-rotation
|
|
119
|
+
still persists the replacement token.
|
|
120
|
+
|
|
121
|
+
- [#361](https://github.com/knpkv/npm/pull/361) [`676419e`](https://github.com/knpkv/npm/commit/676419e39c395dd4cfea6d9ffaee7d002a3f75e2) Thanks [@konopkov](https://github.com/konopkov)! - Update Effect and effect-qb, migrate schema-tagged errors to the current Effect API, and adopt the dialect-scoped SQLite function and type APIs introduced by effect-qb 0.22.
|
|
122
|
+
|
|
123
|
+
- [#357](https://github.com/knpkv/npm/pull/357) [`77e3257`](https://github.com/knpkv/npm/commit/77e3257743aacfaf9e11e016a60206f416c5fe79) Thanks [@konopkov](https://github.com/konopkov)! - Secure local control planes and CI credential boundaries. CodeCommit web now
|
|
124
|
+
uses a process-scoped owner session with CSRF protection and loopback-only
|
|
125
|
+
listeners; review sandboxes use authenticated loopback code-server instances,
|
|
126
|
+
digest-pinned images, constrained mounts, non-root execution, and dropped
|
|
127
|
+
capabilities. OAuth callback listeners validate state before accepting terminal
|
|
128
|
+
outcomes and bind explicitly to loopback. GitHub workflows pin external actions
|
|
129
|
+
to immutable commits and keep long-lived Atlassian credentials out of pull
|
|
130
|
+
request execution.
|
|
131
|
+
- Updated dependencies [[`503d345`](https://github.com/knpkv/npm/commit/503d3459b419a3c9fd366715d5916e41086f493d), [`b08ca20`](https://github.com/knpkv/npm/commit/b08ca2004b3efcd72a695b44c72b56dae20afdfd), [`676419e`](https://github.com/knpkv/npm/commit/676419e39c395dd4cfea6d9ffaee7d002a3f75e2), [`b08ca20`](https://github.com/knpkv/npm/commit/b08ca2004b3efcd72a695b44c72b56dae20afdfd), [`27d2ca1`](https://github.com/knpkv/npm/commit/27d2ca18b0c0b0f8a252d461c0aaf10eb92e9ffc)]:
|
|
132
|
+
- @knpkv/atlassian-common@1.4.0
|
|
133
|
+
- @knpkv/agent-skills@0.3.0
|
|
134
|
+
- @knpkv/jira-api-client@1.1.0
|
|
135
|
+
|
|
3
136
|
## 1.2.3
|
|
4
137
|
|
|
5
138
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -43,6 +43,12 @@ Opens Atlassian Developer Console. Create a new OAuth 2.0 (3LO) app with:
|
|
|
43
43
|
http://localhost:8585/callback
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
The provider-facing callback URL and callback listener both use `localhost`, so
|
|
47
|
+
the browser and listener select the same loopback family on IPv4-only and
|
|
48
|
+
IPv6-only hosts. Requests with a missing
|
|
49
|
+
or mismatched OAuth state are rejected without consuming the pending login, so
|
|
50
|
+
only the provider callback carrying the expected state can complete or fail it.
|
|
51
|
+
|
|
46
52
|
### 2. Configure Credentials
|
|
47
53
|
|
|
48
54
|
```bash
|
|
@@ -112,6 +118,30 @@ jira issue attachment upload PROJ-123 ./evidence.svg --document ./PROJ-123.md
|
|
|
112
118
|
|
|
113
119
|
The command uploads the file, replaces exactly one matching placeholder with the remote attachment reference, and includes hidden `jiraAttachment` metadata so later parses keep the Jira attachment identity.
|
|
114
120
|
|
|
121
|
+
### Edit an issue
|
|
122
|
+
|
|
123
|
+
Edit an issue's list-valued fields (fix versions and labels).
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
jira issue edit PROJ-123 --add-fix-version "REL 100" --add-label release-notes
|
|
127
|
+
jira issue edit PROJ-123 --remove-fix-version "REL 99" --json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
| Option | Description | Default |
|
|
131
|
+
| ---------------------- | -------------------------------------------------------------- | ------- |
|
|
132
|
+
| `--add-fix-version` | Add a fix version by name, keeping existing ones (repeatable) | - |
|
|
133
|
+
| `--remove-fix-version` | Remove a fix version by name (repeatable) | - |
|
|
134
|
+
| `--fix-version` | Replace the fix versions with exactly these names (repeatable) | - |
|
|
135
|
+
| `--add-label` | Add a label, keeping existing ones (repeatable) | - |
|
|
136
|
+
| `--remove-label` | Remove a label (repeatable) | - |
|
|
137
|
+
| `--label` | Replace the labels with exactly these (repeatable) | - |
|
|
138
|
+
| `--json` | Output as JSON | `false` |
|
|
139
|
+
|
|
140
|
+
Both fields are sets, so prefer the incremental flags: the replacing forms
|
|
141
|
+
(`--fix-version`, `--label`) silently drop every value not listed. A replacing
|
|
142
|
+
flag cannot be combined with its own incremental flags. Requires the
|
|
143
|
+
`write:jira-work` scope.
|
|
144
|
+
|
|
115
145
|
### Output Formats
|
|
116
146
|
|
|
117
147
|
**Multi (default):** One markdown file per issue with YAML front-matter.
|
|
@@ -162,6 +192,28 @@ jira version get 10042 --json
|
|
|
162
192
|
|
|
163
193
|
The version id is the **numeric** id (e.g. `10042`); use `version list` to find it.
|
|
164
194
|
|
|
195
|
+
### Create a version
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
jira version create --project PROJ --name "REL 100"
|
|
199
|
+
jira version create --project PROJ --name "REL 100" \
|
|
200
|
+
--description "Q3 release" \
|
|
201
|
+
--start-date 2026-01-05 \
|
|
202
|
+
--release-date 2026-01-19 \
|
|
203
|
+
--json
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
| Option | Alias | Description | Default |
|
|
207
|
+
| ---------------- | ----- | ------------------------------------- | ------- |
|
|
208
|
+
| `--project` | `-p` | Jira project key (e.g. `PROJ`) | - |
|
|
209
|
+
| `--name` | | Version name (e.g. `"REL 100"`) | - |
|
|
210
|
+
| `--description` | `-d` | Version description | - |
|
|
211
|
+
| `--start-date` | | Start date, ISO 8601 (`yyyy-mm-dd`) | - |
|
|
212
|
+
| `--release-date` | | Release date, ISO 8601 (`yyyy-mm-dd`) | - |
|
|
213
|
+
| `--json` | | Output as JSON | `false` |
|
|
214
|
+
|
|
215
|
+
The version is created unreleased. Requires the `manage:jira-project` scope.
|
|
216
|
+
|
|
165
217
|
### Set the description
|
|
166
218
|
|
|
167
219
|
```bash
|
|
@@ -180,10 +232,22 @@ jira version related-work add 10042 \
|
|
|
180
232
|
--title "Release notes" \
|
|
181
233
|
--url "https://example.atlassian.net/wiki/spaces/PROJ/pages/123" \
|
|
182
234
|
--category Communication
|
|
235
|
+
|
|
236
|
+
# Reconcile the category to exactly this set — idempotent, safe to re-run
|
|
237
|
+
jira version related-work sync 10042 \
|
|
238
|
+
--link "Release notes=https://example.atlassian.net/wiki/spaces/PROJ/pages/123" \
|
|
239
|
+
--link "Test report=https://example.atlassian.net/wiki/spaces/PROJ/pages/124" \
|
|
240
|
+
--category Communication \
|
|
241
|
+
--prune
|
|
183
242
|
```
|
|
184
243
|
|
|
185
244
|
`related-work add` requires the `write:jira-work` scope.
|
|
186
245
|
|
|
246
|
+
`related-work sync` is what a release scaffold should use: repeated `add` calls pile up duplicate links, while
|
|
247
|
+
`sync` matches on URL (the only stable identity a link has, since Jira assigns the id and the title is editable)
|
|
248
|
+
and adds only what is missing. `--prune` additionally removes links in the category that are not in the desired
|
|
249
|
+
set, including surplus copies of a URL that _is_ desired. Other categories are never touched.
|
|
250
|
+
|
|
187
251
|
## Auth Commands
|
|
188
252
|
|
|
189
253
|
```bash
|
|
@@ -11,10 +11,10 @@ export interface UploadAttachmentInput {
|
|
|
11
11
|
readonly filename?: string | undefined;
|
|
12
12
|
readonly mediaType?: string | undefined;
|
|
13
13
|
}
|
|
14
|
-
export interface
|
|
14
|
+
export interface AttachmentServiceContract {
|
|
15
15
|
readonly uploadToIssue: (issueIdOrKey: string, input: UploadAttachmentInput) => Effect.Effect<Attachment, JiraApiError>;
|
|
16
16
|
}
|
|
17
|
-
declare const AttachmentService_base: Context.ServiceClass<AttachmentService, "@knpkv/jira-cli/AttachmentService",
|
|
17
|
+
declare const AttachmentService_base: Context.ServiceClass<AttachmentService, "@knpkv/jira-cli/AttachmentService", AttachmentServiceContract>;
|
|
18
18
|
export declare class AttachmentService extends AttachmentService_base {
|
|
19
19
|
}
|
|
20
20
|
export declare const layer: Layer.Layer<AttachmentService, never, JiraApiClient | FileSystem.FileSystem | Path.Path>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AttachmentService.d.ts","sourceRoot":"","sources":["../src/AttachmentService.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACtD,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,IAAI,MAAM,aAAa,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACxC;AAED,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"AttachmentService.d.ts","sourceRoot":"","sources":["../src/AttachmentService.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACtD,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,IAAI,MAAM,aAAa,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACxC;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,aAAa,EAAE,CACtB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,qBAAqB,KACzB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;CAC7C;;AAED,qBAAa,iBAAkB,SAAQ,sBAGC;CAAG;AAwE3C,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,iBAAiB,EAAE,KAAK,EAAE,aAAa,GAAG,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CACzE,CAAA"}
|
|
@@ -56,7 +56,7 @@ const make = Effect.gen(function* () {
|
|
|
56
56
|
const result = yield* client.uploadAttachment(issueIdOrKey, {
|
|
57
57
|
bytes,
|
|
58
58
|
filename,
|
|
59
|
-
...(input.mediaType === undefined
|
|
59
|
+
...(!(input.mediaType === undefined) && { mediaType: input.mediaType })
|
|
60
60
|
}).pipe(Effect.mapError((cause) => new JiraApiError({ message: `Failed to upload attachment to ${issueIdOrKey}`, cause })));
|
|
61
61
|
return yield* decodeFirstUploadedAttachment(result);
|
|
62
62
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AttachmentService.js","sourceRoot":"","sources":["../src/AttachmentService.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,4BAA4B,EAAE,MAAM,qCAAqC,CAAA;AAClF,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACtD,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,IAAI,MAAM,aAAa,CAAA;AACnC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAehD,MAAM,OAAO,iBAAkB,SAAQ,OAAO,CAAC,OAAO,EAGnD,CAAC,mCAAmC,CAAC;CAAG;AAE3C,MAAM,wBAAwB,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7C,EAAE,EAAE,MAAM,CAAC,MAAM;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM;IACtB,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACxD,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACvD,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;CACpD,CAAC,CAAA;AAEF,MAAM,wBAAwB,GAAG,
|
|
1
|
+
{"version":3,"file":"AttachmentService.js","sourceRoot":"","sources":["../src/AttachmentService.ts"],"names":[],"mappings":"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"}
|
package/dist/IssueService.d.ts
CHANGED
|
@@ -81,12 +81,86 @@ export interface SearchResult {
|
|
|
81
81
|
readonly nextPageToken: string | null;
|
|
82
82
|
readonly maxResults: number;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Edits to apply to an issue's list-valued fields.
|
|
86
|
+
*
|
|
87
|
+
* `add`/`remove` are incremental and `set` replaces. Prefer the incremental pair:
|
|
88
|
+
* `fixVersions` and `labels` are **sets**, so a `set` that forgets an existing
|
|
89
|
+
* value silently drops it — the failure mode when scripting a release scope.
|
|
90
|
+
*
|
|
91
|
+
* An empty array reads as "not supplied", not as "clear the field". The CLI's
|
|
92
|
+
* repeatable flags use `Options.atLeast(0)`, which reports an absent flag as
|
|
93
|
+
* `[]`, so treating `[]` as a replacement would turn every unpassed flag into a
|
|
94
|
+
* destructive clear. The consequence is that clearing a field outright is not
|
|
95
|
+
* currently expressible; it needs an explicit `--clear-*` flag (or a
|
|
96
|
+
* presence-aware option type) rather than a reinterpretation of `[]`.
|
|
97
|
+
*
|
|
98
|
+
* @category Types
|
|
99
|
+
*/
|
|
100
|
+
export interface EditIssueInput {
|
|
101
|
+
readonly addFixVersions?: ReadonlyArray<string>;
|
|
102
|
+
readonly removeFixVersions?: ReadonlyArray<string>;
|
|
103
|
+
readonly setFixVersions?: ReadonlyArray<string>;
|
|
104
|
+
readonly addLabels?: ReadonlyArray<string>;
|
|
105
|
+
readonly removeLabels?: ReadonlyArray<string>;
|
|
106
|
+
readonly setLabels?: ReadonlyArray<string>;
|
|
107
|
+
}
|
|
108
|
+
/** Any JSON value, matching what the generated client accepts for field values. */
|
|
109
|
+
type JsonValue = string | number | boolean | null | ReadonlyArray<JsonValue> | {
|
|
110
|
+
readonly [key: string]: JsonValue;
|
|
111
|
+
};
|
|
112
|
+
/** One entry in Jira's `update` map. */
|
|
113
|
+
interface FieldOperation {
|
|
114
|
+
readonly add?: JsonValue;
|
|
115
|
+
readonly remove?: JsonValue;
|
|
116
|
+
}
|
|
117
|
+
/** A `PUT /issue/{key}` body: `fields` replaces, `update` applies operations. */
|
|
118
|
+
interface EditIssuePayload {
|
|
119
|
+
readonly fields?: {
|
|
120
|
+
readonly [field: string]: JsonValue;
|
|
121
|
+
};
|
|
122
|
+
readonly update?: {
|
|
123
|
+
readonly [field: string]: ReadonlyArray<FieldOperation>;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Outcome of {@link buildEditIssuePayload}: either a body to send, or the
|
|
128
|
+
* reason the requested edit is not expressible as one.
|
|
129
|
+
*
|
|
130
|
+
* Tagged rather than discriminated by property presence, so a caller matches on
|
|
131
|
+
* `_tag` and a future third outcome cannot narrow silently.
|
|
132
|
+
*
|
|
133
|
+
* @category Types
|
|
134
|
+
*/
|
|
135
|
+
export type EditIssuePayloadResult = {
|
|
136
|
+
readonly _tag: "Payload";
|
|
137
|
+
readonly payload: EditIssuePayload;
|
|
138
|
+
} | {
|
|
139
|
+
readonly _tag: "Invalid";
|
|
140
|
+
readonly reason: string;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Build the `PUT /issue/{key}` body for an {@link EditIssueInput}.
|
|
144
|
+
*
|
|
145
|
+
* Incremental edits go through Jira's `update` verb rather than a
|
|
146
|
+
* read-modify-write on `fields`: the server applies them atomically, so a
|
|
147
|
+
* concurrent edit cannot be clobbered and no extra GET is needed.
|
|
148
|
+
*
|
|
149
|
+
* Jira rejects a field that appears in both `fields` and `update`, and the error
|
|
150
|
+
* it returns does not name the field — so that combination is caught here with a
|
|
151
|
+
* message that does.
|
|
152
|
+
*
|
|
153
|
+
* Pure so the payload shape can be tested without the API.
|
|
154
|
+
*
|
|
155
|
+
* @category Utilities
|
|
156
|
+
*/
|
|
157
|
+
export declare const buildEditIssuePayload: (input: EditIssueInput) => EditIssuePayloadResult;
|
|
84
158
|
/**
|
|
85
159
|
* IssueService interface.
|
|
86
160
|
*
|
|
87
161
|
* @category Services
|
|
88
162
|
*/
|
|
89
|
-
export interface
|
|
163
|
+
export interface IssueServiceContract {
|
|
90
164
|
/** Get a single issue by key */
|
|
91
165
|
readonly getByKey: (key: string) => Effect.Effect<Issue, JiraApiError>;
|
|
92
166
|
/** Search issues by JQL query */
|
|
@@ -95,8 +169,13 @@ export interface IssueServiceShape {
|
|
|
95
169
|
readonly searchAll: (jql: string, options?: {
|
|
96
170
|
readonly maxResults?: number;
|
|
97
171
|
}) => Effect.Effect<ReadonlyArray<Issue>, JiraApiError>;
|
|
172
|
+
/**
|
|
173
|
+
* Edit an issue's list-valued fields (fixVersions, labels) and return the
|
|
174
|
+
* issue as it stands afterwards. Needs `write:jira-work`.
|
|
175
|
+
*/
|
|
176
|
+
readonly edit: (key: string, input: EditIssueInput) => Effect.Effect<Issue, JiraApiError>;
|
|
98
177
|
}
|
|
99
|
-
declare const IssueService_base: Context.ServiceClass<IssueService, "@knpkv/jira-cli/IssueService",
|
|
178
|
+
declare const IssueService_base: Context.ServiceClass<IssueService, "@knpkv/jira-cli/IssueService", IssueServiceContract>;
|
|
100
179
|
/**
|
|
101
180
|
* IssueService tag.
|
|
102
181
|
*
|
|
@@ -121,6 +200,6 @@ export declare class IssueService extends IssueService_base {
|
|
|
121
200
|
*
|
|
122
201
|
* @category Layers
|
|
123
202
|
*/
|
|
124
|
-
export declare const layer: Layer.Layer<IssueService, never,
|
|
203
|
+
export declare const layer: Layer.Layer<IssueService, never, JiraApiClient | SiteUrl>;
|
|
125
204
|
export {};
|
|
126
205
|
//# sourceMappingURL=IssueService.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IssueService.d.ts","sourceRoot":"","sources":["../src/IssueService.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACtD,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"IssueService.d.ts","sourceRoot":"","sources":["../src/IssueService.ts"],"names":[],"mappings":"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"}
|
package/dist/IssueService.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* like `extractDisplayName` and `extractNameArray`.
|
|
9
9
|
* - **Rendered fields**: Requests include `expand: "renderedFields"` to get HTML-rendered
|
|
10
10
|
* descriptions and comments, falling back to plain text.
|
|
11
|
-
* - **Pagination guard**: {@link
|
|
11
|
+
* - **Pagination guard**: {@link IssueServiceContract.searchAll} iterates pages using
|
|
12
12
|
* `nextPageToken` with a MAX_PAGES (1000) safety limit.
|
|
13
13
|
*
|
|
14
14
|
* **Common tasks**
|
|
@@ -24,6 +24,7 @@ import * as Context from "effect/Context";
|
|
|
24
24
|
import * as Effect from "effect/Effect";
|
|
25
25
|
import * as Layer from "effect/Layer";
|
|
26
26
|
import * as Predicate from "effect/Predicate";
|
|
27
|
+
import * as Schema from "effect/Schema";
|
|
27
28
|
import { JiraApiError } from "./JiraCliError.js";
|
|
28
29
|
/**
|
|
29
30
|
* Site URL configuration for issue links.
|
|
@@ -32,15 +33,91 @@ import { JiraApiError } from "./JiraCliError.js";
|
|
|
32
33
|
*/
|
|
33
34
|
export class SiteUrl extends Context.Service()("@knpkv/jira-cli/SiteUrl") {
|
|
34
35
|
}
|
|
35
|
-
const
|
|
36
|
-
const
|
|
36
|
+
const JsonObject = Schema.Record(Schema.String, Schema.Json);
|
|
37
|
+
const isJsonObject = Schema.is(JsonObject);
|
|
38
|
+
const recordOrEmpty = (value) => isJsonObject(value) ? value : {};
|
|
39
|
+
const recordArray = (value) => Array.isArray(value) ? value.filter(isJsonObject) : [];
|
|
37
40
|
const parseSearchJqlResponse = (value) => {
|
|
38
41
|
const record = recordOrEmpty(value);
|
|
39
|
-
const nextPageToken =
|
|
42
|
+
const nextPageToken = Predicate.isString(record.nextPageToken) ? record.nextPageToken : undefined;
|
|
40
43
|
return {
|
|
41
44
|
issues: Array.isArray(record.issues) ? record.issues : [],
|
|
42
|
-
isLast:
|
|
43
|
-
...(nextPageToken !== undefined
|
|
45
|
+
isLast: Predicate.isBoolean(record.isLast) ? record.isLast : nextPageToken === undefined,
|
|
46
|
+
...((nextPageToken !== undefined) && { nextPageToken })
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Build the `PUT /issue/{key}` body for an {@link EditIssueInput}.
|
|
51
|
+
*
|
|
52
|
+
* Incremental edits go through Jira's `update` verb rather than a
|
|
53
|
+
* read-modify-write on `fields`: the server applies them atomically, so a
|
|
54
|
+
* concurrent edit cannot be clobbered and no extra GET is needed.
|
|
55
|
+
*
|
|
56
|
+
* Jira rejects a field that appears in both `fields` and `update`, and the error
|
|
57
|
+
* it returns does not name the field — so that combination is caught here with a
|
|
58
|
+
* message that does.
|
|
59
|
+
*
|
|
60
|
+
* Pure so the payload shape can be tested without the API.
|
|
61
|
+
*
|
|
62
|
+
* @category Utilities
|
|
63
|
+
*/
|
|
64
|
+
export const buildEditIssuePayload = (input) => {
|
|
65
|
+
const fields = {};
|
|
66
|
+
const update = {};
|
|
67
|
+
const specs = [
|
|
68
|
+
{
|
|
69
|
+
field: "fixVersions",
|
|
70
|
+
flag: "fix-version",
|
|
71
|
+
set: input.setFixVersions,
|
|
72
|
+
add: input.addFixVersions,
|
|
73
|
+
remove: input.removeFixVersions,
|
|
74
|
+
// fixVersions entries are objects keyed by name.
|
|
75
|
+
wrap: (name) => ({ name })
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
field: "labels",
|
|
79
|
+
flag: "label",
|
|
80
|
+
set: input.setLabels,
|
|
81
|
+
add: input.addLabels,
|
|
82
|
+
remove: input.removeLabels,
|
|
83
|
+
// Labels are bare strings.
|
|
84
|
+
wrap: (label) => label
|
|
85
|
+
}
|
|
86
|
+
];
|
|
87
|
+
for (const spec of specs) {
|
|
88
|
+
// An empty repeatable flag arrives as `[]`, which means "not passed" rather
|
|
89
|
+
// than "replace with nothing" — treating it as a set would clear the field.
|
|
90
|
+
const set = spec.set !== undefined && spec.set.length > 0 ? spec.set : undefined;
|
|
91
|
+
const increments = [
|
|
92
|
+
...(spec.add ?? []).map((value) => ({ add: spec.wrap(value) })),
|
|
93
|
+
...(spec.remove ?? []).map((value) => ({ remove: spec.wrap(value) }))
|
|
94
|
+
];
|
|
95
|
+
if (set !== undefined && increments.length > 0) {
|
|
96
|
+
return {
|
|
97
|
+
_tag: "Invalid",
|
|
98
|
+
reason: `--${spec.flag} cannot be combined with --add-${spec.flag}/--remove-${spec.flag}; ` +
|
|
99
|
+
`--${spec.flag} replaces the whole list.`
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (set !== undefined)
|
|
103
|
+
fields[spec.field] = set.map(spec.wrap);
|
|
104
|
+
else if (increments.length > 0)
|
|
105
|
+
update[spec.field] = increments;
|
|
106
|
+
}
|
|
107
|
+
const hasFields = Object.keys(fields).length > 0;
|
|
108
|
+
const hasUpdate = Object.keys(update).length > 0;
|
|
109
|
+
if (!hasFields && !hasUpdate) {
|
|
110
|
+
return {
|
|
111
|
+
_tag: "Invalid",
|
|
112
|
+
reason: "Nothing to edit. Pass at least one of --add-fix-version, --remove-fix-version, --fix-version, --add-label, --remove-label or --label."
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
_tag: "Payload",
|
|
117
|
+
payload: {
|
|
118
|
+
...(hasFields && { fields }),
|
|
119
|
+
...(hasUpdate && { update })
|
|
120
|
+
}
|
|
44
121
|
};
|
|
45
122
|
};
|
|
46
123
|
/**
|
|
@@ -84,12 +161,12 @@ const FIELDS = [
|
|
|
84
161
|
const extractDisplayName = (field) => {
|
|
85
162
|
if (field === null || field === undefined)
|
|
86
163
|
return null;
|
|
87
|
-
if (
|
|
164
|
+
if (Predicate.isString(field))
|
|
88
165
|
return field;
|
|
89
166
|
if (Predicate.isReadonlyObject(field)) {
|
|
90
|
-
if (
|
|
167
|
+
if (Predicate.isString(field.displayName))
|
|
91
168
|
return field.displayName;
|
|
92
|
-
if (
|
|
169
|
+
if (Predicate.isString(field.name))
|
|
93
170
|
return field.name;
|
|
94
171
|
}
|
|
95
172
|
return null;
|
|
@@ -102,10 +179,10 @@ const extractNameArray = (field) => {
|
|
|
102
179
|
return [];
|
|
103
180
|
return field
|
|
104
181
|
.map((item) => {
|
|
105
|
-
if (
|
|
182
|
+
if (Predicate.isString(item))
|
|
106
183
|
return item;
|
|
107
184
|
if (Predicate.isReadonlyObject(item)) {
|
|
108
|
-
if (
|
|
185
|
+
if (Predicate.isString(item.name))
|
|
109
186
|
return item.name;
|
|
110
187
|
}
|
|
111
188
|
return null;
|
|
@@ -134,7 +211,7 @@ const mapIssue = (bean, baseUrl) => {
|
|
|
134
211
|
const attachmentField = fields["attachment"];
|
|
135
212
|
const attachments = Array.isArray(attachmentField)
|
|
136
213
|
? recordArray(attachmentField).map((att) => {
|
|
137
|
-
const mediaType = normalizeAttachmentMediaType(undefined,
|
|
214
|
+
const mediaType = normalizeAttachmentMediaType(undefined, Predicate.isString(att["mimeType"]) ? att["mimeType"] : null);
|
|
138
215
|
return {
|
|
139
216
|
id: String(att["id"] ?? ""),
|
|
140
217
|
filename: String(att["filename"] ?? ""),
|
|
@@ -164,13 +241,13 @@ const mapIssue = (bean, baseUrl) => {
|
|
|
164
241
|
return {
|
|
165
242
|
id: commentId,
|
|
166
243
|
author: extractDisplayName(author) ?? "Unknown",
|
|
167
|
-
body:
|
|
244
|
+
body: Predicate.isString(renderedBody) ? renderedBody : String(c["body"] ?? ""),
|
|
168
245
|
created: parseDate(c["created"]),
|
|
169
246
|
updated: parseDate(c["updated"])
|
|
170
247
|
};
|
|
171
248
|
});
|
|
172
249
|
// Use rendered description (HTML) if available
|
|
173
|
-
const description =
|
|
250
|
+
const description = Predicate.isString(renderedFields["description"])
|
|
174
251
|
? renderedFields["description"]
|
|
175
252
|
: String(fields["description"] ?? "");
|
|
176
253
|
return {
|
|
@@ -207,7 +284,7 @@ const make = Effect.gen(function* () {
|
|
|
207
284
|
params: {
|
|
208
285
|
jql,
|
|
209
286
|
maxResults,
|
|
210
|
-
...(nextPageToken
|
|
287
|
+
...(nextPageToken && { nextPageToken }),
|
|
211
288
|
fields: FIELDS,
|
|
212
289
|
expand: "renderedFields"
|
|
213
290
|
}
|
|
@@ -250,7 +327,25 @@ const make = Effect.gen(function* () {
|
|
|
250
327
|
}
|
|
251
328
|
return allIssues;
|
|
252
329
|
});
|
|
253
|
-
|
|
330
|
+
const edit = (key, input) => Effect.gen(function* () {
|
|
331
|
+
const built = buildEditIssuePayload(input);
|
|
332
|
+
if (built._tag === "Invalid")
|
|
333
|
+
return yield* Effect.fail(new JiraApiError({ message: built.reason }));
|
|
334
|
+
yield* client.editIssue(key, { payload: built.payload }).pipe(Effect.mapError((cause) => new JiraApiError({ message: `Failed to edit issue ${key}`, cause })));
|
|
335
|
+
// The 204 carries no body, so re-read to report the resulting state.
|
|
336
|
+
//
|
|
337
|
+
// Past this point the edit is already committed, so a failed read-back must
|
|
338
|
+
// not be reported as a failed edit: a caller told "edit failed" may redo a
|
|
339
|
+
// change that already landed. `returnIssue` would close the window, but the
|
|
340
|
+
// endpoint takes no `fields`, so the returned issue can silently omit what
|
|
341
|
+
// `getByKey` asks for — quietly wrong data in place of a loud error.
|
|
342
|
+
return yield* getByKey(key).pipe(Effect.mapError((cause) => new JiraApiError({
|
|
343
|
+
message: `Edited issue ${key}, but reading it back failed, so the result below is unverified. ` +
|
|
344
|
+
`The edit was applied — do not repeat it.`,
|
|
345
|
+
cause
|
|
346
|
+
})));
|
|
347
|
+
});
|
|
348
|
+
return IssueService.of({ getByKey, search, searchAll, edit });
|
|
254
349
|
});
|
|
255
350
|
/**
|
|
256
351
|
* Layer for IssueService.
|