Jira Publish
Publish a Jira draft file to create issues, update descriptions, or add comments.
CRITICAL: Preview Before All Mutations
NEVER execute without showing the user a preview first.
For every publish operation:
- Read the draft file
- Validate frontmatter completeness
- Show preview with exact content
- Ask for approval explicitly
- Only then execute the command
- Update draft file with published status
If the user hasn't approved, DO NOT execute.
Workflow
- Read draft file from provided path (use Read tool — do NOT extract body with shell)
- Parse frontmatter fields from the Read output (the YAML between
---delimiters) - Validate required fields for action type
- Resolve assignee - if
assigneeis an email, look up account ID vialookupJiraAccountId - Resolve mentions - look up any
@emailmentions in body (see Mentions section) - Show preview in box format (including resolved assignee and mentions)
- Ask for explicit approval
- Execute via jira-api.sh with all options (--assignee, --component, --parent)
- Add links if any specified
- Update draft with
status: publishedandpublished_issue - Return issue URL
Input
User provides path to a draft file:
/dataops-assistant:jira-publish .jira-drafts/20250203-143022-openai-instance.md
Preview Format
Create Issue
┌─────────────────────────────────────────────────────────┐
│ PREVIEW: Create Issue │
├─────────────────────────────────────────────────────────┤
│ Project: PE │
│ Type: Story │
│ Summary: Set up dedicated OpenAI instance for CMS │
│ Assignee: john@company.com -> John Smith (5b10ac...) │ # if present
│ Parent: PE-100 │ # if present
│ Components: Backend, API │ # if present
│ │
│ Description (first 500 chars): │
│ ───────────────────────────── │
│ :::context │
│ Forever ago, we set up the initial OpenAI instance... │
│ ::: │
│ │
│ ## What We Have Today │
│ - Single OpenAI instance shared across all... │
│ │
│ Links to add after creation: │
│ - blocks PE-456 │
│ - relates-to MAT-28 │
└─────────────────────────────────────────────────────────┘
Update Issue
┌─────────────────────────────────────────────────────────┐
│ PREVIEW: Update Issue │
├─────────────────────────────────────────────────────────┤
│ Target: PE-1234 │
│ Fix Version: 2025.1.0 │ # if present
│ │
│ New Description (first 500 chars): │
│ ───────────────────────────────── │
│ {status:UPDATE 2025-02-03:purple} │
│ │
│ ## Investigation Findings │
│ Checked Nolio server logs and found... │
└─────────────────────────────────────────────────────────┘
Add Comment
┌─────────────────────────────────────────────────────────┐
│ PREVIEW: Add Comment │
├─────────────────────────────────────────────────────────┤
│ Target: PE-1234 │
│ Fix Version: llm_utils#0.6.4 │ # if present
│ │
│ Comment: │
│ ──────── │
│ What's the expected timeline for getting the log │
│ forwarder config approved? │
│ │
│ We have a dependency on this for Q2 monitoring. │
└─────────────────────────────────────────────────────────┘
Commands
CRITICAL: Always use the bin wrapper. Never call scripts by direct path.
The bin wrapper delegates to the run wrapper which sources DATAOPS_ASSISTANT_ROOT and resolves
paths — without it, scripts cannot find their sibling files (like md-to-adf.js).
# CORRECT — single unambiguous path
JIRA_API="$HOME/.dataops-assistant/bin/jira-api.sh"
# WRONG — do NOT use direct paths to scripts:
# JIRA_API=~/.dataops-assistant/skills/jira-publish/scripts/jira-api.sh ← BROKEN
# JIRA_API=/Users/.../incubating-dataops-assistant-plugin/skills/... ← BROKEN
Create Issue
# Basic create
echo 'markdown content' | bash "$JIRA_API" create <project> "<issue_type>" "<summary>" - --md
# With optional fields (assignee must be account ID, not email)
echo 'markdown content' | bash "$JIRA_API" create <project> "<issue_type>" "<summary>" - --md \
--assignee "<account_id>" \
--parent "<epic_key>" \
--component "Backend" --component "API"
Update Issue
# Update description only
echo 'markdown content' | bash "$JIRA_API" update <issue_key> - --md
# Update description and set fix version
echo 'markdown content' | bash "$JIRA_API" update <issue_key> - --md \
--fix-version "2025.1.0"
Add Comment
# Add comment only
echo 'markdown content' | bash "$JIRA_API" comment <issue_key> - --md
# Add comment AND set fix version (two API calls handled internally)
echo 'markdown content' | bash "$JIRA_API" comment <issue_key> - --md \
--fix-version "llm_utils#0.6.4"
Add Links (after create)
For each link in frontmatter, use the exact type specified:
# Frontmatter format:
links:
- type: blocks # Use "blocks" in command
target: PE-456
- type: is-blocked-by # Use "is-blocked-by" in command
target: PE-789
# Execute for EACH link, using the type FROM the frontmatter:
bash "$JIRA_API" link <created_issue> <type_from_frontmatter> <target_from_frontmatter>
# Examples:
bash "$JIRA_API" link PE-1234 blocks PE-456
bash "$JIRA_API" link PE-1234 is-blocked-by PE-789
CRITICAL: Do NOT default to relates-to. Use exactly what the frontmatter specifies.
Valid link types:
- Standard (all Jira):
blocks,is-blocked-by,relates-to,duplicates,clones,is-cloned-by - Extended (Cloud only):
causes,is-caused-by
Note: The 4 core types (Blocks, Relates, Duplicate, Cloners) work in all Jira versions. The Causes type is a Jira Cloud extended feature. Other Cloud extended types (implements, reviews, product discovery types) and custom link types are not supported.
Validation Before Preview
Create Action
projectmust be presentissue_typemust be valid (Story, Task, Bug, Epic, Sub-task)summarymust be present
Update Action
target_issuemust be present and valid format (PROJ-123)fix_version(optional) - version name must exist in the project
Comment Action
target_issuemust be present and valid format (PROJ-123)fix_version(optional) - version name must exist in the project
If validation fails, show error and stop:
❌ Cannot publish: Missing required field 'project' for create action.
Assignee Resolution
CRITICAL: Email Domain is @mcg.com
The company email domain is @mcg.com, NOT @mcghealth.com. Do not be confused by the Atlassian cloud ID being mcghealth - that's just the subdomain for Atlassian Cloud, not the email domain.
- ✅ Correct:
jack.stouffer@mcg.com - ❌ Wrong:
jack.stouffer@mcghealth.com
If assignee is present in frontmatter and looks like an email (contains @), resolve it to an account ID:
1. Call: mcp__atlassian__lookupJiraAccountId(query: "user@company.com")
2. Extract accountId from result
3. If no result found, abort:
❌ Cannot publish: Could not resolve assignee "user@company.com"
Check spelling or use valid Atlassian email address.
4. Store resolved ID to pass to jira-api.sh --assignee
Preview with Assignee
┌─────────────────────────────────────────────────────────┐
│ PREVIEW: Create Issue │
├─────────────────────────────────────────────────────────┤
│ Project: PE │
│ Type: Story │
│ Summary: Set up dedicated OpenAI instance │
│ Assignee: user@company.com -> John Smith (5b10ac8d...) │
│ Parent: PE-100 │
│ Components: Backend, API │
Mentions (@user)
Remember: Email domain is @mcg.com (not @mcghealth.com)
The markdown converter supports @email@domain.com syntax for mentioning users. These are converted to placeholder mentions that must be resolved before publishing.
How It Works
Markdown like
Hey @john.smith@company.com please reviewconverts to ADF with:{ "type": "mention", "attrs": { "id": "__EMAIL__:john.smith@company.com", ... } }Before publishing, scan the body content for
__EMAIL__:patternsFor each email found, call
mcp__atlassian__lookupJiraAccountIdwith the emailIf lookup succeeds, replace
__EMAIL__:emailwith the actual account IDIf ANY lookup fails, abort immediately with error
Resolution Steps
IMPORTANT: Resolution happens AFTER markdown-to-ADF conversion, on the ADF JSON.
1. Scan the Read tool output for @email@domain.com patterns (do NOT use shell to extract body)
2. For each email found:
- Call: mcp__atlassian__lookupJiraAccountId(query: "email@domain.com")
- If result has accountId, store mapping: email -> accountId
- If no result, add to failed list
3. If failed list not empty:
❌ Cannot publish: Could not resolve mentions for:
- john.smith@company.com
- jane.doe@company.com
Check spelling or use valid Atlassian email addresses.
4. Convert whole draft file to ADF: cat "$draft_file" | bun ~/.dataops-assistant/run skills/jira-publish/scripts/md-to-adf.js
(md-to-adf.js strips frontmatter automatically)
5. The ADF will contain: "id": "__EMAIL__:john.smith@company.com"
6. String-replace in ADF JSON: __EMAIL__:email -> resolved_account_id
7. Pass the modified ADF to Jira API (no --md flag)
Practical approach: Since jira-api.sh handles the markdown conversion internally, you have two options:
Option A (recommended): Convert markdown separately, replace in ADF, pass raw ADF:
# Convert whole draft file to ADF (md-to-adf.js strips frontmatter automatically)
adf=$(cat "$draft_file" | bun ~/.dataops-assistant/run skills/jira-publish/scripts/md-to-adf.js)
# Replace email placeholders with account IDs
adf=$(echo "$adf" | sed "s/__EMAIL__:john@example.com/5b10ac8d.../g")
# Pass raw ADF (no --md flag)
echo "$adf" | bash "$JIRA_API" create PE Story "Title" -
Option B: Replace @email in markdown with a temporary marker, then fix after:
- Not recommended - the @email syntax is designed for this flow
Example Preview with Mentions
┌─────────────────────────────────────────────────────────┐
│ PREVIEW: Create Issue │
├─────────────────────────────────────────────────────────┤
│ Project: PE │
│ Type: Story │
│ Summary: Review needed for API changes │
│ │
│ Mentions to resolve: │
│ ✓ @john.smith@company.com -> John Smith (5b10ac8d...) │
│ ✓ @jane.doe@company.com -> Jane Doe (6c21bd9e...) │
│ │
│ Description (first 500 chars): │
│ ───────────────────────────── │
│ Hey @john.smith@company.com can you review this? │
└─────────────────────────────────────────────────────────┘
Failed Resolution Example
❌ Cannot publish: Could not resolve mentions:
- bob.nobody@company.com (no matching Atlassian user)
Fix the email addresses in the draft and try again.
Post-Publish Updates
After successful publish, update the draft file:
- Change
status: drafttostatus: published - Add
published_issue: PE-1234(the created/updated issue key) - Write updated file back
Example update:
# Before
status: draft
published_issue: null
# After
status: published
published_issue: PE-1234
Execution Flow
CRITICAL: Pipe the whole draft file. Do NOT extract the body with awk/sed/shell.
The md-to-adf.js converter strips YAML frontmatter automatically, so piping
the entire file (frontmatter included) is correct and reliable.
For Create
# 1. Build command with optional flags
cmd="bash \"$JIRA_API\" create \"$project\" \"$issue_type\" \"$summary\" - --md"
# Add assignee if present (must be resolved account ID)
[[ -n "$assignee_id" ]] && cmd="$cmd --assignee \"$assignee_id\""
# Add parent if present
[[ -n "$parent" ]] && cmd="$cmd --parent \"$parent\""
# Add components if present (one --component per component)
for comp in "${components[@]}"; do
cmd="$cmd --component \"$comp\""
done
# 2. Pipe the WHOLE draft file (md-to-adf.js strips frontmatter)
# If mentions need resolution, use Option A from Mentions section instead
result=$(cat "$draft_file" | eval "$cmd")
issue_key=$(echo "$result" | jq -r '.issue_key')
# 3. Add links if any (use EXACT type from frontmatter)
for link in links:
bash "$JIRA_API" link "$issue_key" "${link.type}" "${link.target}"
# Example: if frontmatter has {type: blocks, target: PE-456}
# then run: bash "$JIRA_API" link PE-1234 blocks PE-456
# 4. Return URL
echo "$result" | jq -r '.issue_url'
For Update
# Build command with optional flags
cmd="bash \"$JIRA_API\" update \"$target_issue\" - --md"
# Add fix version if present
[[ -n "$fix_version" ]] && cmd="$cmd --fix-version \"$fix_version\""
# Pipe the WHOLE draft file (md-to-adf.js strips frontmatter)
result=$(cat "$draft_file" | eval "$cmd")
echo "$result" | jq -r '.issue_url'
For Comment
# Build command with optional flags
cmd="bash \"$JIRA_API\" comment \"$target_issue\" - --md"
# Add fix version if present (will add comment AND set fix version)
[[ -n "$fix_version" ]] && cmd="$cmd --fix-version \"$fix_version\""
# Pipe the WHOLE draft file (md-to-adf.js strips frontmatter)
result=$(cat "$draft_file" | eval "$cmd")
echo "$result" | jq -r '.issue_url'
Output
Success
✅ Published successfully!
Issue: PE-1234
URL: https://mcghealth.atlassian.net/browse/PE-1234
Links added:
- relates-to MAT-28
Draft updated: .jira-drafts/20250203-143022-openai-instance.md
Failure
❌ Publish failed:
Error: [error message from jira-api.sh]
Draft NOT modified. Fix the issue and try again.
Example Session
User: /dataops-assistant:jira-publish .jira-drafts/20250203-143022-openai-instance.md
Response:
┌─────────────────────────────────────────────────────────┐
│ PREVIEW: Create Issue │
├─────────────────────────────────────────────────────────┤
│ Project: PE │
│ Type: Story │
│ Summary: Set up dedicated OpenAI instance for CMS │
│ │
│ Description (first 500 chars): │
│ ───────────────────────────── │
│ :::context │
│ Forever ago, we set up the initial OpenAI instance for │
│ DataSci (MAT-28). Today they still use that single │
│ instance for everything... │
│ ::: │
│ │
│ ## What We Have Today │
│ - Single OpenAI instance shared across all DataSci... │
│ │
│ Links to add after creation: │
│ - relates-to MAT-28 │
└─────────────────────────────────────────────────────────┘
Create this issue in Jira?
User: Yes
Response:
✅ Published successfully!
Issue: PE-3456
URL: https://mcghealth.atlassian.net/browse/PE-3456
Links added:
- relates-to MAT-28
Draft updated: .jira-drafts/20250203-143022-openai-instance.md
Rules
- ALWAYS preview before execution - No exceptions
- ALWAYS ask for approval - User must explicitly confirm
- Validate before preview - Don't show preview if fields missing
- Update draft after publish - Mark as published with issue key
- Return URL - User needs to verify the result
- Don't modify draft on failure - Only update on success