Status badges populate once the GitHub Actions
CI/CDworkflows have run on the repository; on a fresh fork they render a neutral "no status" state until the first workflow run. The coverage figure is the first-party statement coverage reported bygo test(see Testing and coverage).
A Go CLI and library for working with Jira Cloud from the command
line. Its flagship gojira crawl recursively mirrors a Jira issue
graph into Markdown — including the issue's hierarchy, development
metadata (branches, commits, pull requests, builds, repositories),
and human-labelled custom fields — and can export the discovered
graph as JSON and D2. Beyond crawling, gojira also ships a gRPC
server (gojira serve), a Model Context Protocol server for AI
hosts (gojira mcp), and write operations to create and update
issues, add comments, and run workflow transitions. The same
engine is available as an embeddable Go library.
Pre-1.0 status. APIs may change between minor versions until v1.0 is tagged.
Warning
Breaking changes in v0.2.0. Two changes break compatibility with v0.1.0:
- Package layout — the reusable
classify,client, andlogpackages moved underpkg/, so their import paths gained a/pkg/segment (e.g.github.com/neumachen/gojira/pkg/client). The library facadegithub.com/neumachen/gojirais unchanged. - Protocol services —
internal/grpcserverbecameinternal/grpcandinternal/mcpserverbecameinternal/mcp, each behind an encapsulatedServe(ctx, cfg). Thegojira.ServerConfig/gojira.LoadServerConfigaccessors were removed — readConfig.ServerAddressfrom a loadedConfiginstead.
See the v0.2.0 changelog for the full list. This is a pre-1.0 release; there is no migration shim, but no external consumers depend on the previous paths.
go install github.com/neumachen/gojira/cmd/gojira@latest# Pull (when published to a registry) or build locally:
docker build -t gojira:v0.2.0 .A docker-compose.yml is provided for convenience; see "Docker Compose" below.
gojira can generate completion scripts for bash, zsh, fish, and PowerShell. Add the matching line to your shell's startup file:
# bash — ~/.bashrc
source <(gojira completion bash)
# zsh — ~/.zshrc (ensure `autoload -U compinit && compinit` runs first)
source <(gojira completion zsh)
# fish
gojira completion fish > ~/.config/fish/completions/gojira.fish
# PowerShell — add to $PROFILE
gojira completion pwsh | Out-String | Invoke-ExpressionCompletion works without any gojira configuration, so you can set it up
before running gojira init.
# 1. Generate a Jira Cloud API token at:
# https://id.atlassian.com/manage-profile/security/api-tokens
# 2. Configure credentials. The simplest path is `gojira init`, which
# writes a 0600 config file at ~/.config/gojira/config.yaml — see
# "First run: gojira init" below. The CLI also reads env vars
# directly: either export them or use a .env file.
gojira init # interactive; or pass --site/--user/--token as flags
# alternative: env-only configuration
cp .env.example .env
$EDITOR .env # fill in GOJIRA_SITE, GOJIRA_USER, GOJIRA_TOKEN
# 3. Crawl a single Jira issue and its reachable graph.
gojira crawl PROJ-123
# 4. The Markdown output appears under ./out by default.
ls out/PROJ-123/# After ./out is created by your first run, this works as a
# convenient wrapper:
docker compose run --rm gojira crawl PROJ-123Starting from one Jira issue key, the crawler:
- Fetches the issue via the Jira Cloud REST API v3
(
GET /rest/api/3/issue/{key}?expand=names). - Parses the issue's description from Atlassian Document Format into Markdown.
- Renders an
index.mdfor the issue under<output-dir>/<KEY>/index.mdwith metadata, description, relationships, the## Developmentpanel (pull requests, branches, commits, repositories, builds), and human-labelled custom fields. - Recursively follows links: subtasks, parents, issue links,
hierarchy children (via JQL
parent = "KEY"plus Epic Link), and Jira-flavoured links inside the description. Each discovered key is fetched and rendered the same way. - Recognizes GitHub pull request URLs as PR references even when no Jira key is present in the PR title or branch.
- Writes a per-issue
references/outbound.mdsummarising every outbound reference the issue produced. - Honours configurable caps on depth, issue count, time, and API concurrency.
Use gojira get <ISSUE-KEY> to fetch exactly one Jira issue without
any recursive crawling or disk writes:
# Print Markdown to stdout (default)
gojira get PROJ-123
# Print structured JSON to stdout
gojira get --format json PROJ-123Unlike crawl, get never writes files to disk — it prints directly
to stdout. It does not require GOJIRA_OUTPUT_DIR. The --format
flag accepts markdown (default) or json.
The gojira release command group manages a Jira project's Versions —
the entries on a project's Releases page. Versions are
project-scoped: they belong to a project, not to a single issue. As with
the rest of gojira's write surface, version deletion is intentionally
unsupported. For the end-to-end cut → attach → transition workflow,
see docs/jira-releases.md.
# Create a version (a "Release") on a project. Identify the project by
# key (--project, resolved to a numeric id internally) or by numeric id
# (--project-id).
gojira release create --project EXAMPLE --name Release-abc1234 \
--description "Ship it" --released --release-date 2000-01-01
# Update only the fields you pass; unset flags never blank an existing
# value.
gojira release update 10001 --released --release-date 2000-01-01
# List a project's versions (by key or id).
gojira release list --project EXAMPLErelease create requires --name and exactly one of --project or
--project-id. On success it prints Created version <id> (<name>)
followed by the version's self URL on the next line; release update
prints Updated version <id>. release list prints one line per
version — <id>\t<name>\treleased=<bool>\t<releaseDate> — or
No versions for <project> when the project has none.
Both create and update accept --dry-run, which prints the JSON
request body without any HTTP call. Because the dry-run is
network-free, supplying only --project <KEY> omits projectId from the
previewed body (the key is resolved to a numeric id at send time) and a
one-line note is written to stderr; pass --project-id <numeric> for a
byte-exact dry-run body.
The existing gojira create and gojira update commands can attach one
or more Fix Version(s) to an issue:
--fix-version <name>— match a version by name (repeatable).--fix-version-id <id>— match a version by id (repeatable).
On gojira update, the plain --fix-version / --fix-version-id flags
perform a set-replace of the issue's fix versions. To change them
incrementally instead, use:
--add-fix-version <id>— add a version by id (repeatable).--remove-fix-version <id>— remove a version by id (repeatable).
# Attach a fix version when creating an issue.
gojira create --project EXAMPLE --type Task --summary "New task" \
--fix-version Release-abc1234
# Set-replace the fix versions on an existing issue.
gojira update EXAMPLE-123 --fix-version-id 10001
# Incrementally add/remove a fix version by id.
gojira update EXAMPLE-123 --add-fix-version 10001 --remove-fix-version 10000The crawl subcommand accepts these flags. Each maps to an env
var of the same name in uppercase with a GOJIRA_ prefix; the
flag overrides the env var when both are set.
| Flag | Env var | Default | What it does |
|---|---|---|---|
--site |
GOJIRA_SITE |
(required) | Jira Cloud site URL, e.g. https://your-site.atlassian.net. |
--user |
GOJIRA_USER |
(required) | Atlassian account email. |
--token |
GOJIRA_TOKEN |
(required) | Atlassian API token. |
--output-dir |
GOJIRA_OUTPUT_DIR |
./out |
Output root directory. |
--depth-limit |
GOJIRA_DEPTH_LIMIT |
0 (no cap) |
Max crawl depth from the start issue. |
--issue-cap |
GOJIRA_ISSUE_CAP |
500 |
Max issues to fetch per run. |
--time-cap |
GOJIRA_TIME_CAP_SECONDS |
0 (no cap) |
Max wall-clock seconds per run. |
--concurrency |
GOJIRA_CONCURRENCY |
3 |
Concurrent Jira API requests. |
--refetch |
GOJIRA_REFETCH |
false |
Re-fetch issues that already exist on disk. |
--include-comments |
GOJIRA_INCLUDE_COMMENTS |
false |
Fetch issue comments (not yet rendered as of v0.2.0; the field is fetched but the current renderer ignores it). |
--include-children |
GOJIRA_INCLUDE_CHILDREN |
true |
Discover hierarchy children via JQL parent search. |
--child-search-limit |
GOJIRA_CHILD_SEARCH_LIMIT |
100 |
Max children to discover per parent. |
--epic-link-field |
GOJIRA_EPIC_LINK_FIELD |
(auto-detect) | Tenant's Epic Link custom-field ID. |
--include-dev-status |
GOJIRA_INCLUDE_DEV_STATUS |
true |
Query the Jira Dev Status API for development metadata. |
--dev-status-applications |
GOJIRA_DEV_STATUS_APPLICATIONS |
GitHub |
Comma-separated Dev Status integration types. |
--dev-status-data-types |
GOJIRA_DEV_STATUS_DATA_TYPES |
pullrequest,branch,commit,repository,build |
Comma-separated dataType values to query. |
--render-null-custom-fields |
GOJIRA_RENDER_NULL_CUSTOM_FIELDS |
false |
Include custom fields whose value is JSON null. |
--graph |
GOJIRA_EMIT_GRAPH |
false |
Write graph.json and graph.d2 (D2 source) at the output-dir root. |
--log-level |
GOJIRA_LOG_LEVEL |
info |
One of: error, warn, info, debug, trace. |
--log-format |
GOJIRA_LOG_FORMAT |
text |
One of: text (human-readable), json (one JSON object per line). |
--config |
GOJIRA_CONFIG_FILE |
(discovered) | Path to a YAML config file (see Configuration). |
Every Jira-touching command (crawl, serve, create, update,
comment, transitions, transition, release) requires some form of
configuration before it will contact Jira. The simplest way to provide
it is the gojira init subcommand, which scaffolds a config file with
0600 permissions (it contains your Jira API token).
gojira init writes the global config by default; pass --local
to write a project-local file instead. The two flags are mutually
exclusive, and --local never creates or modifies the global config.
gojira init(orgojira init --global) — writes the global config at the XDG path ($XDG_CONFIG_HOME/gojira/config.yamlor, whenXDG_CONFIG_HOMEis unset,~/.config/gojira/config.yaml).gojira init --local— writes a project-local./gojira.yamlin the current working directory (similar in spirit togit initfor the project's gojira config). The file is complete and self-sufficient: it carries every required field so it loads on its own, without any global config present.
# Global config (default; writes the XDG config.yaml):
gojira init \
--site https://your.atlassian.net \
--user you@example.com \
--token <api-token>
# Project-local config (writes ./gojira.yaml, adds it to .gitignore):
gojira init --local \
--site https://your.atlassian.net \
--user you@example.com \
--token <api-token>Omitted required values are prompted for interactively. The token is
read without echo on a real terminal (via golang.org/x/term) and
falls back to a warned-echo read on non-terminals (CI pipes, etc.).
The --output-dir and --server-address flags accept the documented
defaults (./out and 127.0.0.1:50051) when omitted.
gojira init refuses to overwrite an existing config file unless you
pass --force. The written YAML has the same shape as
gojira.example.yaml and is consumed by the
same loader the cascade below documents.
Security: do not commit
./gojira.yaml. The project-local file contains your Jira API token.gojira init --localwrites it with0600permissions and, when a./.gitignoreis present, appendsgojira.yamlto it (and prints what it did). When no.gitignoreexists, it warns you to ignore the file yourself. Treat./gojira.yamllike any other credentials file.
A guarded command will run iff any one of the following is true:
- A config file is discovered through the cascade (see
Config-file discovery below): an explicit
--configvalue,$GOJIRA_CONFIG_FILE,./gojira.yaml, or the XDG global config file. - The
--config <path>flag was passed. - The
--site,--user, and--tokenflags were ALL passed. - The required env vars are present:
GOJIRA_CONFIG_FILE, or all three ofGOJIRA_SITE,GOJIRA_USER, andGOJIRA_TOKEN.
If none of (1)-(4) hold, the command exits with a clear error pointing
at gojira init. The init, help, and --version invocations are
exempt from this guard so they remain usable from a no-config state.
gojira supports three configuration surfaces — embedded defaults, one
or two YAML config files (a global and an optional project-local
file), and GOJIRA_* environment variables — plus the CLI flags
documented above. They compose into one effective configuration
through a documented cascade:
embedded defaults < XDG global config.yaml < local ./gojira.yaml < GOJIRA_* env vars < CLI flags
A value at any layer overrides the same value at every lower layer; a value absent at every layer keeps its embedded default.
When no --config flag and no $GOJIRA_CONFIG_FILE are given, gojira
layers the global config (~/.config/gojira/config.yaml) and the
project-local ./gojira.yaml field-by-field: the local file
overrides the global on a per-field basis, and any field absent from
the local file is inherited from the global config. This is true
XDG-style layering with local-first precedence, so a project can pin
just the handful of values it cares about (e.g. a different
output.dir or crawl.concurrency) and inherit the rest of the
global config unchanged.
An explicit --config <path> (or $GOJIRA_CONFIG_FILE) pins
exactly that one file with no layering: gojira loads only that
file and does not also read the global or local config. This is the
escape hatch for CI, scripts, and any case where you want a fully
deterministic single-file load.
There are two discovery modes, depending on whether the load is explicitly pinned or left to discovery:
Explicit single-file pin (first match wins, no layering). When either of the explicit candidates is set, gojira uses exactly that file and stops — it does NOT also read the global or local config:
--config <path>(CLI flag)$GOJIRA_CONFIG_FILE
Both are explicit: when set but the file does not exist, gojira exits with a hard error so a misconfigured invocation fails fast.
Pure discovery (global + local layered). When neither explicit candidate is set, gojira layers the following files (each missing file is silently skipped; a fully absent chain falls through to defaults plus environment variables, not an error):
./gojira.yaml— project-local (current working directory). Written bygojira init --local.$XDG_CONFIG_HOME/gojira/config.yaml— XDG global, whenXDG_CONFIG_HOMEis set.~/.config/gojira/config.yaml— XDG global fallback.
In pure discovery, the global config (4 or 5, whichever exists) is
loaded first and the local ./gojira.yaml (3) is layered on top
field-by-field. Either may be absent; if both exist, local fields
override global ones and global fills any gaps the local file leaves.
Behavior change in v0.2.0. Previously a
./gojira.yamlin pure discovery fully shadowed the global config (winner-takes-all). The two files are now merged: global is read first and local is layered over it per field. To restore the old single-file load semantics, point--config(or$GOJIRA_CONFIG_FILE) at the file you want.
A starter file lives at gojira.example.yaml.
Copy it to one of the locations above, edit the values you care about,
and delete the blocks you want to leave at their defaults. The file's
first line embeds a yaml-language-server directive so editors like
VS Code (with the YAML extension) and Neovim get autocomplete and live
validation against the embedded JSON Schema at
internal/config/config.schema.json.
Quick start with a config file:
# Copy the example and edit it.
cp gojira.example.yaml gojira.yaml
$EDITOR gojira.yaml
# Supply the secret out of band so it never lands in version control.
export GOJIRA_JIRA_API_TOKEN="$(security find-generic-password -s gojira -w)"
# Crawl. --config is optional; ./gojira.yaml is auto-discovered.
gojira crawl --config gojira.yaml PROJ-1The table below lists the canonical GOJIRA_* keys gojira reads.
Every key here has a YAML equivalent under the corresponding section
of gojira.yaml; pick whichever surface fits the deployment best.
| Env var | YAML path | Default |
|---|---|---|
GOJIRA_SCHEMA |
schema |
gojira.config.v1 |
GOJIRA_CONFIG_FILE |
(resolver only) | (discovered) |
GOJIRA_JIRA_BASE_URL |
jira.base_url |
(required) |
GOJIRA_JIRA_EMAIL |
jira.email |
(required) |
GOJIRA_JIRA_API_TOKEN |
jira.api_token |
(required) |
GOJIRA_OUTPUT_DIR |
output.dir |
(required) |
GOJIRA_CRAWL_DEPTH_LIMIT |
crawl.depth_limit |
0 |
GOJIRA_CRAWL_ISSUE_CAP |
crawl.issue_cap |
500 |
GOJIRA_CRAWL_TIME_CAP_SECONDS |
crawl.time_cap_seconds |
0 |
GOJIRA_CRAWL_CONCURRENCY |
crawl.concurrency |
3 |
GOJIRA_CRAWL_REFETCH |
crawl.refetch |
false |
GOJIRA_CRAWL_INCLUDE_COMMENTS |
crawl.include_comments |
false |
GOJIRA_CRAWL_INCLUDE_CHILDREN |
crawl.include_children |
true |
GOJIRA_CRAWL_CHILD_SEARCH_LIMIT |
crawl.child_search_limit |
100 |
GOJIRA_CRAWL_EPIC_LINK_FIELD |
crawl.epic_link_field |
(auto-detect) |
GOJIRA_CRAWL_INCLUDE_DEV_STATUS |
crawl.include_dev_status |
true |
GOJIRA_CRAWL_DEV_STATUS_APPLICATIONS |
crawl.dev_status_applications |
GitHub |
GOJIRA_CRAWL_DEV_STATUS_DATA_TYPES |
crawl.dev_status_data_types |
pullrequest,branch,commit,repository,build |
GOJIRA_CRAWL_RENDER_NULL_CUSTOM_FIELDS |
crawl.render_null_custom_fields |
false |
GOJIRA_CRAWL_EMIT_GRAPH |
crawl.emit_graph |
false |
GOJIRA_LOG_LEVEL |
log.level |
info |
GOJIRA_LOG_FORMAT |
log.format |
text |
The v0.1 flat GOJIRA_* keys documented under CLI flags
continue to work; gojira resolves them to their canonical Phase 0
equivalents during load. When both the canonical key and its alias
are set, the canonical key wins — set both only during a migration.
| Deprecated alias | Canonical replacement |
|---|---|
GOJIRA_SITE |
GOJIRA_JIRA_BASE_URL |
GOJIRA_USER |
GOJIRA_JIRA_EMAIL |
GOJIRA_TOKEN |
GOJIRA_JIRA_API_TOKEN |
GOJIRA_DEPTH_LIMIT |
GOJIRA_CRAWL_DEPTH_LIMIT |
GOJIRA_ISSUE_CAP |
GOJIRA_CRAWL_ISSUE_CAP |
GOJIRA_TIME_CAP_SECONDS |
GOJIRA_CRAWL_TIME_CAP_SECONDS |
GOJIRA_CONCURRENCY |
GOJIRA_CRAWL_CONCURRENCY |
GOJIRA_REFETCH |
GOJIRA_CRAWL_REFETCH |
GOJIRA_INCLUDE_COMMENTS |
GOJIRA_CRAWL_INCLUDE_COMMENTS |
GOJIRA_INCLUDE_CHILDREN |
GOJIRA_CRAWL_INCLUDE_CHILDREN |
GOJIRA_CHILD_SEARCH_LIMIT |
GOJIRA_CRAWL_CHILD_SEARCH_LIMIT |
GOJIRA_EPIC_LINK_FIELD |
GOJIRA_CRAWL_EPIC_LINK_FIELD |
GOJIRA_INCLUDE_DEV_STATUS |
GOJIRA_CRAWL_INCLUDE_DEV_STATUS |
GOJIRA_DEV_STATUS_APPLICATIONS |
GOJIRA_CRAWL_DEV_STATUS_APPLICATIONS |
GOJIRA_DEV_STATUS_DATA_TYPES |
GOJIRA_CRAWL_DEV_STATUS_DATA_TYPES |
GOJIRA_RENDER_NULL_CUSTOM_FIELDS |
GOJIRA_CRAWL_RENDER_NULL_CUSTOM_FIELDS |
GOJIRA_EMIT_GRAPH |
GOJIRA_CRAWL_EMIT_GRAPH |
GOJIRA_OUTPUT_DIR, GOJIRA_LOG_LEVEL, and GOJIRA_LOG_FORMAT
already use canonical names in v0.1 and need no migration.
out/
└── PROJ-123/
├── index.md
└── references/
└── outbound.md
Each fetched issue lives at <KEY>/index.md. Outbound references
discovered in that issue are summarised at
<KEY>/references/outbound.md. The references/ directory keeps
the per-issue reference index out of the issue's own rendered
Markdown so a reader who wants the full graph view can find it,
but a reader who just wants the issue content sees only
index.md.
The same engine is available as a Go library. Third-party programs can embed gojira in their pipelines without invoking the CLI binary:
package main
import (
"context"
"fmt"
"os"
"github.com/neumachen/gojira"
)
func main() {
cfg, err := gojira.LoadConfig(map[string]string{
"GOJIRA_SITE": os.Getenv("GOJIRA_SITE"),
"GOJIRA_USER": os.Getenv("GOJIRA_USER"),
"GOJIRA_TOKEN": os.Getenv("GOJIRA_TOKEN"),
"GOJIRA_OUTPUT_DIR": "./out",
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
summary, err := gojira.Crawl(
context.Background(), cfg, []string{"PROJ-123"}, nil,
)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("fetched %d issues; %d PRs discovered\n",
summary.Fetched, summary.PRsFound)
}The library facade groups its capabilities into classification
(Classify), configuration (LoadConfig and friends), fetch/render
(GetIssue, FetchAndRender), crawl (Crawl, CrawlGraph), and
write operations (CreateIssue, UpdateIssue, AddComment,
ListTransitions, TransitionIssue, CreateVersion, UpdateVersion,
ListVersions), plus type aliases for Config,
Summary, Sink, Event, and the graph model
(GraphModel/GraphNode/GraphEdge). All are documented in
gojira.go's package doc.
gojira crawl ships a verbose, structured, correlatable observability
instrument designed for answering "where did the wall-clock go?" on a large
crawl. It's measurement-first: enabling it does not change traversal,
fetch logic, or on-disk output — only what is observed.
gojira extends slog's standard four levels (error, warn, info, debug)
with a fifth, trace. The five levels carry intent-based meaning:
| Level | Meaning |
|---|---|
error |
Failures. |
warn |
Degraded enrichment, partial failures. |
info |
Operationally significant facts and all measurement data — phase/issue spans, per-HTTP-request summaries, the end-of-run crawl.measurement totals. A normal --log-level info run already shows time attribution. |
debug |
Durable diagnostics worth keeping even after a problem is solved — resolved state and decisions (skip-if-exists hits, epic-link field resolution). |
trace |
Traceability woven into the code — span lifecycles, the "because X therefore Y" fan-out reasoning, raw response bodies and full net/http/httptrace timings. |
Select with --log-level trace or GOJIRA_LOG_LEVEL=trace. Use --log-format json
to get machine-filterable JSON lines (one record per line).
Every traced log line carries structured attributes so a single grep/jq filter can reconstruct any subset of the run's work:
| Attribute | Meaning |
|---|---|
run_id |
Opaque short UID for this crawl invocation; on every line. |
ticket_id |
Jira issue key (e.g. PROJ-1417) — named to mirror Jira. Present on every line whose work concerns a specific issue. |
span_id / parent_span_id |
Opaque short IDs per unit of work, linking each unit to whoever enqueued it. Opaque, not hierarchical, because crawls can bleed across projects or boards. |
phase |
One of fetch, parse, hierarchy_jql, dev_status, render, store, enqueue. |
trace_stream |
response (HTTP/data side, from the round-tripper) or stream (orchestration side, from the crawl). |
depth, discovered_from, relation |
Fan-out lineage — present on crawl.fanout TRACE lines explaining why a key entered the queue. |
At end of run, gojira crawl emits a single INFO crawl.measurement line
with the per-phase wall-clock attribution:
{"msg":"crawl.measurement","total_api_time_ms":31872,"total_duration_ms":32114,"call_counts":{"fetch":48,"parse":48,"hierarchy_jql":12,"dev_status":48,"render":48,"store":48},"time_by_phase_ms":{"fetch":18204,"hierarchy_jql":7411,"dev_status":4012,"parse":612,"render":1031,"store":602}}The same totals are also folded into the crawl.Summary
returned to library callers as APICallCounts, APITimeByPhase, and
TotalAPITime.
# All response-stream traces for one issue:
gojira crawl PROJ-1417 --log-level trace --log-format json 2>&1 \
| jq 'select(.trace_stream=="response" and .ticket_id=="PROJ-1417")'
# Only the per-phase measurement summary:
gojira crawl PROJ-1417 --log-level info --log-format json 2>&1 \
| jq 'select(.msg=="crawl.measurement")'
# Reconstruct the fan-out tree (TRACE):
gojira crawl PROJ-1417 --log-level trace --log-format json 2>&1 \
| jq 'select(.msg=="crawl.fanout") | "\(.discovered_from) -[\(.relation)]-> \(.ticket_id)"'Authorization, Cookie, Proxy-Authorization, Set-Cookie, and
X-Atlassian-Token headers are ALWAYS redacted in trace output, even at
--log-level trace. The raw token is never written to logs by design;
the redaction is audited by a unit test
(TestRoundTripper_RedactsAuthorizationEvenAtTrace).
In addition to the one-shot crawl subcommand, gojira can run as a
long-lived gRPC server that exposes its full capability surface —
classification, issue fetch, recursive crawl, in-memory graph export,
and write operations (create/update/comment/transitions) — to other
front-ends such as a TUI or the gojira mcp bridge.
# Start the server (reads the same GOJIRA_* config as `crawl`).
export GOJIRA_SITE="https://your-site.atlassian.net"
export GOJIRA_USER="you@example.com"
export GOJIRA_TOKEN="<api-token>"
gojira serve --address 127.0.0.1:50051The server is single-tenant: one Jira identity is loaded at startup
from the same configuration cascade as crawl. It accepts concurrent
clients (each RPC is isolated) and is intended for a loopback or
otherwise trusted network — Phase 1 ships no TLS and no
authentication.
| RPC | Type | Description |
|---|---|---|
Classify |
unary | Classify a bare key or URL into JiraKey, JiraURL, GitHubPR, or External. |
GetIssue |
unary | Fetch one issue. The response is a structured proto Issue, rendered Markdown, or JSON, selected by the request's OutputFormat (STRUCTURED, MARKDOWN, JSON). |
Crawl |
server-streaming | Recursively crawl from one or more start keys, streaming a CrawlEvent for each state transition. Issue content is written server-side to the configured output directory (streaming content over the wire is deferred to Phase 2). |
GetGraph |
unary | Crawl in-memory from one or more start keys and return the discovered issue graph as nodes and edges, without writing files. Mirrors the library's CrawlGraph and the CLI's crawl --graph. |
CreateIssue |
unary | Create an issue (project + type required; fields via summary/description/labels/parent and a raw_fields map). dry_run returns the request body the server would send, without creating anything. |
UpdateIssue |
unary | Edit fields on an existing issue. Honors dry_run like CreateIssue. |
AddComment |
unary | Append a comment (plain text, converted to ADF server-side) to an issue. |
ListTransitions |
unary | List the workflow transitions currently available for an issue (id, name, target status). |
TransitionIssue |
unary | Move an issue through a transition, selected by transition_id or by target_status_name (resolved server-side via ListTransitions). |
CreateVersion |
unary | Create a project Version (a "Release"). dry_run returns the request body the server would send, without creating anything. |
UpdateVersion |
unary | Update fields on an existing Version; only the fields set are changed. Honors dry_run like CreateVersion. |
ListVersions |
unary | List the versions defined on a project. |
CreateIssueRequest and UpdateIssueRequest additionally carry
repeated string fix_versions (names) and repeated string fix_version_ids (ids) for attaching Fix Version(s) to an issue.
The proto contract is defined in
proto/gojira/v1/gojira.proto and the
generated Go bindings live under gen/gojira/v1/.
The CreateIssue, UpdateIssue, AddComment, and TransitionIssue RPCs let
clients mutate Jira through the same single-tenant identity the server loaded
at startup. Two design points are worth calling out:
- Dry-run.
CreateIssueandUpdateIssueaccept adry_runflag. When set, the server builds and returns the exact JSON request body it would send to Jira (indry_run_body) without performing the write — useful for previewing a mutation before committing to it. - Extensible fields. Beyond the typed fields (summary, description,
labels, parent), any Jira field — including tenant-specific custom fields —
can be set through the
raw_fieldsmap (field id → raw JSON value). In the Go library this is theWithField/WithRawFieldsoption; new fields never require a new method or signature change.
Errors carry Jira's detail: a 400 validation failure maps to gRPC
InvalidArgument with the failing field names in the message; a 409 (e.g. an
invalid workflow transition) maps to FailedPrecondition.
Issue deletion is intentionally unsupported — destructive removal is out of scope for this phase.
| Flag | Env var | Default | Description |
|---|---|---|---|
--address |
GOJIRA_SERVER_ADDRESS |
127.0.0.1:50051 |
gRPC server bind address. |
--config |
GOJIRA_CONFIG_FILE |
(discovered) | Path to a YAML config file. |
--site |
GOJIRA_SITE |
(required) | Jira Cloud site URL. |
--user |
GOJIRA_USER |
(required) | Atlassian account email. |
--token |
GOJIRA_TOKEN |
(required) | Atlassian API token. |
--output-dir |
GOJIRA_OUTPUT_DIR |
./out |
Output root directory for Crawl. |
--log-level |
GOJIRA_LOG_LEVEL |
info |
One of error, warn, info, debug, trace. |
--log-format |
GOJIRA_LOG_FORMAT |
text |
One of text, json. |
The server address can also be set in the YAML config file:
server:
address: 127.0.0.1:50051The server stops gracefully on SIGINT/SIGTERM.
A minimal reference client ships at cmd/gojira-client for smoke-testing
a running server. It is a reference tool, not a production front-end.
go run ./cmd/gojira-client -address 127.0.0.1:50051 -classify PROJ-1147
go run ./cmd/gojira-client -address 127.0.0.1:50051 -key PROJ-1147 -format markdown
go run ./cmd/gojira-client -address 127.0.0.1:50051 -crawl PROJ-1147
go run ./cmd/gojira-client -address 127.0.0.1:50051 -create-project PROJ -create-type Task -create-summary "New task" -dry-run
go run ./cmd/gojira-client -address 127.0.0.1:50051 -comment PROJ-1147 -comment-text "Looks good"
go run ./cmd/gojira-client -address 127.0.0.1:50051 -transitions PROJ-1147
go run ./cmd/gojira-client -address 127.0.0.1:50051 -transition PROJ-1147 -to-status "In Progress"The generated code under gen/ is committed. To regenerate after editing
the proto contract, run buf:
./scripts/gen-proto.sh # runs `buf lint` then `buf generate`gojira mcp runs gojira as a Model Context Protocol
server over the stdio transport so AI hosts (Claude Desktop, Cursor,
Zed, …) can use gojira's tools directly. The host launches gojira mcp
as a subprocess and speaks JSON-RPC over its stdin/stdout. stdout is
reserved for the protocol stream; all logs go to stderr.
# Run from your AI host's mcpServers config (see below); for a manual
# smoke, you can drive a one-shot handshake yourself:
gojira mcp --config ~/.config/gojira/config.yaml < your-jsonrpc-framesgojira mcp is guarded by the same require-config check as crawl/serve
(see First run) and additionally requires
mcp.mode to be set — it exits 1 with a clear message when mcp.mode
is unset or not one of self|bridge.
The mode is set via mcp.mode in the config file or GOJIRA_MCP_MODE
in the environment. It is required for gojira mcp and only for
gojira mcp (crawl/serve continue to load configs without an mcp:
section).
self— gojira does the Jira work in-process via the library facade. The simplest mode; no extra server to run.bridge— gojira forwards each tool call to a runninggojira servegRPC server atserver.address(default127.0.0.1:50051, override withGOJIRA_SERVER_ADDRESSor--address). This is the "one shared gRPC backend, many ephemeral MCP processes" topology: a single long-runninggojira serveis the upstream, and every AI host spawns its own short-livedgojira mcpsubprocess that bridges to it.
The following MCP tools are always available (read-only):
classify— classify an input as Jira key, Jira URL, GitHub PR URL, or external URLget_issue— fetch a single Jira issue with its outbound referencescrawl— recursively crawl Jira issues from one or more start keys; returns a summary on completion (emits MCP progress notifications per fetched issue when the host supplies a progress token)get_graph— crawl in-memory and return the discovered issue graph as{nodes, edges}(no files written)list_transitions— list workflow transitions available for an issuelist_versions— list a project's versions (releases); input{ project }, returns{ versions: [...] }
The mutating tools are gated behind mcp.allow_writes: true
(default false). When allow_writes is false, the write tools are
absent from tools/list — an AI cannot mutate Jira until the
operator explicitly opts in:
create_issueupdate_issueadd_commenttransition_issuecreate_versionupdate_version
The create_issue and update_issue tools also accept fix_versions
(array of names) and fix_version_ids (array of ids) inputs to attach
Fix Version(s) to an issue.
Add an entry to your AI host's MCP servers configuration. The exact
file location varies by host (Claude Desktop:
~/Library/Application Support/Claude/claude_desktop_config.json on
macOS; Cursor and Zed have their own per-app paths) but the schema is
the same:
{
"mcpServers": {
"gojira": {
"command": "gojira",
"args": ["mcp"],
"env": {
"GOJIRA_SITE": "https://your.atlassian.net",
"GOJIRA_USER": "you@example.com",
"GOJIRA_TOKEN": "your-api-token",
"GOJIRA_MCP_MODE": "self"
}
}
}
}For bridge mode, set "GOJIRA_MCP_MODE": "bridge" and ensure a
gojira serve instance is running on the host; add
"GOJIRA_SERVER_ADDRESS": "127.0.0.1:50051" (or your chosen address)
if you do not use the default. The env block in the host config is
not the only configuration channel — gojira init (or a hand-written
~/.config/gojira/config.yaml) is equivalent, and the same
require-config guard described in First run
applies: at least one of a discovered config file, --config, the
--site/--user/--token trio, or the equivalent GOJIRA_* env vars
must be present.
To expose the mutating tools to the host, add
"GOJIRA_MCP_ALLOW_WRITES": "true" to the env block (or set
mcp.allow_writes: true in your config file). Default is off.
- Jira Cloud only. Jira Server / Data Center is out of scope for the entire product.
- Comments are not yet rendered (the field is fetched but the current renderer ignores it; landing in v0.2).
- The Jira Dev Status API used for development metadata is
semi-undocumented. It has been stable for ~10 years because
Atlassian's UI uses it, but no SLA is offered. Disable with
--include-dev-status=falseto opt out. - The gRPC service (
gojira serve) is single-tenant and ships without TLS or authentication; run it only on a loopback or trusted network. Streaming issue content over the wire, multi-tenancy, per-request config overrides, and TLS/auth are deferred to Phase 2. - gRPC write operations (
CreateIssue/UpdateIssue/AddComment/TransitionIssue) use the server's single startup identity; per-request credentials are deferred to a later phase. Issue deletion is not supported. - Observability and tracing (
--log-level trace) is opt-in; defaultinfoalready shows the end-of-run measurement summary. Cross-process tracing (e.g. OpenTelemetry export across the gRPC boundary) is out of scope for this release.
Future releases anticipated:
- A terminal UI (TUI) front-end over the gRPC API, as a gRPC client. (The MCP server already shipped — see MCP server.)
- Phase 2 service work: streaming rendered issue content over the wire, multi-tenant identities, per-request configuration overrides, and TLS/authentication.
gojira fetch— targeted single-issue (or small-list) retrieval without recursive expansion. The same renderer; just no JQL parent search and no descent into linked issues.- Write operations: create issues, update fields, post comments.
- JQL search: list issues matching a query and crawl the result set.
- Improved customisation: per-field rendering options, per-tenant field-name overrides.
No timelines committed. Direction only.
The entire suite runs under the standard Go toolchain — go test, table-
driven tests, golden files, httptest fakes, and bufconn for the gRPC
service. There is no third-party test runner; assertions use a mix of
stretchr/testify and the standard testing package.
# Run everything (the same command CI runs):
go test ./... -count=1
# With the race detector:
go test ./... -count=1 -race
# First-party statement coverage (attributes cross-package coverage so the
# library facade, exercised only by integtest/, is counted correctly):
go test ./... -count=1 -coverpkg=./... -coverprofile=cover.out
go tool cover -func=cover.out | tail -1The current first-party coverage is 77.6% of statements (excluding the
generated gen/ protobuf bindings). The core paths — the library facade,
crawl orchestration, rendering, parsing, and configuration cascade — sit
at 80–100%; the lowest-covered first-party packages are the MCP bridge
write path and the Dev Status error branches.
- docs/jira-markdown-crawler-design.md — the package-boundary design mini-doc.
- docs/engineering-principles.md — the rule book contributors and AI agents follow.
- docs/releasing.md — how gojira's build identity is stamped and how a maintainer cuts a release.
- docs/jira-releases.md — cutting Jira project Versions/Releases and attaching them to issues as Fix Versions.