Claude Skill

jira-syntax

Use when writing or formatting Jira descriptions, comments, or any text destined for Jira. Converts Markdown to Jira wiki markup, provides templates (bug reports, feature requests), and validates syntax before submission. Trigger on any Jira content authoring task.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download netresearch-jira-skill-skills_jira-syntax-1f04f24.zip · 26 KB
Part of netresearch/jira-skill — 3 skills

Install

skills CLI npx skills add https://github.com/netresearch/jira-skill/tree/main/skills/jira-syntax
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install netresearch-jira-skill@llmmart
Git git clone https://github.com/netresearch/jira-skill.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole netresearch/jira-skill collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Jira Syntax

Jira wiki markup syntax, templates, and validation. For API operations, use the jira-communication skill.

Quick Syntax Reference

Jira Syntax Purpose NOT this (Markdown)
h2. Title Heading ## Title
*bold* Bold **bold**
_italic_ Italic *italic*
{{code}} Inline code `code`
{code:java}...{code} Code block java ```
[text\|url] Link [text](url)
[PROJ-123] Issue link -
[~username] User mention @username
* item Bullet list - item
# item Numbered list 1. item
\|\|Header\|\| Table header \|Header\|

See references/jira-syntax-quick-reference.md for complete syntax documentation.

Available Templates

Bug Report

Path: templates/bug-report-template.md

Sections: Environment, Steps to Reproduce, Expected/Actual Behavior, Error Messages, Technical Notes

Feature Request

Path: templates/feature-request-template.md

Sections: Overview, User Stories, Acceptance Criteria, Technical Approach, Success Metrics

Syntax Validation

Run before submitting to Jira:

${CLAUDE_SKILL_DIR}/scripts/validate-jira-syntax.sh path/to/content.txt

It gates the post — run it as its own step, never chained with the posting command.

Validation Checklist

  • Headings: h2. Title (space after period)
  • Bold: *text* (single asterisk)
  • Code blocks: {code:language}...{code}
  • Lists: * for bullets, # for numbers
  • Links: [label|url] or [PROJ-123]
  • Tables: ||Header|| and |Cell|
  • Colors: {color:red}text{color}
  • Panels: {panel:title=X}...{panel}

Common Mistakes

❌ Wrong ✅ Correct
## Heading h2. Heading
**bold** *bold*
`code` {{code}}
[text](url) [text\|url]
- bullet * bullet
h2.Title h2. Title
MR !42 (bare GitLab ref) [MR 42\|url] or full group/project!42 — a bare !…! is image markup
(/) on an open/proposed item (x) — (/) renders as a green check (done); use (x) for open items
( ) as a checkbox (x) — ( ) is not a macro and renders literally
(-) or (x) on a pending step in a review (i) pending: … — (-) renders as a red no-entry sign, and in a review (x) means a failure

Integration with jira-communication Skill

Workflow:

  1. Get template from jira-syntax
  2. Fill content using Jira wiki markup
  3. Validate with ${CLAUDE_SKILL_DIR}/scripts/validate-jira-syntax.sh
  4. Submit via jira-communication skill

References

  • references/jira-syntax-quick-reference.md - Complete syntax documentation Includes Ask the renderer instead of reasoning about it: POST /rest/api/1.0/render returns the HTML Jira will produce for any markup, before anything is posted. Use it to settle a markup question rather than deriving the answer.
  • references/cross-project-refs.md - GitLab cross-project ref convention (group/project!N, group/project#N, group/project@tag) when linking to GitLab from Jira
  • templates/bug-report-template.md - Bug report template
  • templates/feature-request-template.md - Feature request template
  • ${CLAUDE_SKILL_DIR}/scripts/validate-jira-syntax.sh - Automated syntax checker
  • Official Jira Wiki Markup
Files (jira-skill)
  • evals
    • evals.json 2.5 KB
      [
        {
          "name": "convert_markdown_to_jira",
          "prompt": "Convert this Markdown to Jira wiki format: **bold** text with [link](https://example.com) and `code`",
          "assertions": [
            {
              "type": "content",
              "pattern": "\\*bold\\*"
            },
            {
              "type": "content",
              "pattern": "\\[link\\|https://example\\.com\\]"
            },
            {
              "type": "content",
              "pattern": "\\{\\{code\\}\\}"
            },
            {
              "type": "not_content",
              "pattern": "\\*\\*bold\\*\\*"
            }
          ]
        },
        {
          "name": "bug_report_with_table",
          "prompt": "Write a Jira bug report description with a table of reproduction steps (Step, Action, Expected, Actual) for a file upload bug",
          "assertions": [
            {
              "type": "content",
              "pattern": "\\|\\|.*Step.*\\|\\|"
            },
            {
              "type": "content",
              "pattern": "h[1-3]\\."
            },
            {
              "type": "not_content",
              "pattern": "^#{1,3} "
            },
            {
              "type": "not_content",
              "pattern": "\\*\\*"
            }
          ]
        },
        {
          "name": "format_code_block_in_jira",
          "prompt": "Show me how to include a Java stack trace in a Jira comment with proper formatting",
          "assertions": [
            {
              "type": "content",
              "pattern": "\\{code(:java)?\\}"
            },
            {
              "type": "not_content",
              "pattern": "```"
            }
          ]
        },
        {
          "name": "escape_literal_block_tag_mention",
          "prompt": "Write a short Jira QA comment in wiki markup with one finding line stating that all commands are documented in {code} blocks with command and output together, plus one real code block containing: ls -l",
          "assertions": [
            {
              "type": "content",
              "pattern": "\\\\\\{code\\\\\\}"
            },
            {
              "type": "not_content",
              "pattern": "\\{code\\}[^\\n]*[A-Za-z]"
            }
          ]
        },
        {
          "name": "pending_step_marker_in_review",
          "prompt": "Write a short Jira QA review comment in wiki markup with two status lines. Use a status icon on each line: the GitLab service update is done, and the runner manager update is still pending until the service is verified.",
          "assertions": [
            {
              "type": "content",
              "pattern": "\\(/\\)[^\\n]*[Ss]ervice"
            },
            {
              "type": "content",
              "pattern": "\\(i\\)[^\\n]*[Pp]ending"
            },
            {
              "type": "not_content",
              "pattern": "\\(-\\)"
            },
            {
              "type": "not_content",
              "pattern": "\\(x\\)[^\\n]*[Pp]ending"
            }
          ]
        }
      ]
      
  • references
    • cross-project-refs.md 2.4 KB
      # Cross-Project References to GitLab Resources
      
      When referencing GitLab merge requests, issues, tags, or commits **from a Jira description, comment, or worklog**, prefer the GitLab cross-project autolink syntax — even though Jira itself doesn't autolink GitLab paths, the cross-project form removes ambiguity for human readers and matches the convention used in GitLab itself.
      
      ## Convention
      
      | GitLab resource | Short form (single-project context) | Cross-project form (use this in Jira) |
      |---|---|---|
      | Merge request | `!42` | `group/project!42` |
      | Issue | `#123` | `group/project#123` |
      | Tag / commit / branch | `vX.Y.Z` | `group/project@vX.Y.Z` |
      
      The same prefix characters (`!`, `#`, `@`) are what GitLab itself recognises for cross-project autolinks in markdown, commit messages, and MR descriptions — so a reader who pastes the reference into a GitLab UI will get a working link automatically.
      
      ## In Jira wiki markup
      
      Wrap the cross-project text inside the standard Jira link syntax `[text|url]`:
      
      ```
      [jira/jira!25|https://git.netresearch.de/jira/jira/-/merge_requests/25]
      [jira/jira@v9.12.3-2|https://git.netresearch.de/jira/jira/-/tags/v9.12.3-2]
      [jira/jira#42|https://git.netresearch.de/jira/jira/-/issues/42]
      ```
      
      ## Why not just `!25`?
      
      A bare `!25` in a Jira issue forces the reader to guess which GitLab project it lives in — and to *click* to find out. A bare `v9.12.3-2` is even worse: tag names are reused across many repos.
      
      When an issue references multiple repos in the same group (e.g. `jira/jira` for the image build and `jira/app` for the deploy stack), the cross-project form is essential — `!3` could be either repo's MR.
      
      ## Inside `{{...}}` monospace
      
      `!`, `#`, and `@` all work inside `{{...}}`, but **unescaped `{`, `}` and `*` inside a `{{...}}` block break Jira's parser**: a raw `{` makes the whole block render as raw text, and a `*` pair turns bold mid-token. Escape them with a backslash (verified against the Jira Server 9.12 wiki renderer).
      
      Broken — the block falls apart on the raw `{`, and `backup-` renders bold:
      
      ```
      {{compose.example.{yml,override.pga.yml}}}
      {{jira-*backup-*}}
      ```
      
      Correct — renders `compose.example.{yml,override.pga.yml}` and `jira-*backup-*` in monospace:
      
      ```
      {{compose.example.\{yml,override.pga.yml\}}}
      {{jira-\*backup-\*}}
      ```
      
      Splitting into separate `{{...}}` references also works when escaping would hurt readability.
      
      The `validate-jira-syntax.sh` script catches the brace collision.
      
    • jira-syntax-quick-reference.md 16.8 KB
      # Jira Wiki Markup Syntax - Quick Reference
      
      Complete reference for Jira's wiki markup syntax to ensure proper formatting in tickets, comments, and descriptions.
      
      ## Table of Contents
      
      - [Text Formatting](#text-formatting)
      - [Headings](#headings)
      - [Lists](#lists)
      - [Links](#links)
      - [Code Blocks](#code-blocks)
      - [Tables](#tables)
      - [Panels and Quotes](#panels-and-quotes)
      - [Colors](#colors)
      - [Special Blocks](#special-blocks)
      - [Line Breaks and Horizontal Rules](#line-breaks-and-horizontal-rules)
      - [Special Characters](#special-characters)
      - [Checklist Markers](#checklist-markers)
      - [Validation Checklist](../SKILL.md#validation-checklist)
      - [Common Mistakes](../SKILL.md#common-mistakes)
      
      ## Text Formatting
      
      | Syntax | Output | Use Case |
      |--------|--------|----------|
      | `*text*` | **text** | Bold/strong emphasis |
      | `_text_` | *text* | Italic/emphasis |
      | `{{text}}` | `text` | Monospace for code/paths |
      | `-text-` | ~~text~~ | Strikethrough |
      | `+text+` | <u>text</u> | Underline/inserted text |
      | `^text^` | text^superscript^ | Superscript |
      | `~text~` | text~subscript~ | Subscript |
      | `??text??` | text (citation) | Citation format |
      
      ## Headings
      
      ```
      h1. Heading Level 1 (largest)
      h2. Heading Level 2
      h3. Heading Level 3
      h4. Heading Level 4
      h5. Heading Level 5
      h6. Heading Level 6 (smallest)
      ```
      
      Space required after the period (`h2. Title`, not `h2.Title`).
      
      ## Lists
      
      ### Bulleted Lists
      ```
      * Level 1 item
      ** Level 2 nested item
      *** Level 3 nested item
      * Another level 1 item
      ```
      
      ### Numbered Lists
      ```
      # First item
      ## Nested item
      ## Another nested item
      # Second item
      ```
      
      ### Mixed Lists
      ```
      # Numbered item
      #* Nested bullet
      #* Another bullet
      # Another numbered item
      ```
      
      **Rules:**
      - Space after `*` or `#`
      - Nesting uses additional symbols (`**`, `##`), not indentation
      - Can mix list types with combined syntax (`#*`)
      
      ## Links
      
      | Type | Syntax | Example |
      |------|--------|---------|
      | Issue Link | `[KEY-123]` | `[PROJ-456]` |
      | User Mention | `[~username]` | `[~john.doe]` |
      | External URL | `[http://url]` | `[http://example.com]` |
      | Labeled Link | `[Label\|url]` | `[Google\|http://google.com]` |
      | Attachment | `[^filename]` | `[^screenshot.png]` |
      | Email | `[mailto:email]` | `[mailto:team@example.com]` |
      | Anchor | `{anchor:name}` + `[#name]` | `{anchor:intro}` → `[#intro]` |
      
      ## Code Blocks
      
      ### Inline Code
      Use `{{text}}` for inline monospace text.
      
      ### Code Blocks with Syntax Highlighting
      ```
      {code:java}
      System.out.println("Hello, World!");
      {code}
      ```
      
      **Supported Languages (Jira Server / Data Center):**
      
      The Jira Server source-code formatter accepts ONLY this fixed list. Using any other identifier (e.g. `typoscript`, `rust`, `typescript`, `yml`, `shell`) produces:
      
      > Unable to find source-code formatter for language: `<name>`. Available languages are: ...
      
      | Group | Identifiers |
      |-------|-------------|
      | General-purpose | `actionscript`, `ada`, `applescript`, `c`, `c#`, `c++`, `cpp`, `erlang`, `go`, `groovy`, `haskell`, `java`, `javascript`, `js`, `lua`, `objc`, `perl`, `php`, `python`, `r`, `ruby`, `scala`, `swift`, `visualbasic` |
      | Shell / scripting | `bash`, `sh` |
      | Data / markup | `css`, `html`, `json`, `sql`, `xml`, `yaml` |
      | Special | `none` (no highlighting), `nyan`, `rainbow` |
      
      **Notes:**
      - Use `c#` / `c++` literally, not `csharp` / `cplusplus` (though `cpp` is also accepted).
      - There is no `typescript`, `rust`, `kotlin`, `dart`, `powershell`, `shell`, `yml`, `dockerfile`, `terraform`, or `typoscript` formatter.
      - For unsupported languages, fall back to `{code:none}` (or `{noformat}`) to preserve the block without highlighting.
      
      ```
      {code:none}
      [request && request.getNormalizedParams().getHttpHost() == "backend.example.de"]
      page.meta.robots = noindex,noarchive
      [END]
      {code}
      ```
      
      ### Preformatted Text (No Highlighting)
      ```
      {noformat}
      Plain text without syntax highlighting
      Preserves whitespace and formatting
      {noformat}
      ```
      
      ### Block Tags Are Never Inline
      
      `{code}`, `{noformat}`, `{quote}` and `{panel}` are **block-level** macros: the tag must stand alone on its own line. An unescaped tag inside a prose sentence opens a real block mid-line and swallows the rest of the line — the classic failure is writing *about* code blocks.
      
      Broken — renders everything after "in" as an opened code block:
      
      ```
      All commands are documented in {code} blocks with output.
      ```
      
      Correct — renders the literal text `{code}`:
      
      ```
      All commands are documented in \{code\} blocks with output.
      ```
      
      Escape literal mentions of any block tag with backslashes: `\{code\}`, `\{noformat\}`, `\{quote\}`, `\{panel\}`. For inline monospace use `{{...}}`, never an inline `{code}` pair — even `{code}one-liner{code}` renders as a block, not inline.
      
      `scripts/validate-jira-syntax.sh` flags inline block tags and unbalanced tag counts; run it on composed text before submitting to Jira.
      
      ## Tables
      
      ### Basic Table
      ```
      ||Header 1||Header 2||Header 3||
      |Cell A1|Cell A2|Cell A3|
      |Cell B1|Cell B2|Cell B3|
      ```
      
      **Rules:**
      - `||` for header cells (double pipe)
      - `|` for regular cells (single pipe)
      - Rows must have same number of cells
      - No trailing pipe at end of row
      
      ## Panels and Quotes
      
      ### Panel with Title and Background
      ```
      {panel:title=Important Information|bgColor=#FFFFCE}
      Content inside the panel
      {panel}
      ```
      
      **Panel Parameters:**
      - `title=text` - Panel heading
      - `bgColor=#HEXCODE` - Background color
      - `borderStyle=solid|dashed` - Border style
      - `borderColor=#HEXCODE` - Border color
      - `titleBGColor=#HEXCODE` - Title background
      
      ### Quote Block
      ```
      {quote}
      Multi-line quoted text
      Can span multiple paragraphs
      {quote}
      ```
      
      ### Single Line Quote
      ```
      bq. This is a block quote on one line
      ```
      
      ## Colors
      
      ```
      {color:red}Red text{color}
      {color:blue}Blue text{color}
      {color:green}Green text{color}
      {color:#FF0000}Hex color text{color}
      ```
      
      Named colors (`red`, `blue`, `green`, `yellow`, `orange`, `purple`, `black`, `white`, `gray`/`grey`) or any hex code work as the macro parameter.
      
      ## Special Blocks
      
      ### Expand/Collapse Section
      ```
      {expand:title=Click to expand}
      Hidden content that can be toggled
      {expand}
      ```
      
      ## Line Breaks and Horizontal Rules
      
      ```
      Line 1\\
      Line 2 (line break with \\)
      
      First paragraph
      
      Second paragraph (blank line creates new paragraph)
      
      ----
      Horizontal rule (4 dashes)
      ```
      
      ## Special Characters
      
      ```
      --- (em-dash: —)
      -- (en-dash: –)
      \\ (line break)
      \{escaped brace\}
      ```
      
      To escape special characters, use backslash: `\*`, `\{`, `\[`.
      
      - **Only escape characters Jira actually parses as markup** — `*`, `_`, `-`, `+`, `^`, `~`, `{`, `[`, `|`, `\`. Do **not** escape plain punctuation such as `.`, `,`, or `:` — `\.` renders the backslash literally and produces the wrong output.
      - **Never escape inline monospace** — `{{text}}` is not a macro, so `\{\{text\}\}` is wrong. Only escape the opening brace of a *macro name* shown as prose (e.g. `\{code\}`).
      - **Preserve existing backslash escapes** — a source `\*`, `\_`, or `\{` already suppresses Markdown markup; keep it as-is, because Jira uses the same `\` escape mechanism for the same characters.
      
      ### Common gotcha: macro names in prose
      
      Writing a macro name literally in prose (e.g. *"commands wrapped in \{code\} blocks"*) without escaping breaks rendering — Jira parses the literal as the *start* of a code-block macro and either consumes the rest of the comment or pairs with the next unrelated occurrence it finds. The same trap applies to any macro that opens and closes with the same token: `{noformat}…{noformat}`, `{quote}…{quote}`, `{color}…{color}`, `{panel}…{panel}`, `{anchor}…{anchor}`, and so on.
      
      Three ways to write the literal token safely, in order of preference:
      
      | Approach | Example | When to use |
      |----------|---------|-------------|
      | Rephrase to avoid the token | `commands shown as code blocks` | First choice — readers don't need the macro name to understand the prose |
      | Backslash-escape | `\{code\}` | When you genuinely need to show the macro name |
      | Wrap in a code span | `` `{{code}}` `` | Last resort — some style guides ban inline `{{monospace}}` in favour of bold `*term*` for technical terms |
      
      The backslash escape is the official Jira mechanism; the rephrase is editorial; the `{{monospace}}` wrap renders fine but is disliked by teams that reserve monospace for actual code spans rather than inline references.
      
      ### Common gotcha: prose struck through by `-text-`
      
      `-text-` is strikethrough. The grammar below is measured against a live Jira Server 9.12 wiki renderer, and the recorded cases are kept in the source repo (netresearch/jira-skill), not in the standalone skill package; it is not a rule of thumb, and an earlier version of this section stated it wrongly in both directions.
      
      > **opener** — an unescaped `-` at line start or after a **non-word character**, followed by neither whitespace nor another dash
      > **closer** — the next *valid* closer: an unescaped `-` that is not preceded by whitespace and is followed by a non-word character or line end; a dash failing either condition is skipped over, not fatal
      > **body** — anything in between; a dash that fails the closer conditions is skipped over, not fatal
      
      Two consequences are worth stating on their own, because both are the opposite of what the shape suggests.
      
      **A pair of CLI flags is not a span — on its own.** `journalctl -b -p crit` renders literally, and so do `--strict ... -v` and `offset by -5 seconds`, because a dash that *leads* a word can never close a span, so flags cannot pair with each other. They are not immune, though: put a trailing-dash word anywhere later on the same line and the flag becomes the opener — `journalctl -b -p crit zeigt die Fehler; das Modul ist zu- und abschaltbar.` is struck from `-b` to `zu-`.
      
      **The real trap is a dash after an inline element, closed by a trailing-dash word.** Any inline element's closing punctuation — `}}`, `*`, `_`, `]`, `!`, `{color}` — is a non-word character, so `{{nr-pforum}}-Extensions` opens a span; a German elliptical compound (`zu- und abschaltbar`) or any other word ending in a dash then closes it, and everything between renders struck through. German prose produces this shape routinely. A single flag also becomes dangerous once such a closer appears later on the same line: `with -v and a trailing word- here`.
      
      Exempt, and not to be escaped: a dash inside a word (`Round-1`, `2026-09-04`, `Größe-x`), a leading dash with no closer anywhere on the line, em/en-dash typography (`---`, `--`), list bullets (`- item`), Unicode dashes (`–`, `—`), and anything inside `{code}`/`{noformat}`. `{quote}` and `{panel}` are **not** exempt — Jira parses text effects inside them.
      
      The fix is to backslash-escape the whole dash run that opens the span: `{{nr-pforum}}\-Extensions`. A `\-` reaches the rendered HTML as `&#45;` and prints as a plain hyphen, so the reader sees no difference. Escaping only part of a run does not work: in `{{--strict}}` the opener is the *second* dash, and neutralising just that one promotes the first — write `{{\-\-strict}}`. Putting the command in a `{code}` block avoids the question entirely.
      
      You normally do not have to do any of this by hand. Every `jira-communication` option that writes wiki markup — `jira-comment.py add`/`edit`, `jira-transition.py do --comment`, `jira-transition.py path --comment`, `jira-worklog.py add --comment`, and the `--description` of `jira-create.py issue` and `jira-issue.py update` — escapes these spans automatically before posting and report on stderr which lines they changed (`--no-auto-escape` keeps the markup verbatim; posting a deliberate span needs `--no-auto-escape --force`, because the lint and the render check each still refuse it). For a draft that does not go through those scripts, run `skills/jira-syntax/scripts/validate-jira-syntax.sh <file>` on it (from the repo root). The script verifies that the six paired macros (`code`, `panel`, `color`, `noformat`, `quote`, `anchor`) are balanced — every opener matches a closer, even with a language tag like `{code:bash}` — and catches Markdown leakage (` ``` ` fences, `[text](url)` links, `` `code` `` spans), language declarations Jira Server does not recognise, malformed table headers, and dash pairs outside code blocks that would render struck through.
      
      ## Checklist Markers
      
      `(/)` and `(x)` are the conventional checklist markers: `(/)` for a completed
      item, `(x)` for an open one. Use them only with that meaning.
      
      - Do not put `(/)` on items that are merely proposed or not yet implemented.
        It renders as a green check and reads as "done".
      - `( )` (empty parentheses) is **not** a macro. It renders literally as two
        parentheses, so it conveys nothing. For an open item use `(x)`, or a plain
        bullet when no status is intended.
      
      ```
      * (/) Migration script written and tested
      * (x) Rollback procedure documented
      ```
      
      What each marker draws, from the render endpoint of Jira Server 9.12:
      
      | Markup | Icon | Reads as |
      | --- | --- | --- |
      | `(/)` | green check (`check.png`) | done |
      | `(x)` | red cross (`error.png`) | failed, or open in a plain checklist |
      | `(!)` | warning triangle (`warning.png`) | needs attention |
      | `(i)` | blue info (`information.png`) | note |
      | `(?)` | question mark (`help_16.png`) | unclear |
      | `(off)` | grey light bulb (`lightbulb.png`) | not applicable, inactive |
      | `(-)` | red no-entry sign (`forbidden.png`) | forbidden |
      
      In a review or QA comment, `(x)` already means a blocking failure, so it cannot also
      mean "not done yet". Mark a step that is still pending with `(i)` and the word
      "pending", or with a plain bullet. Never use `(-)` for it: the red no-entry sign
      reads as "must not be done". Three of seven tickets in one maintenance window marked
      pending steps that way.
      
      ```
      * (/) Service updated to 19.4.1
      * (i) pending: runner manager, after the service is verified
      ```
      
      ## Ask the renderer instead of reasoning about it
      
      Jira renders wiki markup server-side, and it will tell you what it is going to do — for any markup, before anything is posted:
      
      ```bash
      curl -s -H "Authorization: Bearer $JIRA_PERSONAL_TOKEN" -H 'Content-Type: application/json' \
        -X POST "$JIRA_URL/rest/api/1.0/render" \
        -d '{"rendererType":"atlassian-wiki-renderer","unrenderedMarkup":"Die {{a}}-Extensions, jede zu- und abschaltbar","issueKey":null}'
      ```
      
      This is the endpoint behind Jira's own preview button. It is Server/DC only (Cloud uses ADF and has no equivalent), it needs no issue, and it writes nothing. It rate-limits with HTTP 429 above roughly three to eight parallel requests, so a batch run needs backoff.
      
      **It is the same renderer that stores a comment.** Verified by rendering the full 1185-character source of an existing comment and diffing against that comment's stored `renderedBody` (`GET /rest/api/2/issue/<KEY>/comment/<id>?expand=renderedBody`) — byte-identical. So the preview is proof, not an approximation.
      
      Use it whenever a claim about Jira markup is about to be written down — in a lint, a ticket, a reference page like this one. It costs one call and it settles the question. Two successive hand-derived versions of the strikethrough rule in this repo were wrong in opposite directions, and the second passed 168 hand-picked cases while still being wrong; a generated corpus rendered through this endpoint found the defect in minutes. Two runners built on it — one to re-record the curated cases, one to generate and record a corpus — live in the source repo (netresearch/jira-skill), not in the standalone skill package; the curl above is the whole technique and needs neither.
      
      Two things it cannot settle, because they are not in the markup:
      
      - **Instance state.** Jira substitutes autolinked issue keys before text effects run, so `OPS-899-x ... zu-` renders struck through where OPS-899 exists and literally where it does not. Render against the instance you will post to.
      - **What the markup was meant to say.** The renderer answers "what will this look like", never "is this what you wanted".
      
      ## Validation is a gate, not a formality
      
      `scripts/validate-jira-syntax.sh` only helps if its **result** is read before
      the content is posted. Run it as its own step:
      
      ```bash
      validate-jira-syntax.sh comment.txt   # read the result
      # only then:
      jira-comment.py add PROJ-123 -        # body piped from the same file
      ```
      
      Never chain it with the posting command. With `;` the comment posts even
      though validation failed; with `&&` the report scrolls past unread. Either way
      the broken comment is already on the ticket and needs a follow-up edit —
      visible to everyone watching it.
      
      ### Frequent catch: braces inside a monospace span
      
      `${VAR}` (or any `{`/`}`) inside `{{...}}` breaks the span — the Jira parser
      renders it as raw text:
      
      ```
      {{traefik.http.middlewares.office-allow-${ENVIRONMENT}.ipallowlist.sourcerange}}
      ```
      
      The validator reports:
      
      ```
      ERROR: Found unescaped { or } inside {{...}} monospace block — Jira parser
      will render it as raw text. Escape as \{ \} or split the reference.
      ```
      
      Escape the braces as `\{ \}`, split the reference, or move the whole line into
      a `{code}` block — a `{code}` block is usually the most readable for anything
      command-shaped.
      
  • scripts
    • validate-jira-syntax.sh 26.1 KB
      #!/bin/bash
      
      # Jira Wiki Markup Syntax Validator
      # Checks text for common Jira syntax errors and suggests corrections
      
      set -e
      
      # Colors for output
      RED='\033[0;31m'
      YELLOW='\033[1;33m'
      GREEN='\033[0;32m'
      NC='\033[0m' # No Color
      
      # Counters
      ERRORS=0
      WARNINGS=0
      
      # Function to print error
      # Use pre-increment so the (( )) expression is the new (non-zero) value;
      # `((ERRORS++))` returns the OLD value (0 on first call) and trips `set -e`.
      error() {
          echo -e "${RED}❌ ERROR:${NC} $1"
          ((++ERRORS))
      }
      
      # Function to print warning
      warning() {
          echo -e "${YELLOW}⚠️  WARNING:${NC} $1"
          ((++WARNINGS))
      }
      
      # Function to print success
      success() {
          echo -e "${GREEN}✅ $1${NC}"
      }
      
      # Function to check file
      validate_file() {
          local file="$1"
          echo ""
          echo "=========================================="
          echo "Validating: $file"
          echo "=========================================="
      
          if [ ! -f "$file" ]; then
              error "File not found: $file"
              return
          fi
      
          # Hybrid template files (templates/*.md) are a Markdown wrapper carrying
          # the Jira markup inside ``` fences. For those, validate the fenced
          # payload: untagged ``` fences hold Jira markup to be copy-pasted;
          # language-tagged fences (```bash) are foreign code and are skipped, as is
          # the wrapper prose. Files without fences — drafts about to be posted —
          # keep the full strict treatment.
          local content
          if grep -qE '^[[:space:]]*```' "$file"; then
              echo "   (hybrid file: validating untagged \`\`\` fenced payload as Jira markup)"
              content=$(awk '
                  /^[[:space:]]*```/ {
                      if (infence) { infence = 0 }
                      else { infence = 1; tagged = ($0 ~ /^[[:space:]]*```./) }
                      next
                  }
                  infence && !tagged { print }
              ' "$file")
          else
              content=$(cat "$file")
          fi
      
          # Check for Markdown-style headings (## instead of h2.)
          if echo "$content" | grep -qE "^##+ "; then
              error "Found Markdown-style headings (##). Use Jira format: h2. Heading"
              echo "   Lines with issue:"
              echo "$content" | grep -nE "^##+ " | head -5
          fi
      
          # Check for Markdown-style bold (**text** instead of *text*)
          if echo "$content" | grep -qE "\*\*[^*]+\*\*"; then
              warning "Found Markdown-style bold (**text**). Use Jira format: *text*"
              echo "   Examples found:"
              echo "$content" | grep -oE "\*\*[^*]+\*\*" | head -3
          fi
      
          # Check for Markdown-style italic (_text_ is ok, but *text* for bold might be confused)
          if echo "$content" | grep -qE "\*[^*]+\*\*[^*]+\*"; then
              warning "Found potential Markdown-style italic mixed with bold"
          fi
      
          # Check for Markdown-style code blocks (``` instead of {code})
          if echo "$content" | grep -qE "^\`\`\`"; then
              error "Found Markdown code blocks (\`\`\`). Use Jira format: {code:language}"
              echo "   Lines with issue:"
              echo "$content" | grep -nE "^\`\`\`" | head -5
          fi
      
          # Check for Markdown-style inline code (` instead of {{)
          if grep -qE "\`[^\`]+\`" <<< "$content"; then
              warning "Found Markdown inline code (\`code\`). Consider Jira format: {{code}}"
          fi
      
          # Check for unescaped { or } inside {{...}} monospace blocks. The Jira parser
          # is greedy and breaks on raw inner braces, rendering the block as raw text
          # (e.g. {{compose.example.{yml,override.pga.yml}}} renders verbatim).
          # Backslash-escaped braces (\{ \}) render literally and are fine.
          # Two failure modes:
          #   1. {{ followed by another raw { before any } — e.g. {{path/{a,b}.txt}}
          #      or {{a{b}c}}.
          #   2. A {{ block with an extra raw } before the closing }} — e.g. {{a}b}}.
          # `([^...\\]|\\.)*` skips escaped characters so \{ and \} don't false-positive.
          local brace_re='\{\{([^{}\\]|\\.)*\{|\{\{([^{}\\]|\\.)*\}([^{}\\]|\\.)*\}\}'
          if grep -qE "$brace_re" <<< "$content"; then
              error "Found unescaped { or } inside {{...}} monospace block — Jira parser will render it as raw text. Escape as \\{ \\} or split the reference."
              echo "   Lines with issue:"
              grep -nE "$brace_re" <<< "$content" | head -3
          fi
      
          # Check for unescaped * inside {{...}} monospace blocks. Jira still parses
          # inline markup inside {{...}}: a * pair turns bold mid-token
          # (e.g. {{jira-*backup-*}} renders "backup-" bold). Escape as \*.
          local star_re='\{\{([^{}*\\]|\\.)*\*'
          if grep -qE "$star_re" <<< "$content"; then
              warning "Found unescaped * inside {{...}} monospace block — renders as bold mid-token. Escape as \\* (e.g. {{jira-\\*backup-\\*}})."
              echo "   Lines with issue:"
              grep -nE "$star_re" <<< "$content" | head -3
          fi
      
          # Check for dash pairs Jira renders as a strikethrough span (`-text-`).
          #
          # This is the same grammar as find_strikethrough_spans() in
          # skills/jira-communication/scripts/lib/markup.py, and both are measured
          # against a live Jira Server 9.12 renderer rather than reasoned about:
          # tests/test_validator_parity.py runs BOTH implementations over the
          # single-line cases in tests/fixtures/strikethrough_oracle.json and asserts
          # they agree. It does NOT cover multi-line input, tables or headings, so
          # block handling is pinned separately in tests/test_strikethrough.py, which
          # also holds the Python side against the recorded corpus.
          #
          #   opener  an unescaped `-` at line start or after a non-word character
          #           (which includes the `}}`, `*`, `_`, `]`, `!` that end an inline
          #           element — that is why {{mono}}-Extensions opens a span), and
          #           followed by neither whitespace nor another dash
          #   closer  the first LATER unescaped `-` that is not preceded by
          #           whitespace and is followed by a non-word character or end of
          #           line; dashes that fail those conditions are skipped over. A
          #           dash that leads a word can never close, which is why
          #           `journalctl -b -p crit` alone is not a span — but the same
          #           flags followed by `zu-` later on the line ARE one.
          #
          # Dashes inside {code}/{noformat} render literally, so those lines are
          # skipped via open/close toggling ({quote}/{panel} are NOT skipped — Jira
          # parses text effects inside them). Markdown ``` fences are skipped the
          # same way: their own check above already errors on them.
          #
          # Links, bare URLs and !images! are resolved into a single element before
          # text effects run, so a dash inside them is inert — the `/-/` in a GitLab
          # merge-request URL must not be escaped, or the link breaks. They are
          # masked out before the scan. {{monospace}} is NOT masked: text effects do
          # apply inside it.
          #
          # Word class: NOT space and NOT punct, which is locale-independent. Two
          # dead ends, both measured with gawk 5.2.1: [[:alnum:]] is locale-dependent
          # (ASCII-only under LC_ALL=C), and an explicit high-byte range
          # [\200-\377] is rejected outright in a multibyte locale ("Invalid
          # collation character"), killing the rule silently. Negating space and
          # punct agrees in both for ASCII and for non-ASCII LETTERS: a high byte is
          # neither space nor punct under C, and is alnum under UTF-8. It also
          # matches Jira, where `_` is NOT a word character.
          #
          # The one place the two locales genuinely disagree is non-ASCII
          # PUNCTUATION: under LC_ALL=C a typographic quote is a run of high bytes
          # and therefore reads as a word character, so `Der Wert "-x- ist falsch`
          # (German quotes) would be missed. tests/test_validator_parity.py caught
          # exactly that on five corpus cases. They are folded down to an ASCII
          # quote first, which behaves identically in both locales - gsub on a
          # literal UTF-8 string matches bytes under C and characters under UTF-8.
          # Run the scan under a UTF-8 locale when the machine has one. The word class
          # below is locale-independent for ASCII and for non-ASCII LETTERS, but not
          # for arbitrary non-ASCII PUNCTUATION: under LC_ALL=C a `§`, `€`, `°` or `±`
          # is a run of high bytes and reads as a word character, so the opener
          # boundary is lost and a real span is missed. fold() handles the handful of
          # codepoints that show up in prose; the locale handles the rest. On a
          # C-only machine the fold is what is left, and the gap is the unfolded
          # symbols - tests/test_validator_parity.py runs both locales to keep that
          # visible rather than silent.
          # JIRA_SYNTAX_SCAN_LOCALE overrides the choice, so the C-only fallback can
          # be exercised on a machine that does have UTF-8 (the parity test does that).
          local scan_locale="${JIRA_SYNTAX_SCAN_LOCALE:-}"
          if [ -z "$scan_locale" ]; then
              scan_locale=$(locale -a 2>/dev/null | grep -iE '\.(utf-?8)$' | head -1)
          fi
          : "${scan_locale:=C}"
      
          local dash_hits
          # AWK-DASH-SCAN-BEGIN (marker: tests/test_validator_parity.py extracts the
          # program between these two markers and runs it over the whole corpus in
          # one pass. Keep them.)
          dash_hits=$(LC_ALL="$scan_locale" awk '
              function fold(line) {
                  # NBSP becomes \002, a sentinel that is neither space nor word.
                  # It has to be BOTH at once, because Jira treats U+00A0 as two
                  # different things: an ordinary character for the dash delimiters
                  # (`a -x<NBSP>- y` IS struck through, measured) and a terminator
                  # for the URL autolinker (`https://h/a<NBSP>/-/b` links only
                  # `https://h/a`). Python gets this from `\s` matching U+00A0 while
                  # _is_delimiter_space does not; here \002 is excluded from the
                  # region classes below and from isword(). Folding it to a space
                  # instead would break the delimiters, and folding it to punctuation
                  # - which this did - let a region swallow the dashes after it.
                  gsub(/\xc2\xa0/, "\002", line)
                  gsub(/\xe2\x80\x9e|\xe2\x80\x9c|\xe2\x80\x9d|\xe2\x80\x98|\xe2\x80\x99|\xe2\x80\x9a/, "\"", line)
                  gsub(/\xc2\xab|\xc2\xbb|\xe2\x80\xb9|\xe2\x80\xba/, "\"", line)
                  gsub(/\xe2\x80\x93|\xe2\x80\x94|\xe2\x80\x90|\xe2\x80\xa6|\xe2\x80\xa2/, "\"", line)
                  # Common non-ASCII symbols. Not exhaustive and cannot be - this is
                  # the C-locale fallback only; where a UTF-8 locale exists the
                  # scan runs under it and gawk classifies these itself.
                  gsub(/\xc2\xa7|\xe2\x82\xac|\xc2\xb0|\xc2\xb1|\xc2\xb5|\xc3\x97|\xc3\xb7/, "\"", line)
                  gsub(/\xc2\xa9|\xc2\xae|\xe2\x84\xa2|\xe2\x80\xa0|\xe2\x80\xa1|\xe2\x80\xb0/, "\"", line)
                  return line
              }
              # The \001 test comes first: a masked region must read as a boundary, and a
              # control character is neither space nor punct, so it would otherwise
              # count as a word character and swallow the opener after a link.
              function isword(c) { return (c != "" && c != "\001" && c != "\002" && c !~ /[[:space:][:punct:]]/) }
              # Replace protected regions with \001, preserving length. Mirrors
              # _mask_protected() in lib/markup.py: a bare URL, mailto or image needs
              # an ASCII non-alphanumeric before it (one glued to a word is not
              # autolinked, so it keeps its dashes live) while a square-bracketed
              # link resolves anywhere, and a region starting exactly where the
              # previous one ended does not resolve either.
              function mask(line,    out, pos, rest, start, len_, before, kind, i, prev_end) {
                  out = ""
                  pos = 1
                  prev_end = 0
                  while (pos <= length(line)) {
                      rest = substr(line, pos)
                      if (!match(rest, /(\[[^]\n]*\])|((https?|ftp):\/\/[^][:space:]{}|\002]+)|(mailto:[^][:space:]{}|\002]+)|(![^[:space:]!\002]+!)/)) {
                          out = out rest
                          break
                      }
                      start = pos + RSTART - 1
                      len_ = RLENGTH
                      before = (start > 1) ? substr(line, start - 1, 1) : ""
                      kind = substr(line, start, 1)
      
                      # A backslash-escaped bracket or bang is not a macro, so its
                      # content is ordinary prose and its dashes stay live. Same for a
                      # region that does not sit at a boundary: a bare URL, mailto or
                      # image needs a non-alphanumeric before it (and a URL must not
                      # follow a pipe), while a square-bracketed link resolves
                      # anywhere. In both cases the region is not a region, so the
                      # scan resumes one character in - which is what the Python
                      # regex does by simply not matching there.
                      if (before == "\\" || (kind != "[" && (before ~ /[A-Za-z0-9]/ || (kind != "!" && (before == "|" || before == "!"))))) {
                          out = out substr(line, pos, RSTART)
                          pos = start + 1
                          continue
                      }
      
                      # Two regions written back to back do not both resolve: Jira
                      # renders the first and leaves the second literal. The SECOND
                      # one is skipped whole, not one character at a time - resuming
                      # inside it would let a shorter region nested within it be
                      # masked, which loses the dashes around it. That was a real
                      # miss: `!i.png!https://h/a/-/b[t|...]` comes back struck and
                      # the scan reported nothing. prev_end deliberately does not
                      # advance here, so a third region is masked again, matching
                      # _mask_protected() in lib/markup.py.
                      if (start == prev_end) {
                          out = out substr(line, pos, RSTART - 1 + len_)
                          pos = start + len_
                          continue
                      }
      
                      out = out substr(line, pos, RSTART - 1)
                      for (i = 0; i < len_; i++) out = out "\001"
                      pos = start + len_
                      prev_end = pos
                  }
                  return out
              }
              function strikes(raw,    line, n, i, j, prev, nxt, after) {
                  line = mask(raw)
                  n = length(line)
                  for (i = 1; i <= n; i++) {
                      if (substr(line, i, 1) != "-") continue
                      if (i > 1 && substr(line, i - 1, 1) == "\\") continue
                      prev = (i > 1) ? substr(line, i - 1, 1) : ""
                      if (isword(prev)) continue
                      nxt = (i < n) ? substr(line, i + 1, 1) : ""
                      if (nxt == "" || nxt ~ /[[:space:]]/ || nxt == "-") continue
                      for (j = i + 2; j <= n; j++) {
                          if (substr(line, j, 1) != "-") continue
                          if (substr(line, j - 1, 1) == "\\") continue
                          if (substr(line, j - 1, 1) ~ /[[:space:]]/) continue
                          after = (j < n) ? substr(line, j + 1, 1) : ""
                          if (isword(after)) continue
                          return 1
                      }
                  }
                  return 0
              }
              # Only the SAME tag closes a block, exactly as the Jira renderer does
              # and as _split_verbatim() in lib/markup.py does. Toggling on either tag
              # loses track after `{noformat}` / `{code}` / `{noformat}`, and then
              # misses every span in the rest of the file.
              /^[[:space:]]*\{(code|noformat)(:[^}]*)?\}[[:space:]]*$/ {
                  tag = $0
                  sub(/^[[:space:]]*\{/, "", tag)
                  sub(/[:}].*$/, "", tag)
                  if (opentag == "") opentag = tag
                  else if (opentag == tag) opentag = ""
                  next
              }
              /^[[:space:]]*```/ { infence = !infence; next }
              opentag != "" || infence { next }
              strikes(fold($0)) { printf "%d:%s\n", NR, $0 }' <<< "$content") && dash_rc=0 || dash_rc=$?
          # The `&& … || …` is load-bearing under `set -e` (line 6): a bare assignment
          # takes the command substitution's status, so a failing awk aborted the
          # whole script before this branch could run. It exited non-zero, so nothing
          # was silently passed - but the ERROR below never printed and the remaining
          # files in a multi-file run were skipped. A compound command is exempt from
          # set -e, which is what lets the branch execute.
          # awk's status must not be swallowed by the pipe: a scan that never ran
          # (no awk, a syntax error, a killed process) produces no hits, and no hits
          # is what a clean draft looks like. That is the same "a failure reads as
          # clean" shape RenderVerdict.available exists to prevent on the Python
          # side, and it is an ERROR here rather than silence.
          if [ "$dash_rc" -ne 0 ]; then
              error "The dash-strikethrough scan did not run (awk exited $dash_rc) - this draft was NOT checked for spans Jira would render struck through."
              dash_hits=""
          else
              dash_hits=$(printf '%s\n' "$dash_hits" | grep -v '^$' | head -3)
          fi
          # AWK-DASH-SCAN-END
          if [ -n "$dash_hits" ]; then
              warning "Found a dash pair that renders struck through outside a code block — Jira reads a dash after a non-word character (including the {{}}, *, _, ] or ! that ends an inline element) as a strikethrough opener, and a dash before one as the closer. Escape the opener as \\-foo; a backslash-escaped dash still prints as a plain hyphen."
              echo "   Lines with issue:"
              echo "$dash_hits"
          fi
      
          # Check for Markdown-style links ([text](url) instead of [text|url])
          if echo "$content" | grep -qE "\[([^\]]+)\]\(([^)]+)\)"; then
              error "Found Markdown-style links ([text](url)). Use Jira format: [text|url]"
              echo "   Examples found:"
              echo "$content" | grep -oE "\[([^\]]+)\]\(([^)]+)\)" | head -3
          fi
      
          # Check for headings without space after period (h2.Title instead of h2. Title)
          if echo "$content" | grep -qE "^h[1-6]\.[^ ]"; then
              error "Found headings without space after period. Use: h2. Title (not h2.Title)"
              echo "   Lines with issue:"
              echo "$content" | grep -nE "^h[1-6]\.[^ ]" | head -5
          fi
      
          # Check for code blocks without language specification
          # (skip escaped \{code\} literals and inline-monospace {{code}})
          if grep -qE '(^|[^\\{])\{code\}[^{]' <<< "$content"; then
              warning "Found {code} blocks without language. Consider: {code:java} for syntax highlighting"
          fi
      
          # Check for {code:LANG} using a language Jira Server's formatter does not recognize.
          # Authoritative list from the server error message ("Available languages are: ...").
          # Anything outside this set causes "Unable to find source-code formatter for language: X".
          # Use Bash built-in pattern matching with literal-quoted needle so identifiers
          # containing shell-significant characters (c#, c++) are compared verbatim.
          local valid_langs="actionscript ada applescript bash c c# c++ cpp css erlang go groovy haskell html java javascript js json lua none nyan objc perl php python r rainbow ruby scala sh sql swift visualbasic xml yaml"
          local search_langs=" $valid_langs "
          while IFS= read -r lang; do
              [ -z "$lang" ] && continue
              # Templates ship `{code:language}` as a fill-in placeholder; warn rather than
              # error so templates stay validatable until users substitute a real lang.
              if [ "$lang" = "language" ]; then
                  warning "Found {code:language} placeholder — replace with an actual language before submitting to Jira"
                  continue
              fi
              if [[ "$search_langs" != *" $lang "* ]]; then
                  # Suggest the closest-fit valid language for common stumbles before
                  # falling back to the generic "use {code:none} or ..." message.
                  # Lower-case the identifier so `Dockerfile`, `Makefile` etc. match
                  # without per-variant case entries.
                  local hint=""
                  case "${lang,,}" in
                      hcl|tf|terraform|tofu)               hint="{code:none} for HCL / Terraform / OpenTofu" ;;
                      dockerfile|containerfile)            hint="{code:bash} (Dockerfile RUN lines lex acceptably as bash) or {code:none}" ;;
                      rust|rs)                             hint="{code:none} for Rust" ;;
                      kotlin|kt)                           hint="{code:java} (Kotlin lexes acceptably as Java) or {code:none}" ;;
                      typescript|ts|tsx)                   hint="{code:javascript} or {code:none}" ;;
                      shell|zsh|fish|console)              hint="{code:bash} or {code:none}" ;;
                      powershell|ps1|pwsh)                 hint="{code:none} for PowerShell" ;;
                      make|makefile)                       hint="{code:none} for Makefile" ;;
                      ini|toml|conf|properties)            hint="{code:none} for INI / TOML / config" ;;
                      diff|patch)                          hint="{code:none}" ;;
                      go-template|gotmpl|jinja|jinja2)     hint="{code:none}" ;;
                  esac
                  if [ -n "$hint" ]; then
                      error "Unsupported {code:$lang} language. Jira Server rejects this; use $hint"
                  else
                      error "Unsupported {code:$lang} language. Jira Server rejects this; use {code:none} or one of: $valid_langs"
                  fi
              fi
          done < <(grep -oE '(^|[^\\{])\{code:[^}|\\]+' <<< "$content" | sed 's/.*{code://' | sort -u)
      
          # Check for tables with incorrect header syntax (|Header| instead of ||Header||)
          if echo "$content" | grep -qE "^\|[^|]+\|$" && ! echo "$content" | grep -qE "^\|\|"; then
              warning "Potential table header without double pipes. Headers should use: ||Header||"
          fi
      
          # Check for unclosed {code} blocks
          # Jira wiki markup uses {code} as both the opening and closing tag, so a
          # correctly paired block always produces an even occurrence count.
          # Use `grep -o ... | wc -l` to count each occurrence (not just matching
          # lines), matching the {color} check below for consistency and to catch
          # multiple tags on the same line.
          # `(^|[^\\{])` skips escaped literals (\{code\}) and inline-monospace
          # lookalikes ({{code}}) — both are prose, not block markup.
          local code_count
          code_count=$(grep -oE '(^|[^\\{])\{code[}:]' <<< "$content" | wc -l)
          if [ $((code_count % 2)) -ne 0 ]; then
              error "Mismatched {code} tags: odd number ($code_count) of occurrences (expected pairs)"
          fi
      
          # Check for unclosed {panel} blocks
          # Same rule applies: {panel} opens and closes the block.
          local panel_count
          panel_count=$(grep -oE '(^|[^\\{])\{panel[}:]' <<< "$content" | wc -l)
          if [ $((panel_count % 2)) -ne 0 ]; then
              error "Mismatched {panel} tags: odd number ($panel_count) of occurrences (expected pairs)"
          fi
      
          # Check for unclosed {color} blocks
          local color_count
          color_count=$(echo "$content" | grep -o "{color" | wc -l)
          if [ $((color_count % 2)) -ne 0 ]; then
              warning "Potential unclosed {color} tag (odd number of occurrences)"
          fi
      
          # Check for unclosed {noformat}, {quote}, {anchor} blocks
          # Same single-token open/close rule as {code}, {panel}, {color}: an odd
          # occurrence count signals an unescaped literal in prose or a missing close.
          for macro in noformat quote anchor; do
              local mcount
              mcount=$(grep -oE "(^|[^\\\\{])\{${macro}[}:]" <<< "$content" | wc -l)
              if [ $((mcount % 2)) -ne 0 ]; then
                  warning "Potential unclosed {${macro}} tag (odd number of occurrences)"
              fi
          done
      
          # Check for block-markup tags used inline. {code}, {noformat}, {quote} and
          # {panel} are block-level macros: the tag must stand alone on its own line.
          # An unescaped tag with other text on the same line opens the block
          # mid-prose and swallows the rest of the line (classic case: writing
          # *about* {code} in a sentence). Escape literal mentions as \{code\}.
          # Escaped tags (\{code\}) and {{monospace}} lookalikes ({{code}}) are
          # stripped per line BEFORE testing — a line-level exclusion would hide a
          # genuine unescaped tag sharing a line with an escaped/monospace mention.
          local inline_hits
          inline_hits=$(awk '
              {
                  line = $0
                  gsub(/\\\{(code|noformat|quote|panel)[^}]*\\\}/, "", line)
                  gsub(/\{\{(code|noformat|quote|panel)(:[^}]*)?\}\}/, "", line)
                  if (line ~ /\{(code|noformat|quote|panel)(:[^}]*)?\}/ &&
                      line !~ /^[[:space:]]*\{(code|noformat|quote|panel)(:[^}]*)?\}[[:space:]]*$/)
                      printf "%d:%s\n", NR, $0
              }' <<< "$content" | head -3)
          if [ -n "$inline_hits" ]; then
              error "Block tag used inline — {code}/{noformat}/{quote}/{panel} must stand alone on their own line; escape literal mentions as \\{code\\}"
              echo "   Lines with issue:"
              echo "$inline_hits"
          fi
      
          # Check for Markdown-style lists (- item instead of * item)
          if echo "$content" | grep -qE "^- [^-]"; then
              warning "Found Markdown-style bullets (- item). Jira prefers: * item"
          fi
      
          # Positive checks
          if echo "$content" | grep -qE "^h[1-6]\. "; then
              success "Found correctly formatted Jira headings"
          fi
      
          if echo "$content" | grep -qE "{code:[a-z]+}"; then
              success "Found code blocks with language specification"
          fi
      
          if echo "$content" | grep -qE "\[~[a-z.]+\]"; then
              success "Found user mentions ([~username])"
          fi
      
          if echo "$content" | grep -qE "\[[A-Z]+-[0-9]+\]"; then
              success "Found issue links ([PROJ-123])"
          fi
      }
      
      # Main script
      echo "Jira Wiki Markup Syntax Validator"
      echo "=================================="
      
      if [ $# -eq 0 ]; then
          echo "Usage: $0 <file1> [file2] [file3] ..."
          echo ""
          echo "Validates Jira wiki markup syntax in text files"
          echo ""
          echo "Example:"
          echo "  $0 issue-description.txt"
          echo "  $0 templates/*.md"
          exit 1
      fi
      
      # Validate each file
      for file in "$@"; do
          validate_file "$file"
      done
      
      # Summary
      echo ""
      echo "=========================================="
      echo "Validation Summary"
      echo "=========================================="
      echo "Files checked: $#"
      echo -e "${RED}Errors: $ERRORS${NC}"
      echo -e "${YELLOW}Warnings: $WARNINGS${NC}"
      
      if [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then
          echo -e "${GREEN}✅ All checks passed!${NC}"
          exit 0
      elif [ $ERRORS -eq 0 ]; then
          echo -e "${YELLOW}⚠️  No errors, but $WARNINGS warnings found${NC}"
          exit 0
      else
          echo -e "${RED}❌ $ERRORS errors found - please fix before submitting to Jira${NC}"
          exit 1
      fi
      
  • templates
    • bug-report-template.md 4 KB
      # Jira Bug Report Template
      
      Use this template when creating bug reports in Jira with proper wiki markup syntax.
      
      ## Template
      
      ```
      h2. Bug Description
      
      [Provide a clear, concise description of the bug]
      
      h3. Environment
      * *Browser:* Chrome 120.0
      * *OS:* Windows 11
      * *Version:* 2.1.0
      * *Environment:* Production
      
      h3. Steps to Reproduce
      # Navigate to [specific page/feature]
      # Perform [specific action]
      # Observe [unexpected behavior]
      
      h3. Expected Behavior
      [Describe what should happen]
      
      h3. Actual Behavior
      [Describe what actually happens]
      
      {panel:title=Error Message|bgColor=#FFEBE9}
      {code:java}
      [Paste error message or stack trace here]
      {code}
      {panel}
      
      h3. Additional Context
      * Frequency: [Always/Sometimes/Rare]
      * User Impact: [Critical/High/Medium/Low]
      * Workaround Available: [Yes/No]
      
      h3. Screenshots
      [^screenshot1.png]
      [^screenshot2.png]
      
      h3. Related Issues
      * Blocks [PROJ-XXX]
      * Related to [PROJ-YYY]
      
      h3. Technical Notes
      {code:javascript}
      // Code snippet showing the issue
      function problematicCode() {
          // Details here
      }
      {code}
      
      ---
      *Reported by:* [~username]
      *Date:* YYYY-MM-DD
      ```
      
      ## Example - Filled Template
      
      ```
      h2. Bug Description
      
      Login button becomes unresponsive after failed authentication attempt, requiring page refresh to retry.
      
      h3. Environment
      * *Browser:* Chrome 120.0.6099.109
      * *OS:* Windows 11
      * *Version:* 2.3.1
      * *Environment:* Production
      
      h3. Steps to Reproduce
      # Navigate to {{/login}} page
      # Enter invalid credentials
      # Click *Login* button
      # Observe error message
      # Try to enter correct credentials
      # Click *Login* button again
      # Button remains disabled
      
      h3. Expected Behavior
      After a failed login attempt, the login button should become active again, allowing users to retry with different credentials.
      
      h3. Actual Behavior
      The login button remains in a disabled state after the first failed attempt. Users must refresh the page to attempt login again.
      
      {panel:title=Error in Browser Console|bgColor=#FFEBE9}
      {code:javascript}
      TypeError: Cannot read property 'reset' of null
          at LoginForm.handleSubmit (login.js:45)
          at onClick (login.js:23)
      {code}
      {panel}
      
      h3. Additional Context
      * Frequency: Always (100% reproduction rate)
      * User Impact: High (blocks login functionality)
      * Workaround Available: Yes (page refresh)
      * Affects ~1000 daily users based on error logs
      
      h3. Screenshots
      [^login-disabled-state.png] - Login button stuck in disabled state
      [^console-error.png] - Browser console showing error
      
      h3. Related Issues
      * Blocks [PROJ-234] - User authentication improvements
      * Related to [PROJ-189] - Form validation refactoring
      
      h3. Technical Notes
      {code:javascript}
      // Problem in LoginForm component
      handleSubmit(event) {
          event.preventDefault();
          this.setState({ isSubmitting: true });
      
          // Error occurs here if form ref is null
          this.formRef.reset();  // BUG: formRef can be null
      
          // Rest of submission logic
      }
      {code}
      
      Suggested fix: Add null check before calling {{reset()}} method.
      
      ---
      *Reported by:* [~john.smith]
      *Date:* 2025-11-06
      ```
      
      ## Usage with jira-communication Skill
      
      ```bash
      # Create the bug report using the script
      uv run scripts/workflow/jira-create.py issue PROJ \
        "Login button unresponsive after failed authentication" \
        --type Bug \
        --priority High \
        --labels frontend,authentication,ux \
        --description-file bug-description.txt
      
      # Or with inline description (short version)
      uv run scripts/workflow/jira-create.py issue PROJ \
        "Login button unresponsive after failed authentication" \
        --type Bug \
        --priority High
      ```
      
      ## Checklist Before Submitting
      
      - [ ] h2. heading for main Description section
      - [ ] h3. headings for subsections
      - [ ] Numbered list (#) for Steps to Reproduce
      - [ ] Bulleted list (*) for Environment details
      - [ ] \{panel\} for error messages with appropriate bgColor
      - [ ] \{code:language\} for code snippets with correct language
      - [ ] [^filename] format for attachment references
      - [ ] [PROJ-XXX] format for issue links
      - [ ] [~username] format for user mentions
      - [ ] *bold* for emphasis on key terms
      - [ ] {{monospace}} for UI elements and paths
      
    • feature-request-template.md 7.9 KB
      # Jira Feature Request Template
      
      Use this template when creating feature requests in Jira with proper wiki markup syntax.
      
      ## Template
      
      ```
      h2. Feature Overview
      
      [Provide a brief summary of the feature]
      
      h3. Business Value
      * *User Impact:* [Describe who benefits and how]
      * *Business Goal:* [Align with strategic objectives]
      * *Priority Justification:* [Why this should be prioritized]
      
      h3. User Stories
      
      h4. As a [user type]
      * I want to [action]
      * So that [benefit]
      
      h4. As a [another user type]
      * I want to [action]
      * So that [benefit]
      
      h3. Acceptance Criteria
      # [Specific, testable criterion]
      # [Specific, testable criterion]
      # [Specific, testable criterion]
      
      h3. Functional Requirements
      
      h4. Must Have
      * Requirement 1
      * Requirement 2
      
      h4. Should Have
      * Requirement 3
      * Requirement 4
      
      h4. Could Have
      * Requirement 5
      * Requirement 6
      
      h3. Non-Functional Requirements
      * *Performance:* [Response time requirements]
      * *Security:* [Security considerations]
      * *Scalability:* [Scale requirements]
      * *Accessibility:* [WCAG compliance level]
      
      h3. Technical Considerations
      {code:language}
      // Pseudocode or technical notes
      {code}
      
      h3. UI/UX Mockups
      [^wireframe-01.png] - Main interface mockup
      [^user-flow.png] - User journey diagram
      
      h3. Dependencies
      * Requires [PROJ-XXX] to be completed first
      * Impacts [PROJ-YYY] - needs coordination
      
      h3. Open Questions
      ? Question 1 - needs clarification
      ? Question 2 - requires decision
      
      h3. Success Metrics
      ||Metric||Target||Measurement Method||
      |User Adoption|80% of active users|Analytics tracking|
      |Performance|< 200ms response|Performance monitoring|
      |Satisfaction|4.5/5 rating|User surveys|
      
      ---
      *Requested by:* [~username]
      *Stakeholders:* [~pm], [~designer], [~engineer]
      ```
      
      ## Example - Filled Template
      
      ```
      h2. Feature Overview
      
      Implement bulk export functionality allowing users to export multiple projects to various formats (CSV, JSON, Excel) with customizable field selection.
      
      h3. Business Value
      * *User Impact:* 500+ power users currently export data manually one project at a time (30+ clicks per export)
      * *Business Goal:* Reduce data export time by 90%, improving productivity and user satisfaction
      * *Priority Justification:* #1 requested feature in Q4 2024 user survey (78% of respondents), competitive gap vs competitors
      
      h3. User Stories
      
      h4. As a Project Manager
      * I want to export multiple projects at once with selected fields
      * So that I can create consolidated reports without manual data entry
      
      h4. As a Data Analyst
      * I want to export historical project data in machine-readable formats
      * So that I can perform advanced analytics in external tools
      
      h4. As a Team Lead
      * I want to schedule automated exports to run daily
      * So that stakeholders receive up-to-date reports without manual intervention
      
      h3. Acceptance Criteria
      # User can select 1-100 projects for bulk export from project list
      # Export supports CSV, JSON, and Excel formats
      # User can customize which fields to include in export
      # Export progress is shown with cancel option
      # Completed exports are downloadable from exports history page
      # Export file size limit is 50MB with pagination for larger datasets
      # Exports are available for 30 days before auto-deletion
      
      h3. Functional Requirements
      
      h4. Must Have
      * Multi-select checkbox interface for project selection
      * Format selector (CSV, JSON, Excel)
      * Field customization with drag-and-drop ordering
      * Progress indicator during export generation
      * Download link with expiration notice
      * Export history page showing last 10 exports
      
      h4. Should Have
      * Search and filter for project selection
      * Save field configurations as templates
      * Email notification when export completes
      * Preview sample data before full export
      
      h4. Could Have
      * Schedule recurring exports
      * Share export links with team members
      * Export directly to cloud storage (Google Drive, Dropbox)
      * Advanced filtering within export data
      
      h3. Non-Functional Requirements
      * *Performance:* Export generation completes within 30 seconds for 50 projects
      * *Security:* Exports encrypted at rest, only accessible to authorized users with audit trail
      * *Scalability:* Support 1000 concurrent export requests
      * *Accessibility:* WCAG 2.1 AA compliance for export interface
      * *Browser Support:* Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
      
      h3. Technical Considerations
      {code:python}
      # Proposed export architecture
      class BulkExporter:
          def export_projects(self, project_ids, format, fields):
              # Use async task queue for processing
              task = ExportTask.create(
                  projects=project_ids,
                  format=format,
                  fields=fields,
                  user=current_user
              )
      
              # Process in background
              celery.send_task('exports.process_bulk', args=[task.id])
      
              return task.id
      
          def get_export_status(self, task_id):
              # Return progress percentage and download link when complete
              return ExportTask.get(task_id).status
      {code}
      
      *Database Impact:* New {{exports}} table for tracking, ~1GB storage for 30-day retention
      *API Endpoints:*
      * {{POST /api/exports}} - Initiate export
      * {{GET /api/exports/:id}} - Check status
      * {{GET /api/exports/:id/download}} - Download file
      
      h3. UI/UX Mockups
      [^bulk-export-interface.png] - Main export dialog with project selection
      [^field-customization.png] - Field selector with drag-and-drop ordering
      [^export-progress.png] - Progress indicator during generation
      [^export-history.png] - Export history page design
      
      h3. Dependencies
      * Requires [PROJ-456] - Background task queue infrastructure
      * Impacts [PROJ-789] - Storage quota system (needs capacity planning)
      * Coordinates with [PROJ-321] - API rate limiting (exports count as API calls)
      
      h3. Open Questions
      ? Should exports include archived projects or only active ones?
      ? What permission level is required to export data? (View vs Export permission)
      ? Should we support incremental exports (only new/changed data)?
      ? How to handle very large exports exceeding 50MB limit?
      
      h3. Success Metrics
      ||Metric||Target||Measurement Method||
      |User Adoption|60% of power users within 3 months|Analytics event tracking|
      |Time Savings|90% reduction in export time|User session timing comparison|
      |User Satisfaction|4.5/5 feature rating|In-app feedback survey|
      |Export Volume|5000+ exports per month|Export usage dashboard|
      |Error Rate|< 1% failed exports|Error monitoring logs|
      
      {panel:title=Launch Plan|bgColor=#DEEBFF}
      h4. Phase 1 - Beta (Week 1-2)
      * Release to 50 beta users
      * Gather feedback and fix critical bugs
      
      h4. Phase 2 - Limited Release (Week 3-4)
      * Release to 20% of user base
      * Monitor performance and error rates
      
      h4. Phase 3 - Full Release (Week 5+)
      * Release to all users
      * Announce via email and in-app notifications
      {panel}
      
      ---
      *Requested by:* [~sarah.johnson]
      *Stakeholders:* [~product.manager], [~ux.designer], [~backend.lead], [~frontend.lead]
      *Estimated Effort:* 3 sprints (6 weeks)
      *Target Release:* Q1 2025
      ```
      
      ## Usage with jira-communication Skill
      
      ```bash
      # Create the feature request using the script
      uv run scripts/workflow/jira-create.py issue PROJ \
        "Bulk export functionality for multiple projects" \
        --type Story \
        --priority High \
        --labels feature-request,export,productivity \
        --description-file feature-description.txt
      
      # Or with inline description (short version)
      uv run scripts/workflow/jira-create.py issue PROJ \
        "Bulk export functionality for multiple projects" \
        --type Story \
        --priority High
      ```
      
      ## Checklist Before Submitting
      
      - [ ] h2. heading for Feature Overview
      - [ ] h3. headings for all major sections
      - [ ] h4. headings for User Stories and subsections
      - [ ] Bulleted lists (*) for requirements and criteria
      - [ ] Numbered lists (#) for Acceptance Criteria
      - [ ] Tables (||header|| syntax) for Success Metrics
      - [ ] \{code:language\} blocks for technical details
      - [ ] \{panel\} for important launch/timeline information
      - [ ] [^filename] for mockup/wireframe references
      - [ ] [PROJ-XXX] format for dependency links
      - [ ] [~username] format for stakeholder mentions
      - [ ] *bold* for emphasis on metrics and targets
      - [ ] {{monospace}} for technical terms and paths
      - [ ] ? prefix for Open Questions
      
  • AGENTS.md 1.8 KB
    <!-- Managed by agent: keep sections & order; edit content, not structure. Last updated: 2025-12-12 -->
    
    # AGENTS.md — jira-syntax
    
    Development guide for maintaining templates, references, and validation scripts.
    
    ## Overview
    
    Static content skill: templates, reference docs, and a validation script. No runtime dependencies.
    
    ## Setup & environment
    
    No special setup. Files are plain text/markdown/shell.
    
    ## Build & tests
    
    ```bash
    # Test validation script
    scripts/validate-jira-syntax.sh templates/bug-report-template.md
    ```
    
    ## Code style & conventions
    
    **Templates (`templates/`):**
    - Use actual Jira wiki markup, never Markdown
    - Include section comments explaining purpose
    - Test in real Jira before committing
    
    **References (`references/`):**
    - Keep `jira-syntax-quick-reference.md` as single source of truth
    - Examples must be copy-pasteable into Jira
    
    **Validation script:**
    - Bash with grep patterns
    - Should catch common Markdown mistakes
    
    ## Security & safety
    
    - No executable code in templates
    - No sensitive data in examples
    
    ## PR/commit checklist
    
    - [ ] All examples use valid Jira wiki markup
    - [ ] Tested in actual Jira instance
    - [ ] Updated quick-reference if adding new syntax
    - [ ] Validation script catches the patterns
    
    ## Good vs. bad examples
    
    **Template changes:**
    ```
    # ✓ Test paste into Jira before commit
    # ✓ Update quick-reference if new syntax
    
    # ✗ Assume Markdown works in Jira
    # ✗ Add syntax without testing
    ```
    
    ## When stuck
    
    - Paste content into real Jira to verify rendering
    - Check official docs: https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa
    
    ## House rules
    
    - SKILL.md has the user-facing syntax reference
    - This file is for maintaining the skill itself
    
    ---
    
    **Maintaining this file:** See root `AGENTS.md` for convention reference.
    
  • SKILL.md 3.9 KB
    ---
    name: jira-syntax
    description: "Use when writing or formatting Jira descriptions, comments, or any text destined for Jira. Converts Markdown to Jira wiki markup, provides templates (bug reports, feature requests), and validates syntax before submission. Trigger on any Jira content authoring task."
    license: "(MIT AND CC-BY-SA-4.0). See LICENSE-MIT and LICENSE-CC-BY-SA-4.0"
    metadata:
      author: Netresearch DTT GmbH
      version: "3.32.2"
      repository: https://github.com/netresearch/jira-skill
    ---
    
    # Jira Syntax
    
    Jira wiki markup syntax, templates, and validation. For API operations, use the **jira-communication** skill.
    
    ## Quick Syntax Reference
    
    | Jira Syntax | Purpose | NOT this (Markdown) |
    |-------------|---------|---------------------|
    | `h2. Title` | Heading | `## Title` |
    | `*bold*` | Bold | `**bold**` |
    | `_italic_` | Italic | `*italic*` |
    | `{{code}}` | Inline code | `` `code` `` |
    | `{code:java}...{code}` | Code block | ``` ```java ``` |
    | `[text\|url]` | Link | `[text](url)` |
    | `[PROJ-123]` | Issue link | - |
    | `[~username]` | User mention | `@username` |
    | `* item` | Bullet list | `- item` |
    | `# item` | Numbered list | `1. item` |
    | `\|\|Header\|\|` | Table header | `\|Header\|` |
    
    See `references/jira-syntax-quick-reference.md` for complete syntax documentation.
    
    ## Available Templates
    
    ### Bug Report
    **Path**: `templates/bug-report-template.md`
    
    Sections: Environment, Steps to Reproduce, Expected/Actual Behavior, Error Messages, Technical Notes
    
    ### Feature Request
    **Path**: `templates/feature-request-template.md`
    
    Sections: Overview, User Stories, Acceptance Criteria, Technical Approach, Success Metrics
    
    ## Syntax Validation
    
    Run before submitting to Jira:
    ```bash
    ${CLAUDE_SKILL_DIR}/scripts/validate-jira-syntax.sh path/to/content.txt
    ```
    
    **It gates the post** — run it as its own step, never chained with the
    posting command.
    
    ### Validation Checklist
    - [ ] Headings: `h2. Title` (space after period)
    - [ ] Bold: `*text*` (single asterisk)
    - [ ] Code blocks: `{code:language}...{code}`
    - [ ] Lists: `*` for bullets, `#` for numbers
    - [ ] Links: `[label|url]` or `[PROJ-123]`
    - [ ] Tables: `||Header||` and `|Cell|`
    - [ ] Colors: `{color:red}text{color}`
    - [ ] Panels: `{panel:title=X}...{panel}`
    
    ### Common Mistakes
    
    | ❌ Wrong | ✅ Correct |
    |---------|-----------|
    | `## Heading` | `h2. Heading` |
    | `**bold**` | `*bold*` |
    | `` `code` `` | `{{code}}` |
    | `[text](url)` | `[text\|url]` |
    | `- bullet` | `* bullet` |
    | `h2.Title` | `h2. Title` |
    | `MR !42` (bare GitLab ref) | `[MR 42\|url]` or full `group/project!42` — a bare `!…!` is image markup |
    | `(/)` on an open/proposed item | `(x)` — `(/)` renders as a green check (done); use `(x)` for open items |
    | `( )` as a checkbox | `(x)` — `( )` is not a macro and renders literally |
    | `(-)` or `(x)` on a pending step in a review | `(i) pending: …` — `(-)` renders as a red no-entry sign, and in a review `(x)` means a failure |
    
    ## Integration with jira-communication Skill
    
    **Workflow:**
    1. Get template from jira-syntax
    2. Fill content using Jira wiki markup
    3. Validate with `${CLAUDE_SKILL_DIR}/scripts/validate-jira-syntax.sh`
    4. Submit via jira-communication skill
    
    ## References
    
    - `references/jira-syntax-quick-reference.md` - Complete syntax documentation
      Includes **Ask the renderer instead of reasoning about it**: `POST /rest/api/1.0/render` returns the HTML Jira will produce for any markup, before anything is posted. Use it to settle a markup question rather than deriving the answer.
    - `references/cross-project-refs.md` - GitLab cross-project ref convention (`group/project!N`, `group/project#N`, `group/project@tag`) when linking to GitLab from Jira
    - `templates/bug-report-template.md` - Bug report template
    - `templates/feature-request-template.md` - Feature request template
    - `${CLAUDE_SKILL_DIR}/scripts/validate-jira-syntax.sh` - Automated syntax checker
    - [Official Jira Wiki Markup](https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa?section=all)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related