feat(issue): yt issue history reads activity stream (YT-4) #47

Merged
David merged 1 commit from feat/issue-history into main 2026-05-18 18:56:29 +02:00
Owner

Summary

Adds yt issue history <ID> for reading a YouTrack issue's activity stream. Flags: --category (repeatable), --since, --until (YYYY-MM-DD UTC, converted to ms-since-epoch on the wire), --limit (default 100, paginates with $top/$skip until satisfied), and --json.

The motivating use case from YT-4 is finding issues where YouTrack silently reset State from Done to To do after a project move (when the destination project's State bundle differs from the source). That pattern appears in the default human output as two adjacent rows: an IssueProjectCategory entry with the project shortName flip, followed immediately by a CustomFieldCategory entry where field = State and removed -> added shows the State values.

YouTrack

YT-4 (closes)

What got added

  • src/yt/models.rs: new Activity, ActivityCategory, ActivityField, ActivityCustomFieldRef. added / removed stored as untyped serde_json::Value because their shape varies per category, same pattern as CustomField.value.
  • src/yt/api.rs: list_issue_activities(client, issue_id, categories, since, until, limit) handles pagination and query-param assembly.
  • src/cli.rs: IssueCommands::History(IssueHistoryArgs) variant + args struct.
  • src/commands/issue/history.rs: new module with run, render, parse_ymd_to_ms_utc, summarize/summarize_one, plus tests.
  • src/commands/issue/mod.rs: one line of dispatch wiring.

Discovery worth flagging

YouTrack's /api/issues/<id>/activities endpoint REQUIRES the categories= query param. A missing or empty filter returns HTTP 400 No requested categories specified as a filter parameter. The YT-4 issue body's "Default: all categories" assumption was wrong; there is no wildcard on the wire. The CLI papers over this with a curated DEFAULT_CATEGORIES list at src/commands/issue/history.rs:15 (creation, summary, description, custom field, project, comments, links, tags, sprint, attachments, work items) used when the caller passes no --category flag. Users wanting a narrower or wider set pass explicit --category flags.

Test plan

  • 11 unit + async tests in src/commands/issue/history.rs: date parsing happy/sad path, summarize for object/array/null shapes, smoke-render, query-param wiring (categories/start/end), JSON mode, missing-credentials error, zero-limit error.
  • 2 wiremock tests in src/yt/api.rs: list_issue_activities_decodes_each_category covers all three motivating categories (CustomFieldCategory, IssueProjectCategory, CommentsCategory) in a single response; list_issue_activities_paginates_and_respects_limit covers $top/$skip and the --limit truncation path.
  • just pre-commit passes (fmt, clippy, build, test). Same pre-existing unrelated commands::update::tests::check_writable_returns_friendly_message_on_readonly_dir flake as in #45 and #46; reproduces on origin/main, out of scope.
  • Live-smoked against YT-2 on niceguyit.myjetbrains.com: yt issue history YT-2 --limit 5 returns creation, two comments, and the recent State To do -> In Progress transition (the one this PR-author triggered earlier today).

Out of scope / follow-up

  • Instance-wide forensic scanner via /api/activitiesPage (cursored, multi-issue). Called out as a deferred follow-up in YT-4. The per-issue command shipped here is ergonomic enough that a Nushell each loop builds the scan in a single shell pipeline.
  • Escape handling for category names that contain commas. Not a real-world concern (YouTrack category ids are well-known constant identifiers, not user-typed values).
## Summary Adds `yt issue history <ID>` for reading a YouTrack issue's activity stream. Flags: `--category` (repeatable), `--since`, `--until` (`YYYY-MM-DD` UTC, converted to ms-since-epoch on the wire), `--limit` (default 100, paginates with `$top`/`$skip` until satisfied), and `--json`. The motivating use case from YT-4 is finding issues where YouTrack silently reset `State` from `Done` to `To do` after a project move (when the destination project's State bundle differs from the source). That pattern appears in the default human output as two adjacent rows: an `IssueProjectCategory` entry with the project shortName flip, followed immediately by a `CustomFieldCategory` entry where `field` = `State` and `removed -> added` shows the State values. ## YouTrack YT-4 (closes) ## What got added - `src/yt/models.rs`: new `Activity`, `ActivityCategory`, `ActivityField`, `ActivityCustomFieldRef`. `added` / `removed` stored as untyped `serde_json::Value` because their shape varies per category, same pattern as `CustomField.value`. - `src/yt/api.rs`: `list_issue_activities(client, issue_id, categories, since, until, limit)` handles pagination and query-param assembly. - `src/cli.rs`: `IssueCommands::History(IssueHistoryArgs)` variant + args struct. - `src/commands/issue/history.rs`: new module with `run`, `render`, `parse_ymd_to_ms_utc`, `summarize`/`summarize_one`, plus tests. - `src/commands/issue/mod.rs`: one line of dispatch wiring. ## Discovery worth flagging YouTrack's `/api/issues/<id>/activities` endpoint REQUIRES the `categories=` query param. A missing or empty filter returns HTTP 400 `No requested categories specified as a filter parameter`. The YT-4 issue body's "Default: all categories" assumption was wrong; there is no wildcard on the wire. The CLI papers over this with a curated `DEFAULT_CATEGORIES` list at `src/commands/issue/history.rs:15` (creation, summary, description, custom field, project, comments, links, tags, sprint, attachments, work items) used when the caller passes no `--category` flag. Users wanting a narrower or wider set pass explicit `--category` flags. ## Test plan - [x] 11 unit + async tests in `src/commands/issue/history.rs`: date parsing happy/sad path, summarize for object/array/null shapes, smoke-render, query-param wiring (`categories`/`start`/`end`), JSON mode, missing-credentials error, zero-limit error. - [x] 2 wiremock tests in `src/yt/api.rs`: `list_issue_activities_decodes_each_category` covers all three motivating categories (`CustomFieldCategory`, `IssueProjectCategory`, `CommentsCategory`) in a single response; `list_issue_activities_paginates_and_respects_limit` covers `$top`/`$skip` and the `--limit` truncation path. - [x] `just pre-commit` passes (fmt, clippy, build, test). Same pre-existing unrelated `commands::update::tests::check_writable_returns_friendly_message_on_readonly_dir` flake as in #45 and #46; reproduces on `origin/main`, out of scope. - [x] Live-smoked against YT-2 on `niceguyit.myjetbrains.com`: `yt issue history YT-2 --limit 5` returns creation, two comments, and the recent `State To do -> In Progress` transition (the one this PR-author triggered earlier today). ## Out of scope / follow-up - Instance-wide forensic scanner via `/api/activitiesPage` (cursored, multi-issue). Called out as a deferred follow-up in YT-4. The per-issue command shipped here is ergonomic enough that a Nushell `each` loop builds the scan in a single shell pipeline. - Escape handling for category names that contain commas. Not a real-world concern (YouTrack category ids are well-known constant identifiers, not user-typed values).
feat(issue): yt issue history reads activity stream (YT-4)
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 14s
Create release / Create release from merged PR (pull_request) Has been skipped
fcfdd06c96
Adds `yt issue history <ID>` for reading an issue's YouTrack activity stream: project moves, custom-field flips, comments, links, tags, sprint membership, attachments, work items, and the creation/summary/description events. Flags: `--category` (repeatable), `--since`, `--until` (`YYYY-MM-DD` UTC, converted to ms-since-epoch on the wire), `--limit` (default 100, paginates `$top`/`$skip` until satisfied), and `--json`.

New types in `src/yt/models.rs`: `Activity`, `ActivityCategory`, `ActivityField`, `ActivityCustomFieldRef`. `added` and `removed` are stored as untyped `serde_json::Value` because their shape varies per category (object vs array vs scalar vs null), mirroring the `CustomField.value` pattern. New `list_issue_activities` in `src/yt/api.rs` handles pagination and the `categories`/`start`/`end`/`$top`/`$skip` query-param assembly.

Important discovery during the live smoke: YouTrack's `/api/issues/<id>/activities` endpoint REQUIRES the `categories=` query param (a missing or empty filter returns HTTP 400 `No requested categories specified as a filter parameter`). The YT-4 issue body assumed an implicit "all categories" default; there is no such wildcard on the wire. The CLI papers over this with a curated `DEFAULT_CATEGORIES` list (creation, summary, description, custom field, project, comments, links, tags, sprint, attachments, work items) used when the caller passes no `--category` flag. Users wanting a narrower or wider set can pass explicit `--category` flags.

Forensic scenario the issue called out (project move that resets `State`: `Done` -> `To do` due to schema mismatch) appears in the default human output as two adjacent rows: an `IssueProjectCategory` entry showing the project shortName flip, followed by a `CustomFieldCategory` entry with `field` = `State` and `removed`/`added` showing the State values. No special discovery flag needed.

Tested with 11 unit and async tests in `src/commands/issue/history.rs` and 2 wiremock tests in `src/yt/api.rs` covering decoding of `CustomFieldCategory`, `IssueProjectCategory`, `CommentsCategory`, pagination, the limit cap, the query-param wiring for `categories`/`start`/`end`, JSON mode, and the credential and zero-limit error paths. Live-smoked against YT-2 on the niceguyit instance: returns creation, two comments, and the recent `State To do -> In Progress` transition.

#YT-4 State Done
David merged commit dcd8f5c1bb into main 2026-05-18 18:56:29 +02:00
David deleted branch feat/issue-history 2026-05-18 18:56:29 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pandoras-box/youtrack-cli!47
No description provided.