Skip to content

Latest commit

 

History

History

jira

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

Table of Contents generated with DocToc

JIRA bridge

Capability: contract:tracker + contract:people + contract:security-tracker + contract:body-field

Kind: implementation

Vendor: Atlassian

JIRA REST helpers for the issue-* skill family. Adopters with JIRA-based issue trackers wire this in as their tracker bridge; adopters using GitHub Issues or other trackers contribute a parallel tools/<tracker>/ directory.

The bridge provides both read and write subcommands. The read-only contributor-activity queries of contract:tracker run in Python, in the jira backend of tools/contributor-metrics, through this package's REST client, jira_bridge.rest; the profile lookup of contract:people is a single REST read. See People and contributor-activity reads. The same package ships magpie-jira-tracker, the private-Jira backend of the security-tracker contract, whose reads and writes go through that client too — see Security tracker. Write operations require JIRA_API_TOKEN and follow the same write-path discipline as the GitHub bridge: every mutation is gated on explicit user confirmation in the calling skill — the bridge only executes confirmed actions.

Prerequisites

  • Runtime: Groovy 4.x+ on PATH (groovy tools/jira/bridge.groovy …); @Grab pulls the HTTP-client dependencies on first run, no separate install step. Python 3.11+ via uv (standard library only; body fields go through a vendored copy of github-body-field's parser) for jira_bridge.rest, magpie-jira-tracker and the pytest harness.
  • CLIs: groovy (4.x — the @Grab coordinate uses the org.apache.groovy group ID); uv only to run the tests.
  • Credentials / auth: ISSUE_TRACKER_URL (required) and ISSUE_TRACKER_PROJECT exported by the caller; write subcommands require JIRA_API_TOKEN (JIRA_AUTH_SCHEME = Basic default, or Bearer for ASF PATs). Anonymous-read trackers need no auth for read subcommands.
  • Network: the configured <issue-tracker> JIRA host (e.g. issues.apache.org/jira); @Grab reaches Maven Central on first run to resolve dependencies.
  • Optional: groovy on PATH for the pytest suite — tests auto-skip when Groovy is absent.

Layout

tools/jira/
├── README.md          (this file)
├── bridge.groovy      (Groovy reference implementation)
├── pyproject.toml     (Python test harness config)
├── src/jira_bridge/   (Python REST client, rest.py; the security tracker,
│                       security_tracker.py + tracker_client.py + markup.py)
└── tests/             (pytest test suite)

Other languages (Python, Bash + curl) are welcome via PR.

Invocation

groovy tools/jira/bridge.groovy <subcommand> [args]

The Groovy implementation uses @Grab for HTTP client dependencies — no separate install step. Requires Groovy 4.x or newer on PATH (the @Grab coordinate uses org.apache.groovy, which is the Groovy 4 group ID).

Read subcommands

search <JQL>

Run a JQL query against <issue-tracker> and emit matching issues as JSON to stdout:

groovy tools/jira/bridge.groovy search \
  'project = <KEY> AND status = Open AND resolution = Unresolved'

Output (truncated):

{
  "total": 42,
  "issues": [
    {"key": "<KEY>-9999", "title": "...", "status": "Open", "components": [...], "fixVersion": "..."},
    ...
  ]
}

The --limit <N> flag caps the result count (default: 50).

issue <KEY>

Fetch a single issue's full state (body, comments, attachments list, labels, fixVersion, etc.) as JSON:

groovy tools/jira/bridge.groovy issue <KEY>-9999

Output is the JIRA REST /rest/api/2/issue/<KEY> response, shaped for skill consumption.

projects

List the JIRA projects available at the configured <issue-tracker> URL. Useful during initial adoption to confirm the project key is correct.

groovy tools/jira/bridge.groovy projects

Write subcommands

All write subcommands require JIRA_API_TOKEN to be set and follow the write-path discipline described below.

comment <KEY> --body-file <path>

Post a comment on an issue. The comment body is read from a file to avoid shell-quoting issues:

groovy tools/jira/bridge.groovy comment FOO-9999 --body-file /tmp/comment.txt

Output:

{"ok": true, "key": "FOO-9999", "commentId": "12345"}

transition <KEY> <transition-name>

Move an issue to a new workflow state. The transition name is resolved case-insensitively against the issue's available transitions:

groovy tools/jira/bridge.groovy transition FOO-9999 "Resolve Issue"

Output:

{"ok": true, "key": "FOO-9999", "transition": "Resolve Issue", "transitionId": "21"}

If the transition name does not match any available transition, the command exits with an error listing the valid names.

label <KEY> --add <name> --remove <name>

Toggle labels on an issue. Both --add and --remove can be specified multiple times in a single call:

groovy tools/jira/bridge.groovy label FOO-9999 --add security --remove needs-triage

Output:

{"ok": true, "key": "FOO-9999", "added": ["security"], "removed": ["needs-triage"]}

Uses JIRA's atomic update API — no read-modify-write race.

assign <KEY> <username>

Set the assignee on an issue. Data Center only — Cloud uses accountId, which is not currently supported:

groovy tools/jira/bridge.groovy assign FOO-9999 jdoe

Output:

{"ok": true, "key": "FOO-9999", "assignee": "jdoe"}

field <KEY> <field-name> --value <value> / --value-json <json>

Edit a single field (including custom fields) on an issue. Use --value for plain string/number values. Use --value-json for structured values (priority, version, single-select, user picker, etc.):

# String value
groovy tools/jira/bridge.groovy field FOO-9999 customfield_10100 --value "high"

# Structured value (e.g. priority)
groovy tools/jira/bridge.groovy field FOO-9999 priority --value-json '{"name":"High"}'

# Array value (e.g. fixVersions)
groovy tools/jira/bridge.groovy field FOO-9999 fixVersions --value-json '[{"name":"1.2.3"}]'

Output:

{"ok": true, "key": "FOO-9999", "field": "priority", "value": {"name": "High"}}

attach <KEY> <file>

Attach a file to an issue:

groovy tools/jira/bridge.groovy attach FOO-9999 /tmp/report.txt

Output:

{"ok": true, "key": "FOO-9999", "attachments": [{"id": "99", "filename": "report.txt"}]}

People and contributor-activity reads

The read-only activity queries of contract:tracker and the profile lookup of contract:people, against the same Jira Data Center REST API v2 the bridge uses and with the same configuration (ISSUE_TRACKER_URL, ISSUE_TRACKER_PROJECT, JIRA_API_TOKEN, JIRA_AUTH_SCHEME). The per-person streams are implemented by the jira backend of tools/contributor-metrics over jira_bridge.rest; the others are plain REST reads a skill makes.

jira_bridge.rest treats the token as the user's personal credential and the URL as project configuration, which anyone with write access can commit:

  • the URL must be https:// (http:// only for localhost, 127.0.0.1 or ::1);
  • the token is sent only to the host the user confirmed for it — JIRA_API_HOST, or a host= line beside a token= line in ~/.config/apache-magpie/jira-token — and only on requests to that host; for any other host it is withheld, reads go out anonymously, and the caller is told why;
  • redirects are refused, so the token never follows one;
  • the token never appears in an error message. <user> is the person's Jira username, validated against [A-Za-z0-9._@+-]{1,255} before it reaches a query.
Verb Jira resolution
tracker list_filed(<user>, since, end) GET /rest/api/2/search with JQL project = "<KEY>" AND reporter = "<user>" AND created >= "<since>" AND created <= "<end> 23:59"
tracker list_triaged(<user>, since, end) GET /rest/api/2/search?expand=changelog&fields=comment,… over project = "<KEY>" AND updated >= "<since>" AND created <= "<end> 23:59"; an issue the user did not report counts when its changelog shows the user changing status, labels, component, priority, assignee, resolution or Fix Version, or the user commented, inside the window — dated by the first such action
tracker list_commented(<user>, since, end) the same search; an issue counts when the user commented inside the window, dated by the first such comment
tracker list_created(since, until) project = "<KEY>" AND created >= "<since>" AND created <= "<until> 23:59"; author_first_time from one reporter = "<author>" AND created < "<created>" count per author
tracker first_reply(<KEY-N>) GET /rest/api/2/issue/<KEY-N>/comment, the first comment by a rostered maintainer (Jira has no author-association signal)
people get_profile(<user>) GET /rest/api/2/user?username=<user> → displayName; a 404 means the account does not exist. Organisation, website and linked accounts are not exposed: null.
people list_collaborators, add_team_member not provided — Jira has no repository collaborators or forge teams (NotApplicable)

Jira has no pull requests: authored changes and reviews come from the project's code host through contract:change-request. Comment bodies are read inside the backend to find pushback candidates and never leave it.

Security tracker (magpie-jira-tracker)

The jira backend of the security-tracker contract: a project's security trackers in a private Jira project whose issues carry an issue-security level. It exposes the same subcommands and JSON as the local-files backend, and skills reach it through vetted operations, never directly (mapping).

uv run --project <framework>/tools/jira magpie-jira-tracker --url <url> --project <KEY> --security-level <level> <op> [args…]

Why the level is mandatory. Jira projects are often browsable by any logged-in user; the issue-security level is what makes a tracker private. create always sets the configured level and refuses to run without one. Before every write on an existing issue (edit, field-set, comment, close, reopen, rollup-*, attach) the backend reads the issue's security field and refuses with exit 4 unless it is exactly the configured level, before sending any content: a scheme can define levels open to every logged-in user. It never sets a level on an existing issue itself: an issue without one may already have been exposed, and a human decides what to do.

Credentials are jira_bridge.rest's. Every request goes through JiraRest.send, so the token comes from JIRA_API_TOKEN or the token file ~/.config/apache-magpie/jira-token, is bound to a host the user confirmed (JIRA_API_HOST or a host= line), is sent only to that host over https, follows no redirect, and is never printed. When the token is withheld — no confirmed host, or a different one — every write is refused (exit 4) rather than sent anonymously; reads go out anonymously. JIRA_AUTH_SCHEME (Basic default, Bearer for Data Center PATs) and JIRA_USER (pairs a raw token for Basic) work as for the bridge.

Flag Environment project.md key Meaning
--url ISSUE_TRACKER_URL tracker.jira.url Jira base URL.
--project SECURITY_TRACKER_PROJECT tracker.jira.project Key of the private project. Deliberately separate from ISSUE_TRACKER_PROJECT, which names the public tracker; an ID from another project is a usage error.
--security-level SECURITY_TRACKER_SECURITY_LEVEL tracker.jira.security_level The level create sets. Required for create.
--issue-type SECURITY_TRACKER_ISSUE_TYPE tracker.jira.issue_type Default Bug.
--deployment {server,cloud} JIRA_DEPLOYMENT tracker.jira.deployment Cloud uses the search endpoint /search/jql and account-ID assignees. When given, it wins over JIRA_DEPLOYMENT and --jira-cloud; vetted-ops always passes it.
--jira-cloud JIRA_DEPLOYMENT=cloud — Compatibility alias for --deployment cloud.

Under vetted-ops the URL, project and level are pinned by the policy's [tracker] table, not by the caller.

Operation Jira mapping
IDs FOOSEC-12, or a bare 12 expanded with the project key.
view closed when resolved or the status category is Done; milestone is the first fix version; adds security_level and warnings (one when the issue has no level).
list JQL project = KEY, the same open / closed rule as view (resolution or Done category), one labels = "L" per label, text ~ "…" for --search, newest first.
labels Distinct labels across the project (at most 5000 issues scanned).
create / edit summary, description, labels, issuetype, security; labels via update.labels, milestone via fixVersions, one assignee.
comment --author is echoed but Jira records the authenticated user.
close / reopen A transition into (or out of) the Done category; close sets Fixed or Won't Fix and retries without a resolution when the screen rejects one.
attach Multipart upload; --name reduced to a base name.
body The description with its field headings as ### <Heading> and h4.–h6. as ####–######, raw (not JSON), for generate-cve-json --stdin: the same fields field-get reads.
pending / publish No-ops: Jira writes are immediate.
doctor URL, project, confirmed token, configured level, /myself, project reachability, and whether the level exists (403/404 on the listing reports unverified).

Storage. Body fields are h3. <Heading> wiki-markup sections of the description (markdown ### sections are read too), read and written through a byte-identical copy of github-body-field's parser (body_field_format.py, kept exact by a test), so they follow the body-field contract: every column-0 ### line ends a field, code block or not, a heading inside a closed code block is never a field, and a {code} / {noformat} macro opened mid-line before field headings is refused as ambiguous. A wiki heading of level 1 to 3 outside closed blocks reads as ### <Heading>; closed blocks are CommonMark fence pairs and {code} / {code:…} / {noformat} blocks opened at the start of a line and closed on the bare macro, and a block left open marks nothing. create and edit escape every ### line inside a closed block with a leading \, so quoted text never becomes a field copy. Names match with case and inner whitespace ignored; field-get and field-set exit 4 on a duplicate or disputed field, and field-set on a value that would add, hide or move a field heading. The status rollup is one comment opening with the invisible anchor {anchor:magpie-status-rollup-v1}, entries *YYYY-MM-DD · @user · Action* separated by ----, oldest first; rollup-amend-latest exits 4 when the latest action differs. Neither rollup command prints the rollup text.

Configuration

The bridge reads its configuration from the environment:

Variable Notes
ISSUE_TRACKER_URL required; e.g. https://issues.apache.org/jira
ISSUE_TRACKER_PROJECT project key (e.g. FOO)
JIRA_API_TOKEN required for write subcommands — see auth notes below
JIRA_AUTH_SCHEME Basic (default) or Bearer — see auth notes below

The caller is responsible for exporting these (a skill resolves them from <project-config>/issue-tracker-config.md and passes them in the environment). Direct file-fallback inside the bridge is a possible future enhancement — it is not implemented today; the bridge exits if ISSUE_TRACKER_URL is unset.

For anonymous-read trackers, no auth is required for read subcommands. Write subcommands always require JIRA_API_TOKEN and exit with an error if it is unset.

Authentication: This bridge targets JIRA Data Center (DC), specifically ASF JIRA at issues.apache.org/jira. Cloud is not currently supported (assign uses DC name, not Cloud accountId).

  • Basic auth (default): set JIRA_API_TOKEN to the base64-encoded username:password or username:pat string.
  • Bearer auth (ASF PATs): set JIRA_AUTH_SCHEME=Bearer and JIRA_API_TOKEN to the raw PAT string. ASF JIRA DC PATs use Authorization: Bearer <pat>.

Output contract

Every subcommand emits JSON to stdout on success, or a non-zero exit code with a human-readable error to stderr on failure.

Write subcommands return {"ok": true, "key": "<KEY>", ...} with operation-specific fields as documented per subcommand above.

The output schema is documented per subcommand above. Skills parse the JSON via standard JSON tooling — no special envelope, no wrapper.

Write-path discipline

The bridge executes mutations but does not decide whether to mutate. Every write operation is gated on explicit user confirmation in the calling skill — the bridge only executes confirmed actions.

This mirrors the GitHub bridge's write-path discipline (see tools/github/operations.md): skills surface the proposed action to the maintainer, wait for confirmation, then call the bridge to execute.

Testing

The Groovy bridge tests use a mock HTTP server and require groovy on PATH; they are skipped automatically when Groovy is not available. The Python tests (test_rest.py, test_security_tracker.py, test_markup.py) need no network: JiraRest takes a fake opener, and the security tracker a fake transport.

cd tools/jira
uv run pytest

Cross-references