kd-cli 2.2.1__tar.gz → 2.3.0__tar.gz
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.
- {kd_cli-2.2.1/kd_cli.egg-info → kd_cli-2.3.0}/PKG-INFO +1 -1
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/youtrack.py +25 -2
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/article/__init__.py +12 -1
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/client.py +112 -11
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/resources/CHANGELOG.md +13 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/resources/operating-guide.md +5 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0/kd_cli.egg-info}/PKG-INFO +1 -1
- {kd_cli-2.2.1 → kd_cli-2.3.0}/pyproject.toml +1 -1
- {kd_cli-2.2.1 → kd_cli-2.3.0}/LICENSE +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/MANIFEST.in +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/README.md +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/articles.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/http.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/hub.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/saved_queries.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/tags.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/article_marker.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/auth.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/core/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/core/command_base.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/core/command_parser.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/core/namespaces.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/core/option_registry.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/input.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/main.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/alias/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/init.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/issue/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/primer/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/project/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/query/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/release_notes.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/search/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/system/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/tag/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/time/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/user/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/output.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/tree.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/config.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/config_read.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/config_schema.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/connection.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/debug.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/exceptions.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/history.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/article.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/assist.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/attachment.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/comment.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/common.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/issue.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/project.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/saved_query.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/search.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/tag.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/tag_entities.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/user.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/work_item.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/operating_guide.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/primer.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/query_parser.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/release_notes.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/resources/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/resources/command-synonyms.yaml +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/resources/config-template.yaml +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/resources/error-messages.yaml +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd_cli.egg-info/SOURCES.txt +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd_cli.egg-info/dependency_links.txt +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd_cli.egg-info/entry_points.txt +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd_cli.egg-info/requires.txt +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/kd_cli.egg-info/top_level.txt +0 -0
- {kd_cli-2.2.1 → kd_cli-2.3.0}/setup.cfg +0 -0
|
@@ -24,6 +24,7 @@ from kd.models import (
|
|
|
24
24
|
Attachment,
|
|
25
25
|
Comment,
|
|
26
26
|
CommentCreate,
|
|
27
|
+
CustomField,
|
|
27
28
|
FieldSet,
|
|
28
29
|
Issue,
|
|
29
30
|
IssueCreate,
|
|
@@ -38,6 +39,28 @@ from kd.models import (
|
|
|
38
39
|
WorkItemType,
|
|
39
40
|
)
|
|
40
41
|
|
|
42
|
+
|
|
43
|
+
def _custom_field_payload(field: CustomField) -> dict[str, Any]:
|
|
44
|
+
payload = field.model_dump(by_alias=True, exclude_none=True)
|
|
45
|
+
if field.value is None:
|
|
46
|
+
payload["value"] = None
|
|
47
|
+
return payload
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _issue_create_payload(issue: IssueCreate) -> dict[str, Any]:
|
|
51
|
+
payload = issue.model_dump(by_alias=True, exclude_none=True)
|
|
52
|
+
if issue.custom_fields is not None:
|
|
53
|
+
payload["customFields"] = [_custom_field_payload(field) for field in issue.custom_fields]
|
|
54
|
+
return payload
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _issue_update_payload(update: IssueUpdate) -> dict[str, Any]:
|
|
58
|
+
payload = update.model_dump(by_alias=True, exclude_none=True)
|
|
59
|
+
if update.custom_fields is not None:
|
|
60
|
+
payload["customFields"] = [_custom_field_payload(field) for field in update.custom_fields]
|
|
61
|
+
return payload
|
|
62
|
+
|
|
63
|
+
|
|
41
64
|
# Default field selections for common queries
|
|
42
65
|
_PROJECT_LIST_FIELDS = str(
|
|
43
66
|
FieldSet(
|
|
@@ -226,7 +249,7 @@ class YouTrackAPI:
|
|
|
226
249
|
def create_issue(self, issue: IssueCreate) -> Issue:
|
|
227
250
|
data = self._http.post(
|
|
228
251
|
"/issues",
|
|
229
|
-
json=issue
|
|
252
|
+
json=_issue_create_payload(issue),
|
|
230
253
|
fields=_ISSUE_FIELDS,
|
|
231
254
|
)
|
|
232
255
|
return Issue.model_validate(data)
|
|
@@ -234,7 +257,7 @@ class YouTrackAPI:
|
|
|
234
257
|
def update_issue(self, issue_id: str, update: IssueUpdate) -> Issue:
|
|
235
258
|
data = self._http.post(
|
|
236
259
|
f"/issues/{issue_id}",
|
|
237
|
-
json=update
|
|
260
|
+
json=_issue_update_payload(update),
|
|
238
261
|
fields=_ISSUE_FIELDS,
|
|
239
262
|
)
|
|
240
263
|
return Issue.model_validate(data)
|
|
@@ -181,7 +181,7 @@ class ArticlePushCommand(Command):
|
|
|
181
181
|
requires_client=True,
|
|
182
182
|
usage=(
|
|
183
183
|
"kd /article push FILE --project P --title T "
|
|
184
|
-
"[--slug S] [--parent ID] [--tag T] [--force] [--dry-run]"
|
|
184
|
+
"[--slug S] [--parent ID] [--tag T] [--create-tags] [--force] [--dry-run]"
|
|
185
185
|
),
|
|
186
186
|
long_description=(
|
|
187
187
|
"Push a local markdown file to YouTrack as a knowledge base "
|
|
@@ -201,6 +201,7 @@ class ArticlePushCommand(Command):
|
|
|
201
201
|
'kd /article push guide.md --project FB --title "User Guide" --parent FB-A-1',
|
|
202
202
|
'kd /article push guide.md --project FB --title "Guide" --tag "type:guide"',
|
|
203
203
|
'kd /article push guide.md --project FB --title "Guide" --tag "type:guide" --tag "platform:rdk-e"',
|
|
204
|
+
'kd /article push guide.md --project FB --title "Guide" --tag "type:guide" --create-tags',
|
|
204
205
|
'kd /article push guide.md --project FB --title "Guide" --dry-run',
|
|
205
206
|
'kd /article push guide.md --project FB --title "Guide" --force',
|
|
206
207
|
],
|
|
@@ -264,6 +265,15 @@ class ArticlePushCommand(Command):
|
|
|
264
265
|
help_text="Resolve and validate, but do not write anywhere",
|
|
265
266
|
)
|
|
266
267
|
)
|
|
268
|
+
self.register_option(
|
|
269
|
+
OptionDeclaration(
|
|
270
|
+
name="create-tags",
|
|
271
|
+
layer=OptionLayer.COMMAND,
|
|
272
|
+
type=bool,
|
|
273
|
+
default=False,
|
|
274
|
+
help_text="Create missing tags before pushing",
|
|
275
|
+
)
|
|
276
|
+
)
|
|
267
277
|
|
|
268
278
|
def execute(
|
|
269
279
|
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
@@ -289,6 +299,7 @@ class ArticlePushCommand(Command):
|
|
|
289
299
|
force=bool(options.command.get("force")),
|
|
290
300
|
dry_run=bool(options.command.get("dry-run")),
|
|
291
301
|
tags=tags or None,
|
|
302
|
+
create_tags=bool(options.command.get("create-tags")),
|
|
292
303
|
)
|
|
293
304
|
return {
|
|
294
305
|
"ok": True,
|
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
import builtins
|
|
22
22
|
import difflib
|
|
23
|
+
import shlex
|
|
23
24
|
from collections import OrderedDict
|
|
24
25
|
from dataclasses import dataclass, field as dataclass_field
|
|
25
26
|
from datetime import datetime, timezone
|
|
@@ -43,7 +44,7 @@ from kd.article_marker import (
|
|
|
43
44
|
from kd.config import ResolvedConfig
|
|
44
45
|
from kd.connection import ConnectionContext
|
|
45
46
|
from kd.debug import DebugLog
|
|
46
|
-
from kd.exceptions import APIError, ConfigError, NotFoundError, ValidationError
|
|
47
|
+
from kd.exceptions import APIError, ConfigError, KdError, NotFoundError, ValidationError
|
|
47
48
|
from kd.query_parser import youtrack_query_literal
|
|
48
49
|
from kd.models import (
|
|
49
50
|
Article,
|
|
@@ -279,11 +280,7 @@ class FieldSchema:
|
|
|
279
280
|
)
|
|
280
281
|
|
|
281
282
|
validated_value = self._validate_field_value(field, value)
|
|
282
|
-
field_value = (
|
|
283
|
-
{"login": validated_value}
|
|
284
|
-
if self._is_user_field(field)
|
|
285
|
-
else {"name": validated_value}
|
|
286
|
-
)
|
|
283
|
+
field_value = self._field_value_payload(field, validated_value)
|
|
287
284
|
self._debug.debug(
|
|
288
285
|
"fields",
|
|
289
286
|
f"resolved '{slot}' -> field '{field.name}' ($type={issue_type})",
|
|
@@ -350,9 +347,7 @@ class FieldSchema:
|
|
|
350
347
|
f"Unsupported field type '{field.project_field_type}' for field '{field.name}'"
|
|
351
348
|
)
|
|
352
349
|
validated_value = self._validate_field_value(field, value)
|
|
353
|
-
field_value = (
|
|
354
|
-
{"login": validated_value} if self._is_user_field(field) else {"name": validated_value}
|
|
355
|
-
)
|
|
350
|
+
field_value = self._field_value_payload(field, validated_value)
|
|
356
351
|
self._debug.debug(
|
|
357
352
|
"fields",
|
|
358
353
|
f"resolved generic '{field_name}' -> field '{field.name}' "
|
|
@@ -392,16 +387,36 @@ class FieldSchema:
|
|
|
392
387
|
"""Return True if this field is a user-type field (user[1])."""
|
|
393
388
|
return field.field_type_id.startswith("user")
|
|
394
389
|
|
|
390
|
+
def _field_value_payload(
|
|
391
|
+
self,
|
|
392
|
+
field: ProjectCustomField,
|
|
393
|
+
value: str | None,
|
|
394
|
+
) -> dict[str, str] | None:
|
|
395
|
+
"""Build the YouTrack issue-field value payload.
|
|
396
|
+
|
|
397
|
+
``None`` is a deliberate clear operation and must serialize as
|
|
398
|
+
``"value": null`` in the final issue mutation payload.
|
|
399
|
+
"""
|
|
400
|
+
if value is None:
|
|
401
|
+
return None
|
|
402
|
+
return {"login": value} if self._is_user_field(field) else {"name": value}
|
|
403
|
+
|
|
395
404
|
def _validate_field_value(
|
|
396
405
|
self,
|
|
397
406
|
field: ProjectCustomField,
|
|
398
407
|
value: str,
|
|
399
|
-
) -> str:
|
|
408
|
+
) -> str | None:
|
|
400
409
|
"""Validate a value against the field's bundle.
|
|
401
410
|
|
|
402
|
-
Returns the canonical value name (case-corrected)
|
|
411
|
+
Returns the canonical value name (case-corrected), or ``None``
|
|
412
|
+
when an optional field should be cleared.
|
|
403
413
|
Raises ValidationError with allowed values on mismatch.
|
|
404
414
|
"""
|
|
415
|
+
if value == "":
|
|
416
|
+
if field.can_be_empty:
|
|
417
|
+
return None
|
|
418
|
+
raise ValidationError(f"Field '{field.name}' is required and cannot be cleared.")
|
|
419
|
+
|
|
405
420
|
if self._is_user_field(field):
|
|
406
421
|
return value # User fields: server validates against group members
|
|
407
422
|
if not field.bundle_values:
|
|
@@ -999,6 +1014,44 @@ class TimeOperations:
|
|
|
999
1014
|
raise ValidationError(f"Unknown work item type '{name}'. Available: {available}")
|
|
1000
1015
|
|
|
1001
1016
|
|
|
1017
|
+
def _tag_prefix_for_list(tag: str) -> str:
|
|
1018
|
+
"""Return the smallest useful prefix for missing-tag discovery."""
|
|
1019
|
+
if ":" in tag:
|
|
1020
|
+
return tag.split(":", 1)[0] + ":"
|
|
1021
|
+
return tag
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def _extract_tag_suggestions(error: NotFoundError) -> builtins.list[str]:
|
|
1025
|
+
"""Extract close-match names from the existing tag resolver message."""
|
|
1026
|
+
message = str(error)
|
|
1027
|
+
marker = "Did you mean: "
|
|
1028
|
+
if marker not in message:
|
|
1029
|
+
return []
|
|
1030
|
+
raw = message.split(marker, 1)[1].rstrip(".?")
|
|
1031
|
+
return [part.strip() for part in raw.split(",") if part.strip()]
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _missing_tag_resolution(tag: str, error: NotFoundError) -> str:
|
|
1035
|
+
"""Format an actionable missing-tag instruction for article push."""
|
|
1036
|
+
lines = [f"Tag '{tag}' not found."]
|
|
1037
|
+
suggestions = _extract_tag_suggestions(error)
|
|
1038
|
+
if suggestions:
|
|
1039
|
+
lines.append(f" Near matches: {', '.join(suggestions)}")
|
|
1040
|
+
prefix = _tag_prefix_for_list(tag)
|
|
1041
|
+
lines.extend(
|
|
1042
|
+
[
|
|
1043
|
+
"",
|
|
1044
|
+
f"To use an existing tag: kd /tag list --prefix {shlex.quote(prefix)}",
|
|
1045
|
+
f"To create a new tag: kd /tag create {shlex.quote(tag)}",
|
|
1046
|
+
f"To retry push without this tag: omit --tag {shlex.quote(tag)}",
|
|
1047
|
+
"",
|
|
1048
|
+
"If you are still curating the tag set, push without --tag and apply tags",
|
|
1049
|
+
f"separately with kd /tag add ARTICLE-ID {shlex.quote(tag)}.",
|
|
1050
|
+
]
|
|
1051
|
+
)
|
|
1052
|
+
return "\n".join(lines)
|
|
1053
|
+
|
|
1054
|
+
|
|
1002
1055
|
class ArticleOperations:
|
|
1003
1056
|
"""High-level knowledge base article operations."""
|
|
1004
1057
|
|
|
@@ -1123,6 +1176,41 @@ class ArticleOperations:
|
|
|
1123
1176
|
failed.append(f"{tag} ({exc})")
|
|
1124
1177
|
return applied, failed
|
|
1125
1178
|
|
|
1179
|
+
def _preflight_tags(
|
|
1180
|
+
self,
|
|
1181
|
+
tags: builtins.list[str],
|
|
1182
|
+
*,
|
|
1183
|
+
create_missing: bool = False,
|
|
1184
|
+
dry_run: bool = False,
|
|
1185
|
+
) -> builtins.list[str]:
|
|
1186
|
+
"""Resolve tags before mutating an article.
|
|
1187
|
+
|
|
1188
|
+
Returns actionable failure blocks. When ``create_missing`` is true,
|
|
1189
|
+
missing tags are created before the article write; ``dry_run`` never
|
|
1190
|
+
writes tags and treats missing tags as would-create.
|
|
1191
|
+
"""
|
|
1192
|
+
if self._tag_ops is None:
|
|
1193
|
+
raise KdError("Tag operations not available")
|
|
1194
|
+
failed: builtins.list[str] = []
|
|
1195
|
+
for tag in tags:
|
|
1196
|
+
try:
|
|
1197
|
+
self._tag_ops.get(tag)
|
|
1198
|
+
except NotFoundError as exc:
|
|
1199
|
+
if create_missing:
|
|
1200
|
+
if dry_run:
|
|
1201
|
+
continue
|
|
1202
|
+
try:
|
|
1203
|
+
self._tag_ops.create(tag)
|
|
1204
|
+
except KdError as create_exc:
|
|
1205
|
+
failed.append(
|
|
1206
|
+
f"Tag '{tag}' not found and could not be created. {create_exc}"
|
|
1207
|
+
)
|
|
1208
|
+
continue
|
|
1209
|
+
failed.append(_missing_tag_resolution(tag, exc))
|
|
1210
|
+
except KdError as exc:
|
|
1211
|
+
failed.append(f"Tag '{tag}' could not be resolved. {exc}")
|
|
1212
|
+
return failed
|
|
1213
|
+
|
|
1126
1214
|
def update(
|
|
1127
1215
|
self,
|
|
1128
1216
|
article_id: str,
|
|
@@ -1146,6 +1234,7 @@ class ArticleOperations:
|
|
|
1146
1234
|
force: bool = False,
|
|
1147
1235
|
dry_run: bool = False,
|
|
1148
1236
|
tags: builtins.list[str] | None = None,
|
|
1237
|
+
create_tags: bool = False,
|
|
1149
1238
|
) -> PushResult:
|
|
1150
1239
|
"""Idempotently upsert a local markdown file as a YouTrack article.
|
|
1151
1240
|
|
|
@@ -1264,6 +1353,18 @@ class ArticleOperations:
|
|
|
1264
1353
|
f"move {existing_remote.id_readable} --parent {parent}."
|
|
1265
1354
|
)
|
|
1266
1355
|
|
|
1356
|
+
if tags:
|
|
1357
|
+
preflight_failed = self._preflight_tags(
|
|
1358
|
+
tags,
|
|
1359
|
+
create_missing=create_tags,
|
|
1360
|
+
dry_run=dry_run,
|
|
1361
|
+
)
|
|
1362
|
+
if preflight_failed:
|
|
1363
|
+
raise KdError(
|
|
1364
|
+
f"Article {'update' if is_update else 'create'} aborted because "
|
|
1365
|
+
"tagging failed.\n\n" + "\n\n".join(preflight_failed)
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1267
1368
|
# Dry-run bails out here with all validation done but no writes.
|
|
1268
1369
|
if dry_run:
|
|
1269
1370
|
existing_sec = 0
|
|
@@ -4,6 +4,19 @@ All notable changes to kd-cli are recorded here. The project follows [Semantic V
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.3.0] — 2026-04-18
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `/article push --create-tags` explicitly creates missing tags before pushing and attaching them, for scripted flows that own their tag namespace (KDCLI-85).
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `/article push --tag` missing-tag failures now print actionable recovery commands instead of a bare not-found / close-match message (KDCLI-85).
|
|
14
|
+
|
|
15
|
+
## [2.2.2] — 2026-04-18
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `kd /issue update ID --field "NAME="` now clears optional custom fields by sending `value: null` to YouTrack. Required fields reject empty values with a clear client-side error instead of the generic "invalid value" bundle message (KDCLI-81).
|
|
19
|
+
|
|
7
20
|
## [2.2.1] — 2026-04-18
|
|
8
21
|
|
|
9
22
|
### Added
|
|
@@ -80,6 +80,7 @@ kd /issue update KDCLI-42 --state Fixed -o yaml
|
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
For article push, use `--dry-run` before a risky write and inspect conflicts with `/article pull`.
|
|
83
|
+
`/article push --tag` rejects missing tags by default and prints the concrete recovery commands. Use `--create-tags` only for scripted flows that already own the tag namespace.
|
|
83
84
|
|
|
84
85
|
## YouTrack Gotchas
|
|
85
86
|
|
|
@@ -97,6 +98,8 @@ Issue custom fields are project-specific and need `$type` discriminators on muta
|
|
|
97
98
|
kd /project/field list KDCLI
|
|
98
99
|
```
|
|
99
100
|
|
|
101
|
+
Clear optional custom fields with an empty value, for example `kd /issue update KDCLI-42 --field "Milestone="`. Required fields reject empty values before any upstream write.
|
|
102
|
+
|
|
100
103
|
Attachment metadata download URLs may start with `/api/`. kd handles this internally on download commands; do not assume the raw URL can be appended unchanged to an API base URL in custom scripts.
|
|
101
104
|
|
|
102
105
|
Tag existence, tag visibility, and tag sharing rights are different concerns. A visible tag may not be usable for every operation, and sharing settings are provider-specific security posture, not just display metadata.
|
|
@@ -141,6 +144,8 @@ Use `/article show` for metadata plus rendered content. Use `/article pull` when
|
|
|
141
144
|
|
|
142
145
|
Do not use push for one-off web edits or broad tree migration. It is single-file by design. If the remote changed, pull, diff, merge by hand, then push again. Use `--force` only after inspecting the remote state.
|
|
143
146
|
|
|
147
|
+
When pushing with `--tag`, kd validates all tags before the article write. Missing tags abort with commands to list the namespace, create the tag, retry without it, or apply the tag separately after push. `--create-tags` is the explicit opt-in for automation that should create missing tags first.
|
|
148
|
+
|
|
144
149
|
## Tags And Discovery
|
|
145
150
|
|
|
146
151
|
Tags are the main YouTrack-native cross-entity discovery primitive. A namespaced taxonomy such as `kind:*`, `component:*`, and `outcome:*` gives both humans and LLMs a stable way to move across issues and articles.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|