Skip to content

feat(jira): read my worklogs in a span of time - #148

Merged
Bakhomious merged 1 commit into
mainfrom
step/jira-worklogs
Oct 29, 2026
Merged

Bakhomious merged 1 commit into
mainfrom
step/jira-worklogs

Conversation

@Bakhomious

Copy link
Copy Markdown
Contributor

Brief

Goal. The shared Jira client reads my worklogs in a span of time, for the time service that the next step brings: Client.Worklogs(ctx, from, to) returns each worklog of mine that starts from from up to, but not including, to, as when it started and how long it took, the earliest first. The second of time-tracking's eleven steps (decision 180).

Scope.

  • internal/jira/worklog.go: the type Worklog (Started, Spent) and Worklogs:
    • my account ID from GET /rest/api/3/myself; an answer without one fails;
    • the tickets I logged on from the search, with worklogAuthor = currentUser() AND worklogDate >= "<the day before from's UTC date>" AND worklogDate < "<the second day after to's UTC date>" ORDER BY key, fields=id, 100 a page, at most 10 pages;
    • each ticket's worklogs by its ID from GET /rest/api/3/issue/{id}/worklog with startedAfter a millisecond before from, startedBefore at to, startAt, and 1,000 a page, paging on total, at most 10 pages;
    • only mine, by author.accountId, starting within the span; started read with Jira's layout 2006-01-02T15:04:05.000-0700.
  • internal/jira/client.go: the search's paging and the GET request shared by Search and Worklogs.
  • docs/plan.md: decision 180 brought up to date with what Jira's spec says (below), §0 row, §8.2 marked done.

Out of scope. The time service and its day report (time-service); adding worklogs (worklog-add); keeping the account ID between calls; asking for several tickets' worklogs at once.

Acceptance criteria.

  1. Worklogs asks Jira for my account, then searches with the JQL above, fields=id and maxResults=100, then asks for each ticket's worklogs by its ID, path-escaped, with startedAfter, startedBefore, startAt and maxResults=1000; every request is a GET with Basic auth and Accept: application/json.
  2. It returns only the worklogs whose author is my account and that start within the span: one that starts at from counts, one at to doesn't; another account's doesn't; the earliest first, across tickets.
  3. It reads every page of the search (nextPageToken) and of each ticket's worklogs (startAt until total), stops at an empty page, and fails after 10 pages of either.
  4. The JQL's dates hold the whole span whatever the zones of from, to and the Jira profile.
  5. Any failure fails the whole call, with no worklogs and an error that names the step, never the token: an error status, an answer that isn't JSON or is over 4 MiB, an account without an ID, a worklog of mine whose started isn't in Jira's layout, Jira out of reach, an ended context (with no request). Another account's worklog with a bad started is skipped, not a failure.
  6. Search behaves as before: its tests pass unchanged.
  7. mise run check passes.

Facts checked in Jira's OpenAPI spec (2026-10-29). GET /rest/api/3/myself answers accountId under the classic scope read:jira-user, and GET /rest/api/3/issue/{issueIdOrKey}/worklog takes startAt, maxResults (default 5,000), startedAfter and startedBefore (UNIX milliseconds) under read:jira-work: both scopes are on the token (decision 103). The search's fields defaults to id, and "the greatest number of items returned per page is achieved when requesting id or key only". The spec's example started is 2021-01-17T12:34:00.000+0000. Jira Cloud's JQL reference calls worklogDate a DATE field that finds "work logged on a specific date", and doesn't say in which zone.

What changed

  • internal/jira/worklog.go (new): Worklog and Client.Worklogs(ctx, start, end), as in the brief. The code names the span's bounds start and end rather than from and to, which the linter's varnamelen refuses. Worklogs that start together keep the order Jira gave them (a stable sort).
  • internal/jira/client.go: Search keeps its behaviour and now builds its list from a shared search(ctx, jql, fields), which holds the paging. page takes the JQL and the fields. The new get builds a GET and hands it to do, for page, account and worklogPage. issue reads the ticket's id too.
  • docs/plan.md: decision 180's "Reading worklogs" bullet now says what was built and why, with what Jira's spec says: fields=id, worklogs read by the ticket's ID, 1,000 a page under the 4 MiB cap, startedAfter a millisecond early, a bad started failing only for my own worklogs. JQL searches only each ticket's 1,000 latest worklogs, which joins §2's risk. Also the §0 row and §8.2 marked done.

QA

  • Every acceptance criterion has a test; Search's tests and the fake Jira they share are unchanged (AC6).
  • Mutations: 50 of 50 caught (go test ./internal/jira/, with a dead proxy so no mutant reaches a real Jira). They cover the JQL's dates and zones, every query parameter, the paths, the paging and its caps, the author and span checks, the sort, every dropped error and the shared client code. One mutant survived the first run: slices.SortFunc instead of SortStableFunc. The tie test had 14 worklogs that all started at once, and pdqsort leaves a list of equal keys as it is, since it sees it as sorted already. The test now alternates two start times, so the unstable sort reorders the ties, and it's caught.
  • QA also fixed a test case's name that a rename had garbled ("someone else's").
  • Coverage of internal/jira: 98.6%. The gaps are get's error from building the request, which can't happen (a parsed URL, a fixed method, a context), and fetch's, which was already there.

Review

No blocking findings. Non-blocking:

  • About 155 hand-written lines (worklog.go's 133 lines of code plus the client's refactor), around the 150 guideline.
  • Started keeps the offset Jira sends, the Jira profile's zone. The sort and the span checks compare instants, so they don't mind, but time-service must turn each start to time.Local before it counts it on a day.
  • The requests run one after another: 2 plus one a ticket (and more for a ticket with over 1,000 worklogs). A week's handful of tickets every 5 minutes is fine; reading tickets in parallel was out of scope.
  • startedBefore is the end's millisecond, rounded down. If Jira doesn't count its own bound, an end with a fraction of a millisecond would miss a worklog that starts in that millisecond. time-service will pass whole days, so this can't happen there.
  • A span whose start isn't before its end gives no worklogs, after the requests, rather than an error.
  • Not checked against the real Jira: the token's read:jira-user scope for /myself (decision 103 lists it). time-service will show a failure if it's missing.

Gates

mise run --jobs 1 check passed locally: Go fmt, golangci-lint (0 issues), buf, govulncheck (none), Go tests with -race, Go coverage 95.8% total, Biome, svelte-check, tsc, knip, pnpm audit, Vitest 1,082 tests (99.8% of statements), Playwright E2E 319 passed and 7 skipped. SonarQube Cloud runs on demand (decision 132).

Merged under the user's standing OK (2026-10-26): merge once CI's check is green, and review everything at the end.

The time service, the next step of time-tracking, reports the hours I logged on each day. The shared Jira client now reads them: my account from /myself, the tickets I logged on from a worklogAuthor search, then each ticket's worklogs, keeping mine that start in the span, the earliest first (decision 180).
@Bakhomious
Bakhomious merged commit 5bbf0ef into main Oct 29, 2026
2 checks passed
@Bakhomious
Bakhomious deleted the step/jira-worklogs branch October 29, 2026 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant