Skip to content

Fix jira init 404 at configuring metadata step on Jira Cloud - #11

Open
rethab wants to merge 18 commits into
mainfrom
fix/issue-757
Open

Fix jira init 404 at configuring metadata step on Jira Cloud#11
rethab wants to merge 18 commits into
mainfrom
fix/issue-757

Conversation

@rethab

@rethab rethab commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Jira Cloud has deprecated the bulk `GET /issue/createmeta` endpoint (with the `expand=projects.issuetypes.fields` parameter) that `jira init` uses to fetch issue types. This causes the "Configuring metadata" step to fail with a 404 for genuine Cloud tenants, as reported by multiple users.

Jira Server 9+ hit the same deprecation earlier and was already worked around by using the newer per-project `GET /issue/createmeta/{project}/issuetypes` endpoint instead, but that code path was only enabled for local/on-prem installations.

This change routes Cloud installations through the same per-project endpoint. The issue type `id`/`name`/`subtask` data it returns is all `configureMetadata` actually consumes from the old bulk response — the `Fields` data fetched via the old endpoint's `expand` was never read downstream (custom field metadata is fetched separately via `GetField()` in `configureFields`), so this is a safe, no-new-endpoint substitution.

Adds a test verifying that a Cloud installation hits the new per-project endpoint instead of the deprecated bulk one.

Addresses ankitpokhrel#757.

rethab added 18 commits July 12, 2026 07:04
The Jira wiki-to-markdown parser corrupted non-ASCII characters (Greek,
Cyrillic, CJK, accented Latin, emoji, etc.) whenever they appeared on a
line that also contained wiki markup such as *bold*, a heading, or a
list marker.

In secondPass's tokenized-line loop, the fallback branch for bytes that
don't start a recognized token wrote `out.WriteRune(rune(line[beg]))`
while advancing one byte at a time. This treats every raw UTF-8 byte as
its own Unicode code point and re-encodes it, mangling any multibyte
sequence. Lines without any markup skip this loop entirely (they're
written verbatim), which is why the bug only showed up on lines mixing
wiki syntax with non-ASCII text.

The fix decodes a full rune at the current byte offset with
`utf8.DecodeRuneInString` and advances by its actual byte width instead
of a fixed one byte, so multibyte characters pass through intact
regardless of what markup surrounds them on the line.

Added a test case mixing bold markup with Greek, Cyrillic, and Japanese
text to lock in the fix.

