Skip to content

Releases: kmoneil/jr

v0.20.2

Choose a tag to compare

@github-actions github-actions released this 20 Oct 14:58
89c38d1

Take this one if you reopen issues, or write to one issue more than once
under --if-unchanged.
A sequence can now open with its move, so a reopen
and the changes that go with it are one plan (issue 265), and a guarded write
can hand back the precondition for the next one (issue 266).

Nothing changed for anybody who does not use the new spellings, except that
issue edit, issue move and issue assign report their new schema version
in v: a consumer pinning v on those three has a version to add.

A reopen is one sequence. issue sequence carried one move, as its last
step, which is a close. A reopen runs the other way round: the move first, then
the edits that belong to the reopened ticket, which a closed status can refuse
outright (the classic workflow's Closed sets jira.issue.editable=false).

[["issue", "move", "ENG-101", "Reopen Issue"],
 ["issue", "edit", "ENG-101", "--summary", "Flaky on ARM"],
 ["issue", "comment", "add", "ENG-101", "Reopening: it came back on the ARM runners."]]
  • One move, as the first step or the last. A move in the middle, or a second
    move, is still TRANSITION_NOT_LAST.
  • Every step is still dry-run when the plan is written, so a field, a value, a
    person or a sprint that could never work refuses the plan before anything is
    sent. The checks that depend on the status (the permission, the edit screen,
    the assignee) wait for the move, and each later step's checks say
    after step 1: ....
  • The apply sends the move, runs those checks against the reopened issue, then
    sends the rest. One that fails stops the run with the issue moved and nothing
    else sent, and that step is failed with the check's own code. Re-applying
    the same file once it is fixed sends the rest.
  • Measured against Jira Cloud. A status with jira.issue.editable=false has
    not been measured on either deployment; the tests stand in for one with a
    constructed issue.

The next precondition, from the write. A write moves the issue's
updated, so the token that guarded it is stale once it lands, and a chain of
guarded writes cost an issue get between every two.

$ p=$(jr issue move ENG-101 Open --if-unchanged "$p" --precondition \
        --format json | jq -r '.issue["next-precondition"]')
$ jr issue edit ENG-101 --summary 'Reopened' --if-unchanged "$p"
  • --precondition on issue edit, issue move and issue assign reads the
    issue back after the write and reports its token as next-precondition, with
    or without --if-unchanged. It costs one request, so it is asked for.
  • It is the issue as read after the write, not the version the write made. A
    change landing in between, such as an automation rule the write fired, is
    inside it, as it would be in an issue get after the write.
  • If the write lands and the read fails, the result still says the write
    happened and the exit is the read's. The error says not to send it again and
    is never retryable: retrying a move sends a second transition.
  • A token after --precondition is refused as PRECONDITION_TAKES_NO_VALUE,
    because it belongs to --if-unchanged. Beside --dry-run, --plan-out or
    --apply the flag is PRECONDITION_NEEDS_A_WRITE.
  • Measured against Jira Cloud. On Data Center it is covered by constructed
    fixtures and has not been run against a live server.

--tz on a live Data Center. The 0.20.1 notes said --tz had not been run
against a live Data Center. It has since, on 10.4.0: issue get, issue list,
issue activity and issue changes under a named zone, local and account,
JIRA_TZ, and a printed value given back to --updated-after. Nothing needed
to change.

Documentation

  • docs/output-contract.md: what a write's next-precondition is and what it
    is not; a sequence's first move and when the checks after it run; an
    archived project on Data Center answers a search with no rows.
  • docs/recipes.md: reopening an issue and changing it, as one sequence; two
    guarded writes to one issue.
  • docs/troubleshooting.md: PRECONDITION_TAKES_NO_VALUE and
    PRECONDITION_NEEDS_A_WRITE; TRANSITION_NOT_LAST with a move first or
    last; an archived Data Center project that reads as empty.
  • The skill: --precondition on the first of two writes to one issue, and a
    sequence that reopens.

Internal

  • Recordings from Jira Cloud: a guarded edit, then a move guarded by the
    token the edit handed back; a reopen sequence's plan and its apply. A
    recording from Data Center of one unknown project, with its manifest row.
  • golang.org/x/sys 0.49.0 and golang.org/x/term 0.47.0.

Output contract

  • issue.edit v3, issue.move v4, issue.assign v3: an optional
    next-precondition attribute on issue, present only under
    --precondition.
  • New error codes PRECONDITION_TAKES_NO_VALUE and
    PRECONDITION_NEEDS_A_WRITE, exit 2, reached only through --precondition.
    On a sequence step the flag is STEP_TAKES_NO_PLAN_FLAG.
  • TRANSITION_NOT_LAST no longer refuses a move as the first step.
    issue.sequence.plan and issue.sequence did not move: a first move's
    waiting checks are entries in the checks list the plan already had.
  • Priced a patch. Each change is additive by its row. A first move was
    refused and now answers, and so does --precondition, which was an unknown
    flag: accepting an input that used to be refused. Three kinds gain an
    optional attribute. The two new codes are reached only through the new flag,
    so no invocation that worked before answers with one. No command makes more
    requests than it did unless --precondition asks it to.

v0.20.1

Choose a tag to compare

@github-actions github-actions released this 13 Oct 12:20
db9a11d

Take this one if you read jr's timestamps anywhere but UTC, or if a
context or a script of yours can name a project that is gone.
--tz writes
timestamps in a zone you name (issue 218). And on Jira Cloud an empty answer
over a project Jira does not know is now refused, where through 0.20.0 it was
reported as an empty result at exit 0.

One consequence worth knowing before you upgrade: on Jira Cloud, a search
over one project that reads no issue now costs a second request. A
--max-requests that was exactly enough for that answer is one short, and the
command stops at the budget, REQUEST_BUDGET_EXCEEDED at exit 3, where it
answered at exit 0. --max-requests 1 on issue list is the plain case: make
it 2.

Timestamps in a zone you choose. Every timestamp jr printed was UTC,
while every date it is given is read in the Jira account's zone, so a write-up
for people in one place meant converting each one by hand.

$ jr issue get ENG-101 --tz Europe/Berlin | grep -E 'created|updated'
  <created>2026-05-12T01:30:00+02:00</created>
  <updated>2026-05-12T11:10:00+02:00</updated>
  • --tz takes utc (the default, and the same bytes as before), account
    (the zone on your Jira profile, for one GET to /myself), local (the
    machine jr runs on), or an IANA name such as America/Chicago. The value
    keeps its offset, so it is still one instant, still RFC 3339, and still goes
    straight back into any date flag.
  • It can be asked for in three places, and the nearest wins: --tz, then
    JIRA_TZ, then a context's tz, which jr context create --tz and jr context edit --tz set and --unset tz clears. jr context show reports the
    one in effect.
  • The flag exists only on a command that writes a timestamp. jr schema <command> lists it there, and jr version --tz local is an unknown flag.
    JIRA_TZ and a context's tz are ignored by a command with nothing to
    respell.
  • Over MCP a tool call takes tz on the same commands, and one that names none
    falls back to the zone jr mcp serve was started with.
  • The zone is settled before the command runs. A write under --tz account
    that cannot learn the zone is refused before anything is sent, so it cannot
    change Jira and then fail to say so.
  • What stays in UTC: jr doctor, which has no --tz, because under account
    its zone would be a request made with the credential it is there to
    diagnose; every instant in a warning or an error on stderr, the reached of
    a walk that was cut short and the bounds in EMPTY_RESULT among them; and
    version's built. A date has no zone and does not move.
  • A zone that names nothing is INVALID_TZ at exit 2, and the message says
    whether it was read from --tz, JIRA_TZ or the context. An empty --tz is
    refused and not read as UTC. JIRA_TZ is checked on every command, as
    JIRA_FORMAT is.
  • Sorting a timestamp column as text sorts it by time only in UTC. Under a zone
    with daylight saving, use --sort or parse the values.
  • Measured against Jira Cloud. On Data Center it is covered by recorded
    fixtures and has not been run against a live server.

An unknown project is not an empty result. Jira Cloud answers a search over
a project it has never heard of with HTTP 200 and no rows, which is exactly
what it says of a real project with nothing in it, and jr passed both on as
EMPTY_RESULT at exit 0. A context whose project had been deleted or renamed
went on reporting that there was nothing to do.

  • When issue list, issue activity or issue changes reads no issue at all
    over a scope of one project, it asks Jira about the project, and refuses with
    UNKNOWN_PROJECT at exit 5, in Jira's own words, when Jira does not know it.
    Nothing has reached stdout at that point, in any format, and an MCP caller
    gets the same refusal.
  • The scope is asked about wherever it came from: --project, JIRA_PROJECT,
    or the context.
  • A project that exists and holds nothing that matches is still an empty result
    at exit 0.
  • The question costs one request, and only when no issue came back. An answer
    with rows is never asked about. A poll of issue changes that finds nothing
    updated pays it on every tick.
  • Jira Cloud only. Data Center refuses such a search itself, as BAD_REQUEST
    at exit 2 (measured on 10.4.0), and that has not changed.
  • TSV writes its header with the first row, or at the end of a collection that
    has none, so a command that reads an empty page and then refuses has written
    nothing. Output from a command that succeeds is the same bytes.

Documentation

  • docs/output-contract.md: a section for --tz, with what moves and what does
    not; EMPTY_RESULT says when a project is asked about and what that costs;
    UNKNOWN_PROJECT covers one project as well as a set. The stability policy
    gains a row for a command that makes more requests than it did.
  • docs/troubleshooting.md: entries for INVALID_TZ and for UNKNOWN_PROJECT
    on one project.
  • docs/recipes.md: writing timestamps in a zone.
  • The skill tells an agent to ask for a zone and not convert by hand, and that
    no rows over a project that does not exist is an error.

Internal

  • A sweep runs every command the test harness can answer under a zone, on both
    deployments' fixtures, and fails on any timestamp left in UTC. It is the
    check for an instant built into a field declared a string, which every other
    check would pass. It found none.
  • Two recordings from Jira Cloud: the empty answer over a project that does not
    exist and over one that does, which the search answers identically.

Output contract

  • context.get v3 and context.list v3: an optional tz attribute,
    absent when no zone is set.
  • timestamp is RFC 3339, in UTC unless a zone was asked for, and then with
    that zone's offset in the value. No kind that holds one moved, and with no
    zone asked for every document is the bytes it was.
  • New error code INVALID_TZ, exit 2. NO_ACCOUNT_TIMEZONE and
    UNKNOWN_ACCOUNT_TIMEZONE, exit 2, are also reached by --tz account.
  • UNKNOWN_PROJECT, exit 5, is also reached by one project that answers empty
    on Jira Cloud, where the result was EMPTY_RESULT at exit 0.
  • On Jira Cloud an answer with no issues over one project costs one more
    request than it did.
  • Priced a patch. Each change is additive by its row. --tz was an unknown
    flag and now answers: accepting an input that used to be refused. Two kinds
    gain an optional attribute. INVALID_TZ is reached only through --tz,
    JIRA_TZ or a context's tz, none of which jr read before. An unknown
    project reported as empty was a successful answer that was wrong, and an
    error code for that condition is additive. The extra request is priced by a
    row this release writes: a command that makes more requests than it did is
    additive and is listed, which is what 0.18.0 did with the request it added
    to every issue activity run. The reader that row costs something is the
    one named at the top of this section, whose budget was exactly enough.
  • JIRA_TZ is a name jr did not read before, so no setting made for jr is
    refused. A shell that already exports it for something else now has it read,
    and refused on every command if it is not a zone.
  • No exit code changed meaning.

v0.20.0

Choose a tag to compare

@github-actions github-actions released this 13 Oct 00:22
ca378c4

Take this one if you ask about more than one project, and read it first if
anything you run passes --project twice.
--project now repeats where the
answer is a union over projects, and is refused where it is not (issue 216).
Through 0.19.8 a second --project was accepted on every command and the last
one won, without a word about the others.

$ jr issue list --project OPS --project NET --project DO --updated-after -3d
  • issue list, issue activity, issue changes and jql explain take
    several, and search project IN ("OPS", "NET", "DO") with every key quoted. A
    key that is a reserved JQL word, as DO is, needs nothing from you. The
    spelling this replaces, --all-projects --jql 'project in (...)', still
    works, and still leaves the quoting and the scope to the caller.
  • The result names the set. The envelope carries projects="OPS NET DO", a
    list in JSON and YAML, and no project. One project is written as project,
    exactly as before, so check for either when you need to know an answer was
    scoped. EMPTY_RESULT says projects=OPS NET DO, and SCOPE_MISMATCH
    compares a --jql against the whole set.
  • A key Jira does not know is refused, UNKNOWN_PROJECT at exit 5, in Jira's
    own words. Jira Cloud's search leaves an unknown key out of project in (...) and answers with the rest, so a mistyped key would have shortened the
    answer and said nothing. The check costs one request, and only when there are
    two or more keys.
  • Every other command refuses a second --project, as PROJECT_REPEATED
    at exit 2, before any request. jr board list --project OPS --project NET
    used to list NET's boards and say nothing about OPS. Run such a command once
    per project. jr schema <command> reports repeatable on --project per
    command, so a caller can ask before it sends.
  • A member of a repeated --project has to be a project key
    (INVALID_PROJECT_KEY otherwise), and a key given twice is one key. One
    --project is still passed to Jira as typed. JIRA_PROJECT and a context's
    project still hold one key each.
  • jr mcp serve started with several projects follows the same rule: a tool
    call for a command that reads one project is refused.

auth login reports the credential it stored as site-scoped. Its result
said site-scoped="false" for a credential it had just stored keyed by the
site's host, where auth status for the same site said true. Only the
login's own result was wrong. How a credential is found and where it is sent
did not change.

Documentation

  • docs/output-contract.md: repeatable on --project is per command, the
    envelope's projects, the three codes a repeated --project can meet, and
    jql.explain v3. The stability policy's row for an answer that contradicted
    the command's own account now covers a value as well as rows.
  • docs/troubleshooting.md: entries for PROJECT_REPEATED and for
    UNKNOWN_PROJECT on a set, and the reserved-word section teaches repeating
    --project instead of hand-written project in (...).
  • The skill tells an agent to repeat --project rather than write the JQL.

Internal

  • A registry sweep holds every command's declaration that it takes several
    projects to the accessor the command calls, with a negative control. A
    command that declared the set and read one project would be handed nothing
    and answer for the whole site.
  • Three recordings: a two-project listing on Cloud whose third page crosses
    from one project to the other, and the refusal of an unknown key on Cloud and
    on Data Center 10.4.0. On Data Center a set of projects is walked by offset
    and never by issue key, because a key bound does not compare across
    projects: measured there, over two projects, to the last row.

Output contract

  • jql.explain v3: an optional projects list, written in place of
    project when the scope is several projects.
  • The envelope gains an optional projects attribute, written in place of
    project for a set. It moves no kind's version: the envelope is outside
    every kind's shape.
  • New error code PROJECT_REPEATED, exit 2. UNKNOWN_PROJECT, exit 5, and
    INVALID_PROJECT_KEY, exit 2, are also reached by a repeated --project.
  • auth.status: site-scoped on a login's result is true where it was
    false. The kind did not move.
  • Priced a minor, on one row. A second --project on a command that reads
    one project was accepted, the last key winning, and now exits 2 with
    PROJECT_REPEATED: refusing an input that used to be accepted. The rest is
    additive. On the four commands that take a set, the same input now answers
    for every key and not the last, and a set holding an unknown key is an error
    where it was an answer about fewer projects than were named: both are a
    wrong answer corrected, with nothing on a command line to edit. So is the
    login's site-scoped, under the row this release widens from rows to a
    value.
  • No exit code changed meaning.

v0.19.8

Choose a tag to compare

@github-actions github-actions released this 12 Oct 21:31
d6d9d49

Take this one whichever commands you use. Nothing jr does changes: no
flag, no output, no exit code. The binaries are built with Go 1.26.9, where
0.19.7 and every release back to 0.13.3 were built with 1.26.8, and 1.26.9
fixes nine advisories in the standard library that govulncheck reports
reachable from this code:

  • GO-2026-6603, net/http: HTTP/2 server memory exhaustion due to Trailer
    headers.
  • GO-2026-6605, net/http: HTTP/1 client connection desynchronization after
    CONNECT rejection.
  • GO-2026-6607, crypto/tls: reject malformed ECH outer extension
    references.
  • GO-2026-6608, net/textproto: memory limit bypass when parsing MIME
    headers.
  • GO-2026-6610, net/http: HTTP/2 transport accepts malformed
    framing-related headers.
  • GO-2026-6611, net/http: excessive CPU consumption from repeated initial
    window changes.
  • GO-2026-6612, net/http: double flow control refund on HTTP/2 server
    streams.
  • GO-2026-6613, net/http: HTTP/1 server connection desynchronization after
    a 2xx CONNECT response.
  • GO-2026-6617, net/http: HTTP/2 server crash due to an HPACK encoder race.

Those are the advisories' own titles. "Reachable" is the scanner's word and
means the call graph gets there. Nobody has judged here which of the nine a
Jira server, or a proxy in front of one, could use against jr. What the
titles do say: four name an HTTP server (6603, 6612, 6613, 6617), and jr
listens on no socket in any profile. Two name the client (6605, 6610), and
three do not say whose side they are on.

Internal

  • internal/render gains the two tests the weekly mutation sweep asked for
    (issue 252). The truncation warning's total, and its sort, order and
    reached, were asserted only from internal/cli, and the sweep measures a
    package against its own tests. The package's count of surviving mutants
    falls from 12 to 11.

Output contract

  • No kind moved, no code was added, and no invocation is answered differently.
  • Priced a patch. A toolchain is nothing a caller sends and nothing a
    consumer parses, so no row of the stability policy is touched and the
    default position moves.

v0.19.7

Choose a tag to compare

@github-actions github-actions released this 06 Oct 14:56
46c7480

Take this one if you work against more than one Jira site, or read
comment-heavy activity feeds.
Logging in to a second site now makes its
context (issue 206), and issue activity --body-chars keeps the first part of
each body rather than all of it or none of it (issue 219).

$ printf '%s' "$TOKEN" | jr auth login --site other-company.atlassian.net \
      --email you@company.com --token-stdin
$ jr --context other-company issue list --assignee currentUser
  • With a context already defined, a login to a site no context named stored
    the credential, reported authenticated="true", and made nothing, so
    --context answered UNKNOWN_CONTEXT. It now makes a context named for the
    host's first label and reports it in context. The current context never
    changes.
  • A context already naming the site is left alone. Naming it means the same
    host in any case, with the scheme and a trailing slash ignored and the path
    exact, so a Data Center site under /jira and the root of its host are two
    sites.
  • The first label of a Data Center host is usually jira, so a second one
    often finds that name taken. Then nothing is made, the credential is still
    stored, and the warning CONTEXT_NOT_CREATED names the context holding the
    name and both ways out. --context-name names the context yourself; a name
    another site's context holds is refused as CONTEXT_NAME_TAKEN, exit 2,
    before the credential is checked or stored, because making it would replace
    that context.
$ jr issue activity --since -1d --body-chars 120
  • --body-chars N keeps the first N characters of each comment and worklog
    body, counted in code points so a cut never splits one. A cut body carries
    truncated="true" and the whole body's length; TSV gains a body-length
    column after body, filled only where the body was cut.
  • A cut body does not make the result incomplete. Every event is there, and
    the exit stays 0.
  • Refused before any request beside --no-body (BODY_CHARS_AND_NO_BODY),
    beside --raw-body (BODY_CHARS_AND_RAW_BODY, since an ADF document cut
    short is not a document), and below 1 (INVALID_BODY_CHARS).
  • In markdown, a bounded feed stays one table instead of a section per event.

Output contract

  • issue.activity v2: optional truncated and length on an event's
    body, written only when --body-chars cut it. No other kind carrying a
    body moved.
  • auth.status: context is now populated on a second site's login, where it
    used to be absent. The kind did not move.
  • New warning CONTEXT_NOT_CREATED, from auth login.
  • New error codes, each only on an invocation using one of the new flags:
    CONTEXT_NAME_TAKEN, INVALID_BODY_CHARS, BODY_CHARS_AND_NO_BODY,
    BODY_CHARS_AND_RAW_BODY.
  • Priced a patch. Two new flags, a new warning code, new optional
    attributes, and an optional attribute populated where it was absent are each
    additive. The new refusals refuse only invocations that use a new flag, so
    nothing that worked before is refused.

v0.19.6

Choose a tag to compare

@github-actions github-actions released this 05 Oct 18:30
6a088bd

Take this one if you sort a list and cut it short, or read jr through its MCP
server.
A list cut short under --sort now says how far down that order it
got (issue 217), and the MCP server's truncation warning now carries the
total the command line has carried since 0.19.2.

$ jr issue list --updated-after -3d --sort updated --order desc --limit 100
# stderr: RESULT_TRUNCATED, count: 100, sort: updated, order: desc,
#         reached: 2026-05-09T14:20:00Z, remedy: resume with --page-token, ...
  • reached is the sort field's value on the last row written, as the row
    renders it, so "every issue updated after 14:20" is a statement the warning
    supports. Rows sharing that value can sit on either side of the cut: a walk
    resumed from the token may begin on it again.
  • sort names the field id and order the resolved direction. A named field
    ascends unless --order says otherwise, and reached reads the opposite way
    in each.
  • The sort field is fetched for this whether or not a column shows it, and is
    not added to the rows. A --sort naming a field outside the default set
    (due, 'Story Points') is resolved through the field catalogue, the cached
    request --field makes; sorting by the key or a default field costs nothing
    more.
  • The three are absent where the boundary is unknown: a list-valued sort field
    (labels, a multi-select, a sprint field), a sort the catalogue cannot name,
    and an empty value on the last row.

The MCP server built its own copy of the truncation warning and never learned
total when 0.19.2 added it to the command line, so a model was told
"truncated" where a terminal was told "100 of 104". Both now come from one
conversion.

Output contract

  • The RESULT_TRUNCATED warning gains optional sort, order and reached
    leaves on a cut issue list --sort. The warning's v stays 1, and no kind
    changed.
  • Over MCP, the same warning now carries total wherever the command knows it,
    as the command line's has since 0.19.2.
  • Priced a patch. Optional leaves on a warning, populated where they used
    to be absent; nothing a consumer reads today reads differently.

v0.19.5

Choose a tag to compare

@github-actions github-actions released this 02 Oct 18:59
7ffadca

Take this one if your issues link outward, or your projects are filed under
categories.
Two reads the API had and this tool did not, issues 214 and 220,
both measured on Cloud and Data Center before anything was built.

jr issue remotelink list returns an issue's links to things outside Jira:
the web links a person pastes in, and the links an integration writes to a
commit, a build, a document. Until now issue activity showed a link being
added and nothing could return its URL.

$ jr issue remotelink list ENG-101
id     application  relationship  title           url
10000                             Deploy runbook  https://wiki.example.com/runbook
10001  Example CI   mentioned in  Pipeline 42     https://ci.example.com/pipelines/42
  • A link an application wrote names it, and may carry the remote side's own
    resolved state. One a person added arrives with neither: Jira sends an empty
    application object for it, and that is reported by absence, so a
    remotelink with no application is a hand-added web link.
  • The id is the value an activity item names (RemoteIssueLink on Data
    Center, RemoteWorkItemLink on Cloud), so the listing resolves what the
    changelog refers to. After a removal the id resolves to nothing: read the
    links while they exist.
  • issue get --with-remote-links folds the same set into the record. The
    endpoint serves it whole in one response on both deployments, so unlike the
    comment thread this container is never partial.

jr project list now reports the category a project is filed under, and can
filter by it, so "the platform team's projects" is one invocation on a site
whose categories name the owning teams:

$ jr project list --category Platform --with-category
  • The category element carries the server's name and id, and is absent, not
    empty, on a project with none, which is how both deployments report that.
  • --category keeps the projects filed under that exact name, ignoring case.
    It is not a substring search, unlike --match: a category is a closed set
    an administrator curates, so a fragment matches nothing rather than
    guessing. A blank value is refused as EMPTY_QUERY.
  • The filter runs client-side on both deployments, because Cloud's search can
    filter by category where Data Center's listing silently ignores the same
    parameter, and a rule only one server applies cannot be held to one answer.
  • --with-category appends the column; the default table keeps its four
    columns, because adding one there is a breaking change.

Output contract

  • New command: issue remotelink list; new kind issue.remotelink.list v1.
  • issue.get v12: an optional remotelinks container, present only with
    --with-remote-links.
  • project.list v3 and project.get v3: an optional category element,
    present when the server sent one.
  • New flags: issue get --with-remote-links; project list --category and
    --with-category. --raw-field refuses --with-remote-links beside it,
    as it refuses every document-shaping flag.
  • New error code: MALFORMED_REMOTE_LINKS, exit 9.
  • Priced a patch. Everything here is additive: a new command, a new kind,
    and optional elements nothing existing looks for. The three kind versions
    moved because a changed shape at an unchanged version is refused, and a
    kind's version moving decides nothing about the release by itself.

v0.19.4

Choose a tag to compare

@github-actions github-actions released this 02 Oct 16:26
aa205df

Take this one if you make several changes to one issue at a time. Closing a
ticket the way a team expects is a comment, a field and a transition: three
commands, and a failure at the second leaves the issue half done with nothing
saying so. jr issue sequence plans them as one document and applies them in
order (issue 157). Each step is written as the command it would have been, as
JSON, and read by that command's own flags:

$ jr issue sequence ENG-101 --steps-file close.json --plan-out close.xml
$ jr issue sequence --apply close.xml --dry-run
$ jr issue sequence --apply close.xml
  • Planning sends nothing. Every step is dry-run as its own command, so it is
    refused where that command would be, and then checked for what the commands
    leave to Jira: the permission each step needs, every edited field on the edit
    screen, the assignee assignable, a linked issue readable, the sprint open. A
    plan with any step that cannot run is not written, and the refusal names
    every such step.
  • Applying sends nothing from the file. The issue's baseline is compared,
    and every step is rebuilt through its own command and held byte for byte to
    what the plan recorded, so an issue changed since planning (STALE_WRITE) or
    a plan that drifted or was edited (PLAN_DRIFTED) runs nothing.
  • The first failure stops the run, and the document says which steps were
    applied, which failed with what code, and which were not attempted; the exit
    is the failing step's. Applying the same file again resumes.
  • A step is issue edit, issue assign, issue comment add, issue link add,
    sprint add or issue move, on the sequence's issue, with at most one move,
    last. Create stays its own call.

Documentation

  • docs/recipes.md closes a ticket as one sequence; docs/troubleshooting.md
    gains the sequence's refusals; the skill's workflows reference teaches it.

Internal

  • Parsing a command's flags moved from the CLI into the registry, so a step
    parses exactly as the same words do at a prompt; a sweep holds the two to one
    answer for every command.
  • A plan of six steps is recorded on each deployment and replayed, so every
    read the pre-flight makes was answered by a real server.

Output contract

  • New command: issue sequence.
  • New kinds: issue.sequence.plan v1, what --plan-out writes, and
    issue.sequence v1, what --apply reports.
  • New error codes, all exit 2 unless noted: SEQUENCE_NEEDS_A_PLAN,
    NO_STEPS, STEPS_AND_STEPS_FILE, INVALID_STEPS, TOO_MANY_STEPS,
    STEP_NOT_ALLOWED, GLOBAL_FLAG_IN_STEP, STEP_TAKES_NO_PLAN_FLAG,
    STEP_NAMES_ANOTHER_ISSUE, STEP_READS_STDIN, TRANSITION_NOT_LAST,
    FIELD_NOT_ON_SCREEN, USER_NOT_ASSIGNABLE, PERMISSION_DENIED (exit 6),
    PERMISSION_NOT_REPORTED (exit 9), PLAN_DRIFTED (exit 7).
  • Priced a patch. A new command and two new kinds only it emits are
    additive by the stability policy; no existing kind moved, nothing that ran
    before is refused or answered differently.
  • No exit code changed meaning.

v0.19.3

Choose a tag to compare

@github-actions github-actions released this 02 Oct 13:12
4b35c68

Take this one if you paste a timestamp jr printed back into a date flag.
Every date flag refused the RFC 3339 instants jr prints, so asking what else
happened around a moment meant converting a UTC timestamp into the Jira
account's timezone by hand, and a conversion off by an hour is a complete,
exit-0 answer for the wrong window (issue 213). Every date flag now takes one:
2026-05-12T09:00:00Z, without seconds, with a fraction, or with a numeric
offset. JQL reads a date only to a minute of the account's clock, so jr
converts the instant into that zone, which costs one request to learn, and only
when a flag holds an instant.

  • An instant with seconds is widened to the minute, outward: down for an
    -after flag and up for a -before, so the window sent holds the one you
    asked for. On issue list the new DATE_ROUNDED warning names the bound
    that was sent. An instant on the minute is sent exactly and nothing is said.
  • On the night the account's clock goes back, a minute in the repeated hour
    names two instants, and jr never sends one: the bound moves on to a minute
    that names one instant, up to two hours further, and the warning says why.
  • issue activity and issue changes compare each event to the instant
    itself, so their answers are exact and they never warn.
  • A clock with no offset, and Jira's own +0000 spelling, are refused as
    INVALID_DATE, with a hint naming the two spellings that work. On Data
    Center, --worklog-after and --worklog-before refuse an instant as they
    refuse any time of day.

issue changes could skip an hour on the night the clocks go back. Its
query bound was the window's start truncated to the minute in the account's
zone, and inside the repeated hour that minute names two instants. A server
reading it as the later one left out an hour of changes the window still
needed. The bound now moves to a minute that names one instant, as above.

Documentation

  • The recipe for meaning your own day, in docs/recipes.md,
    docs/troubleshooting.md and the skill, is one command now: send your
    midnight with your own offset, from date -Iseconds, and jr converts it.
    docs/troubleshooting.md gains DATE_ROUNDED.

Internal

  • scripts/land waits on and merges the commit its checkout holds, refuses a
    branch holding commits that are not pushed, and from a second worktree says
    where the base is checked out instead of pulling into the pull request's
    branch. It has a test now, against a bare repository and a stand-in gh.
  • Two new invariants: no accepted date form reaches a query in a shape JQL
    refuses, and a rounded bound moves outward and never to a minute that names
    two instants. The second is a year-long sweep in eight zones, dense around
    every transition, checked against a reading that does not use the code.

Output contract

  • No kind moved.
  • New warning code on issue list: DATE_ROUNDED.
  • Priced a patch. An input that used to be refused now answers, and a
    warning code is added, and the stability policy prices both as additive.
    Nothing that ran before is refused or answered differently, except that
    issue changes reads the hour it used to skip on the night the clocks go
    back.
  • No exit code changed meaning.

v0.19.2

Choose a tag to compare

@github-actions github-actions released this 01 Oct 22:02
d95be9f

Take this one if a script or an agent reads a truncated result to decide what
to do next.
The RESULT_TRUNCATED warning carried total, how many rows the
whole answer held, on two commands. It now carries it wherever jr knows the
number: on every list that reads its whole set before --limit cuts it (the
board, epic, sprint, project, component, version, status, field, transition,
create-field, link and attachment lists), on issue list against Data Center,
whose search counts every query, and on issue comment list and
issue worklog list, whose endpoints send a count. "100 of 104" and "100 of
5,000" now read differently, which is the difference between one more page and
a narrower question (issue 217). Where nothing counts the rows, issue list
and issue history on Cloud and user list, there is still no total, and
its absence still means unknown.

issue activity --until ends a window. "What changed in the half hour
before 09:30" is one command rather than a feed trimmed by hand (issue 215).
--since keeps the events at or after its instant and --until the events
before its own, so two windows that meet share no event and miss none. It
narrows the answer and not the search: every issue updated after --since is
still read. A window that holds no instant is refused as EMPTY_WINDOW.

project list --match finds projects by key or name, ignoring case, and
repeats for any of several, in place of --limit all piped to grep
(issue 220). jr applies it on both deployments, so they agree, and it costs
no request, because the listing already reads the whole catalogue. A blank
--match is refused as EMPTY_QUERY, so an unset shell variable cannot list
everything as if it had been filtered.

Documentation

  • The skill's main page says that a pipe hides the exit code: jr ... | head
    reports head's zero over a truncated result's 3 (issue 221). It was only in
    reference files an agent may never open.
  • docs/troubleshooting.md gains EMPTY_WINDOW and EMPTY_QUERY,
    docs/recipes.md an incident-window recipe, and docs/getting-started.md finds
    a project's key with --match.

Internal

  • A fourth sweep, driven from the registry with a negative control and a
    ledger: every paginated command is cut to one row on both deployments, and
    the warning's total has to be the answer's size, or the ledger has to say
    why the command cannot know it. Against 0.19.1 it failed 34 runs across 17
    commands.
  • The paging sweep's fake no longer sends a count on Cloud's enhanced search,
    which the real server never does.

Output contract

  • No kind moved.
  • New error code, exit 2, on issue activity: EMPTY_WINDOW, for --until
    at or before --since.
  • EMPTY_QUERY, exit 2, which user list gives a blank search, is also what
    project list gives a blank --match.
  • Priced a patch. The warning's total is populated for inputs where it
    used to be absent, which is the stability policy's row for an optional
    element the schema already declared. issue activity --until and
    project list --match are new optional flags; nothing that ran before is
    refused or answered differently.
  • No exit code changed meaning.