Addresses ankitpokhrel#1003.
…utput (#9)

\`jira issue view\` currently only supports the full pretty/plain
rendering (emoji header, subtasks, linked issues, comments, and a Jira
URL footer) or \`--raw\`, which dumps the entire Jira API JSON response.
There was no way to get just the subject and description as clean text,
so users resorted to piping \`issue list --columns\` output through
custom jq/sed scripts.

This adds a \`--description\` flag that fetches the issue and prints
only the summary and the translated description text (reusing the
existing description-translation logic used by the pretty view), with no
header, footer, comments, or emoji decoration. This makes the output
easy to consume from scripts or other tools.

Addresses ankitpokhrel#933.
When an issue has a parent, \`jira issue view\` currently gives no
indication of it — you'd have to open the issue in the browser to find
out. The API response already includes the parent key
(\`Fields.Parent\`), it just wasn't being rendered.

This adds a \`👪 <PARENT-KEY>\` entry to the header line, right after the
issue key, but only when a parent exists so unparented issues (epics,
top-level stories) keep the existing layout unchanged.

Addresses ankitpokhrel#766.
IssueList.header() in internal/view/issues.go always prepended the KEY
column to the requested --columns list whenever it wasn't already
present, with no exception for --plain mode. This meant \`jira issue
list --plain --columns type\` (for example) would still print a KEY
column the user never asked for.

KEY is only needed by the interactive TUI to drive its view/move/copy
actions. Plain output has no such interactive actions, so this change
only forces KEY in when not in --plain mode, and respects the
user-supplied --columns list as-is otherwise, including letting KEY be
omitted entirely.

Addresses ankitpokhrel#747.
`go.mod` still declared the module as
`github.com/ankitpokhrel/jira-cli`, so this fork's own packages were all
imported through the upstream path. Anyone `go install`-ing or importing
from `rethab/jira-cli` would get a module whose declared path disagrees
with where it actually lives, and the two would be treated as separate
modules if both ended up in one build graph.

The build configs hardcoded the same path, which is the part that could
ship broken silently: the `-X` ldflags in `Makefile` and
`.goreleaser.yml` inject version/commit into `internal/version`, and a
`-X` against a package path that isn't in the binary is a no-op rather
than an error. Left as-is after a module rename, releases would build
fine and report an empty version. Verified by building through the
Makefile and confirming `jira version` still reports the injected
values.

README badges and links to upstream discussions/issues are deliberately
untouched — those point at the upstream project's community, not at
code, and are a separate call to make.
Jira Cloud can return a 500 Internal Server Error (instead of an empty
list) when `jira init` fetches boards for a project that doesn't support
Jira Software boards, e.g. a Business/JWM project. This is a known quirk
on Jira's side, not something the CLI can fix server-side.

The existing code in `getBoardSuggestions` already handles the "no
boards available" case gracefully for local/on-prem installs by falling
back to a "None" board option (added to handle installs without the
agile add-on returning 404), but for Cloud it special-cased the error
and aborted `init` entirely, leaving the user unable to finish setup at
all.

This change drops the Cloud-specific hard-fail and applies the same
fallback used for local instances: if the board fetch fails, skip it and
offer "None" as the board choice. The fallback is no longer silent: a
warning including the response status (e.g. `500 Internal Server Error`)
is printed so users can tell why only "None" is offered. This is safe
because credentials and server details are already validated earlier in
the init flow via `Me()`, so a failed board fetch at this point reflects
the project/board API, not auth.

Addresses ankitpokhrel#1001.
The Snapshot workflow publishes a GitHub release tagged
`snapshot-<version>` on every push to main, which creates a real git
tag. That tag isn't valid semver, so on the next run GoReleaser picks it
up as the current tag via `git describe`, fails to parse it, and
`incpatch` in the snapshot `version_template` blows up with `invalid
semantic version`.

The very first snapshot run worked fine (no tags existed yet, so
GoReleaser defaulted to `0.0.0`), but it permanently poisoned every run
after it — every Snapshot workflow run since has failed, while the
regular Build/CI workflow is unaffected.

Fix: add `git.ignore_tags: ["snapshot-*"]` to `.goreleaser.yml` so
GoReleaser never considers its own snapshot release tags when computing
the current/previous version. Verified locally with `goreleaser release
--snapshot --clean` (v2.17.0, matching the pinned CI version) — it now
correctly falls back to `v0.0.0` and builds successfully.
Adds a \`--board\` flag to \`jira sprint list\` that lets you fetch
sprints from a board other than the one configured during \`jira init\`,
without needing to reconfigure. Previously the board ID used for sprint
listing was read exclusively from the stored config, so users working
across multiple boards had no way to query a different one for a single
invocation.

When the flag is used, the board name shown in the interactive sprint
explorer falls back to displaying the overridden board's ID, since the
configured board name no longer applies and fetching the name would
require an extra API call.

Addresses ankitpokhrel#847.
The `--raw` flag is registered on both `sprint list` and `epic list`
(inherited from `issue list`) and shows up in `--help`, but neither
command ever read it, so `--raw` still rendered the interactive/table
view instead of JSON.

`epic list` was only half-broken in an easy-to-miss way: piping it
happened to work, because the non-TTY branch delegates to the issue list
handler, which does honor `--raw`. On a TTY, and for `epic list <KEY>`
in every case, the flag was silently ignored. Printing the
already-fetched epics directly keeps the piped output identical while
fixing the cases that were broken.

The JSON printing now lives in `cmdutil` so all three list commands
share one helper, rather than `issue list` keeping a private copy and
the new callers adding more.

Tests drive the real command handlers through the real flag set against
a stubbed Jira server, so they cover the flag plumbing that was actually
broken — including the `--current`/`--prev`/`--next` paths, where
`--raw` has to survive the hand-off to the single-sprint view.

Addresses ankitpokhrel#931.
The fork inherited upstream's Docker workflow, which publishes to
`ghcr.io` only when a release is cut. Between releases there was nothing
to pull, even though every merge to `main` already produces a snapshot
release and a Homebrew snapshot cask. Merges to `main` now publish an
image too.

`latest` follows `main`, and releases publish version tags (`1.2.3`,
`1.2`). That is the opposite of the Docker default, where `latest` means
"newest release", so it is called out in the README: pin a version tag
if you want a stable image. Merges additionally get an immutable
`main-<sha>` tag.

Building four platforms on every merge is only affordable if the build
is fast, so the Dockerfile now cross-compiles with Go from the build
platform instead of running anything under QEMU emulation. The runtime
stage is kept free of `RUN` instructions to make that complete — its
`apk` install of ca-certificates was redundant anyway, since Alpine
ships the certificate bundle in the base image since 3.9 — so nothing
ever executes on the target platform and the workflow needs no
QEMU/binfmt setup at all. Cross-compiling meant swapping `go install`
for `go build`, since `go install` refuses to cross-compile into
`GOBIN`; the linker flags still come from the Makefile (new `ldflags`
target), with the tag's `v` prefix stripped, so the version stamped into
an image matches a release binary (`1.2.3`, not `v1.2.3`).

Smaller things that fall out of that:

- `linux/arm/v8` is dropped from the platform list. No client asks for
it — ARMv8 hardware reports `linux/arm64`, which we already build — so
it was a redundant entry in the manifest.
- The runtime base moves from `alpine:3.19` to `3.23`, the release the
builder already uses.
- The image now carries a signed provenance attestation, matching what
the release and snapshot workflows already do for their artifacts. There
is deliberately no build cache: a cache that any branch can write to is
a way into the images we publish, which is the same reasoning that
already keeps the Go cache off the release builds.
jira sprint list only skipped the interactive TUI explorer when --table
was passed. Passing --plain alone was silently ignored: the explorer
still opened, and the output was never rendered in plain mode.

This is inconsistent with jira sprint list <SPRINT_ID> --plain, which
always renders directly with no TUI/table gating, and with jira issue
list --plain, which is sufficient on its own to get plain output.

The fix makes --plain imply the non-interactive table render, same as
--table, --dumb-terminal, and non-TTY output already do. The branching
logic was pulled into a small pure function so the flag combinations can
be unit tested without depending on the actual terminal state.

Addresses ankitpokhrel#968.
Adds a --raw flag to jira project list that prints the raw JSON output
instead of the rendered table view, matching the existing --raw behavior
already available on jira issue list.

Addresses ankitpokhrel#885.
`jira issue view --plain` (and non-TTY output that auto-detects plain
mode) still leaked non-ASCII decorations into piped output. Parts of the
view — `separator()` and `footer()` — already branch on `Display.Plain`,
but the rest didn't. Plain output should be safe to pipe, grep, and
render in a non-UTF-8 locale like `LC_ALL=C`, so no decoration the view
adds around issue data may be non-ASCII.

Two changes:

- `header()` now follows the same pattern as the rest of the view: when
`Display.Plain` is set it renders ASCII labels (`Type:`, `Status:`,
`Assignee:`, …) instead of emoji-decorated fields.
- The remaining decorations go ASCII in plain mode too: subtask,
linked-issue, and comment fields are separated by `|` instead of `•`,
and truncated summaries end in `...` instead of `…` (`shortenAndPad` now
takes the ellipsis as a parameter and keeps the same column width either
way, so alignment is unaffected).

Non-plain output is unchanged. A new test asserts the plain issue view
contains no non-ASCII characters at all, so future decorations can't
silently regress this; a counterpart test pins that the non-plain view
keeps its decorations.

Addresses ankitpokhrel#213.
Addresses ankitpokhrel#930.

`jira init` reads the debug setting via `viper.GetBool("debug")`, so
`JIRA_DEBUG` works there. Every other subcommand instead reads debug
directly off the pflag object with `cmd.Flags().GetBool("debug")` or
`flags.GetBool("debug")`, which never consults viper or the env var,
only the `--debug` flag on the command line. As a result
`JIRA_DEBUG=true jira issue view ...` (and the equivalent for every
other subcommand) silently did nothing.

This switches all remaining call sites (issue, epic, sprint, board,
project, release and serverinfo commands, plus the sprint query params)
to `viper.GetBool("debug")`, mirroring the pattern already used in
`internal/config/generator.go`. Since `--debug` is already bound via
`viper.BindPFlag("debug", ...)` in `internal/cmd/root/root.go`, and
`viper.AutomaticEnv()` + `SetEnvPrefix("jira")` are configured there
too, this makes `JIRA_DEBUG` resolve consistently everywhere while
leaving existing `--debug` flag behavior unchanged.

Also added a test in `internal/query/sprint_test.go` covering that
`JIRA_DEBUG` is picked up when constructing sprint query params.
Jira Cloud has sunset the bulk GET /issue/createmeta endpoint with the
issuetypes.fields expand, causing `jira init` to fail with a 404 at the
"Configuring metadata" step. Jira Server 9+ already worked around the
same deprecation by using GET /issue/createmeta/{project}/issuetypes
instead. Route Cloud installations through that same call, since the
issue type id/name/subtask data it returns is all configureMetadata
actually needs (the Fields data from the old bulk call was never used).
@rethab

rethab commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto current main (the conflict in internal/config/generator_test.go was both sides adding tests — kept both, and took the fork's import path).

One thing to fix before this merges: the per-project endpoint silently drops untranslatedName, so Cloud users lose Handle.

The PR body says the Fields data from the old expand was the only thing the bulk response carried that isn't used elsewhere. That's true of Fields, but not of the whole response — Handle goes missing too:

  • internal/config/generator.go:657 sets Handle: it.Handle on the bulk path.
  • configureIssueTypesForJiraServerV9 (internal/config/generator.go:685-690) doesn't, because CreateMetaResponseJiraServerV9.Values (pkg/jira/createmeta.go:29-33) has no untranslatedName field.

Handle isn't dead code — GetSubtaskHandle (internal/cmdutil/utils.go:171-178) and issue create (internal/cmd/issue/create/create.go:135,198,230,272) use it to send the untranslated issue type name. The field exists precisely for non-English Jira sites. So after this change, jira init against a translated Cloud site writes a config whose issuetypes have no handles, degrading issue create.

Cloud's IssueTypeIssueCreateMetadata does return untranslatedName, so the fix is small: add Handle string `json:"untranslatedName,omitempty"` to the V9 values struct and map it in the generator.

Smaller things, none blocking:

  • generator_test.go:98t.Fatalf is called from the HTTP handler goroutine. t.Fatal* has to come from the test goroutine; elsewhere it calls runtime.Goexit on the wrong one. Use t.Errorf + w.WriteHeader(http.StatusNotFound).
  • generator_test.go:115-117 — should be require, not assert. With the fix reverted the test doesn't fail cleanly, it panics with an index-out-of-range at :116, because assert.Len doesn't halt.
  • The generator still passes Expand: "projects.issuetypes.fields" into the V9 request (generator.go:672-675), which is meaningless for that endpoint. Jira ignores it today, but it's misleading — consider dropping it.
  • Pagination: the endpoint paginates (maxResults defaults to 50) and GetCreateMetaForJiraServerV9 neither sets it nor loops, so a project with >50 issue types is silently truncated. Pre-existing on the Server 9 path, but this change extends the exposure to every Cloud user. Fine as a follow-up.

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