Skip to content

Instantly share code, notes, and snippets.

@eranco74
Created March 18, 2026 13:29
Show Gist options
  • Select an option

  • Save eranco74/dc07b0aec680597ebafd29f74f9cd0ee to your computer and use it in GitHub Desktop.

Select an option

Save eranco74/dc07b0aec680597ebafd29f74f9cd0ee to your computer and use it in GitHub Desktop.
GSD-Jira Integration: Auto-sync GSD milestones/phases with Jira epics/tickets, Jira keys in commit messages

GSD-Jira Integration Setup Guide

This guide explains how to set up automatic Jira synchronization with the GSD (Get Stuff Done) workflow in Claude Code. When configured, GSD will automatically create/update Jira issues as you work through milestones and phases, and use Jira ticket IDs in commit messages.

What It Does

GSD Event Jira Action Commit Format
/gsd:new-milestone Creates Epic (or skips if already linked)
/gsd:plan-phase N Creates Task under Epic for the phase
/gsd:execute-phase N Moves Task to "In Progress" MGMT-XXXXX: description
PR created Move Task to "Code Review" (manual)
/gsd:complete-milestone Moves Epic to "Done"

Commit messages change from the default GSD format:

feat(08-02): create user registration endpoint

to Jira-prefixed format:

MGMT-12346: create user registration endpoint

You can also link existing Jira epics and tickets instead of creating new ones:

/jira-sync link-epic MGMT-12345
/jira-sync link-phase 3 MGMT-12346
/jira-sync status
/jira-sync unlink

Prerequisites

  1. Claude Code installed
  2. GSD installed (npx get-shit-done-cc@latest)
  3. jira-cli installed and configured for Red Hat Jira
  4. Jira Claude Code plugin installed

Install jira-cli

go install github.com/ankitpokhrel/jira-cli/cmd/jira@latest

Configure jira-cli

jira init --installation local --server https://issues.redhat.com --auth-type bearer

Add your Personal Access Token to ~/.netrc:

machine issues.redhat.com
  login <your-email>
  password <your-personal-access-token>

Generate a token at: issues.redhat.com → Profile → Personal Access Tokens

Verify:

jira me
# Should print your email

Install the Jira plugin

claude plugins install jira@ecosystem-claude-plugins

Installation

The integration requires two types of changes:

  1. New files — a workflow file and a command file (easy to copy)
  2. Patches to existing GSD workflow files — modifications to 4 GSD files (need to be reapplied after GSD updates)

Step 1: Create the /jira-sync command

Create the command file at ~/.claude/commands/gsd/jira-sync.md:

---
name: gsd:jira-sync
description: Link GSD milestones and phases to Jira epics and tickets, or view current mapping
argument-hint: "<link-epic MGMT-XXXXX | link-phase N MGMT-XXXXX | status | unlink>"
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash
  - AskUserQuestion
---
<objective>
Manage the Jira mapping for the current GSD milestone. Link existing Jira epics and tickets to milestones and phases, view current mapping, or remove mappings.

Subcommands:
- `link-epic MGMT-XXXXX` — Link existing Jira epic to current milestone
- `link-phase <phase-number> MGMT-XXXXX` — Link existing Jira ticket to a phase
- `status` — Show current Jira mapping with live status from Jira
- `unlink` — Remove all Jira mappings

When no subcommand is given, show status.
</objective>

<execution_context>
@/home/eran/.claude/get-shit-done/workflows/jira-sync.md
</execution_context>

<context>
Subcommand and arguments: $ARGUMENTS

Jira CLI is pre-configured for Red Hat Jira (issues.redhat.com), MGMT project.
Mapping is stored in `.planning/config.json` under the `jira` key.
</context>

<process>
Execute the jira-sync workflow from @/home/eran/.claude/get-shit-done/workflows/jira-sync.md.

Use the jira-task-management skill knowledge for Jira CLI commands.
</process>

Note: Update the @/home/eran/ paths to match your home directory.

Step 2: Create the jira-sync workflow

Copy the workflow file to ~/.claude/get-shit-done/workflows/jira-sync.md.

The source file is available in this repository at docs/gsd-jira-sync-workflow.md.

cp docs/gsd-jira-sync-workflow.md ~/.claude/get-shit-done/workflows/jira-sync.md

Step 3: Patch GSD workflow files

Apply these changes to the GSD workflow files in ~/.claude/get-shit-done/workflows/. Each patch shows the exact location and content to insert.

Important: These patches must be reapplied after running /gsd:update. GSD automatically backs up modified files and you can use /gsd:reapply-patches to restore them.

Patch 1: new-milestone.md — Create Jira Epic

Location: Insert new step 10.5 between step 10 (roadmap commit) and step 11 (Done banner).

Find this line: ## 11. Done

Insert BEFORE it:

## 10.5. Jira Sync — Create or Link Epic

After roadmap is committed, sync with Jira. If an epic is already linked (via `/jira-sync link-epic`), skip creation. Otherwise, create a new one.

```bash
if command -v jira &>/dev/null; then
  # Check if epic already linked
  EXISTING_EPIC=$(node -e "try { const c=JSON.parse(require('fs').readFileSync('.planning/config.json','utf8')); console.log(c.jira?.epic || ''); } catch(e) { console.log(''); }")

  if [ -n "$EXISTING_EPIC" ]; then
    echo "Jira Epic already linked: ${EXISTING_EPIC} — skipping creation"
  else
    MILESTONE_TITLE="v[X.Y] [Milestone Name]"
    MILESTONE_GOAL="[One sentence goal from step 4]"

    EPIC_OUTPUT=$(jira epic create -s "${MILESTONE_TITLE}" -n "${MILESTONE_TITLE}" -b "GSD Milestone: ${MILESTONE_GOAL}" -l OSAC --no-input 2>&1)
    EPIC_KEY=$(echo "$EPIC_OUTPUT" | grep -oE 'MGMT-[0-9]+' | head -1)

    if [ -n "$EPIC_KEY" ]; then
      node -e "
      const fs = require('fs');
      const cfg = JSON.parse(fs.readFileSync('.planning/config.json', 'utf8'));
      cfg.jira = { epic: '${EPIC_KEY}', phases: {} };
      fs.writeFileSync('.planning/config.json', JSON.stringify(cfg, null, 2) + '\n');
      "
      echo "Jira Epic created: ${EPIC_KEY}"
    else
      echo "Warning: Could not create Jira epic (jira CLI may not be configured)"
    fi
  fi
fi
```

Patch 2: plan-phase.md — Create Jira Task for Phase

Location: Insert new step 12.5 between step 12 (revision loop) and step 13 (present final status).

Find this line: ## 13. Present Final Status

Insert BEFORE it:

## 12.5. Jira Sync — Create Task for Phase

After plans are verified, create a Jira Task under the Epic for this phase.

```bash
if command -v jira &>/dev/null; then
  EPIC_KEY=$(node -e "try { const c=JSON.parse(require('fs').readFileSync('.planning/config.json','utf8')); console.log(c.jira?.epic || ''); } catch(e) { console.log(''); }")
  EXISTING=$(node -e "try { const c=JSON.parse(require('fs').readFileSync('.planning/config.json','utf8')); console.log(c.jira?.phases?.['${PHASE}'] || ''); } catch(e) { console.log(''); }")

  if [ -n "$EPIC_KEY" ] && [ -z "$EXISTING" ]; then
    TASK_OUTPUT=$(jira issue create -tTask -s "Phase ${PHASE}: ${phase_name}" \
      -b "GSD Phase ${PHASE}: ${phase_name}" \
      -P "$EPIC_KEY" -l OSAC --no-input 2>&1)
    TASK_KEY=$(echo "$TASK_OUTPUT" | grep -oE 'MGMT-[0-9]+' | head -1)

    if [ -n "$TASK_KEY" ]; then
      node -e "
      const fs = require('fs');
      const cfg = JSON.parse(fs.readFileSync('.planning/config.json', 'utf8'));
      if (!cfg.jira) cfg.jira = { epic: '', phases: {} };
      if (!cfg.jira.phases) cfg.jira.phases = {};
      cfg.jira.phases['${PHASE}'] = '${TASK_KEY}';
      fs.writeFileSync('.planning/config.json', JSON.stringify(cfg, null, 2) + '\n');
      "
      echo "Jira Task created: ${TASK_KEY} (linked to ${EPIC_KEY})"
    fi
  fi
fi
```

Patch 3: execute-phase.md — Move Task to "In Progress"

Location: Insert new step jira_sync_in_progress BEFORE the existing validate_phase step.

Find this line: <step name="validate_phase">

Insert BEFORE it:

<step name="jira_sync_in_progress">
Move the Jira Task for this phase to "In Progress":

```bash
if command -v jira &>/dev/null; then
  JIRA_KEY=$(node -e "try { const c=JSON.parse(require('fs').readFileSync('.planning/config.json','utf8')); console.log(c.jira?.phases?.['${PHASE_NUMBER}'] || ''); } catch(e) { console.log(''); }")
  if [ -n "$JIRA_KEY" ]; then
    jira issue move "$JIRA_KEY" "In Progress" 2>/dev/null || true
    echo "Jira ${JIRA_KEY} → In Progress"
  fi
fi
```
</step>

Patch 4: execute-plan.md — Jira commit message format

This patch has 4 sub-changes in the same file:

4a. Task Commit Protocol (replace the commit type table and format line)

Find the section starting with **3. Commit type:** through **4. Format:**. Replace the entire block with:

**3. Resolve commit prefix — check for Jira key first:**

```bash
JIRA_KEY=$(node -e "
try {
  const fs = require('fs');
  const c = JSON.parse(fs.readFileSync('.planning/config.json', 'utf8'));
  const phase = '${PHASE}'.split('-')[0].replace(/^0+/, '');
  console.log(c.jira?.phases?.[phase] || '');
} catch(e) { console.log(''); }
" 2>/dev/null)
```

**If JIRA_KEY is set**, use Jira-prefixed format:

| Example |
|---------|
| MGMT-12346: create user registration endpoint |
| MGMT-12346: correct email validation regex |
| MGMT-12346: add failing test for password hashing |
| MGMT-12346: add bcrypt dependency |

**Format:** `{JIRA_KEY}: {description}` with bullet points for key changes.

**If JIRA_KEY is empty**, fall back to conventional commit format:

| Type | When | Example |
|------|------|---------|
| `feat` | New functionality | feat(08-02): create user registration endpoint |
| `fix` | Bug fix | fix(08-02): correct email validation regex |
| `test` | Test-only (TDD RED) | test(08-02): add failing test for password hashing |
| `refactor` | No behavior change (TDD REFACTOR) | refactor(08-02): extract validation to helper |
| `perf` | Performance | perf(08-02): add database index |
| `docs` | Documentation | docs(08-02): add API docs |
| `style` | Formatting | style(08-02): format auth module |
| `chore` | Config/deps | chore(08-02): add bcrypt dependency |

**Format:** `{type}({phase}-{plan}): {description}` with bullet points for key changes.

4b. TDD commit messages

Find the 3 lines for RED/GREEN/REFACTOR commits. Replace with:

2. **RED:** ... commit using resolved prefix: `{JIRA_KEY}: add failing test for [feature]` or fallback `test({phase}-{plan}): add failing test for [feature]`
3. **GREEN:** ... commit using resolved prefix: `{JIRA_KEY}: implement [feature]` or fallback `feat({phase}-{plan}): implement [feature]`
4. **REFACTOR:** ... commit using resolved prefix: `{JIRA_KEY}: clean up [feature]` or fallback `refactor({phase}-{plan}): clean up [feature]`

4c. Metadata commit step

Find <step name="git_commit_metadata">. Replace the entire step with:

<step name="git_commit_metadata">
Task code already committed per-task. Commit plan metadata using resolved prefix:

```bash
# Use Jira key if mapped, otherwise default format
JIRA_KEY=$(node -e "
try {
  const fs = require('fs');
  const c = JSON.parse(fs.readFileSync('.planning/config.json', 'utf8'));
  const phase = '${PHASE}'.split('-')[0].replace(/^0+/, '');
  console.log(c.jira?.phases?.[phase] || '');
} catch(e) { console.log(''); }
" 2>/dev/null)

if [ -n "$JIRA_KEY" ]; then
  COMMIT_MSG="${JIRA_KEY}: complete [plan-name] plan"
else
  COMMIT_MSG="docs({phase}-{plan}): complete [plan-name] plan"
fi

node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "${COMMIT_MSG}" --files .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md .planning/REQUIREMENTS.md
```

4d. Codebase map git log grep

Find the FIRST_TASK=$(git log ...) line in the update_codebase_map step. Replace with:

# Search for commits by Jira key or phase-plan pattern
JIRA_KEY=$(node -e "try { const fs=require('fs'); const c=JSON.parse(fs.readFileSync('.planning/config.json','utf8')); const p='${PHASE}'.split('-')[0].replace(/^0+/,''); console.log(c.jira?.phases?.[p]||''); } catch(e) { console.log(''); }" 2>/dev/null)
if [ -n "$JIRA_KEY" ]; then
  FIRST_TASK=$(git log --oneline --grep="${JIRA_KEY}:" --reverse | head -1 | cut -d' ' -f1)
else
  FIRST_TASK=$(git log --oneline --grep="feat({phase}-{plan}):" --grep="fix({phase}-{plan}):" --grep="test({phase}-{plan}):" --reverse | head -1 | cut -d' ' -f1)
fi

Patch 5: complete-milestone.md — Move Epic to "Done"

Location: Insert new step jira_sync_done BEFORE the existing git_tag step.

Find this line: <step name="git_tag">

Insert BEFORE it:

<step name="jira_sync_done">

Move Jira Epic to "Done":

```bash
if command -v jira &>/dev/null; then
  EPIC_KEY=$(node -e "try { const c=JSON.parse(require('fs').readFileSync('.planning/config.json','utf8')); console.log(c.jira?.epic || ''); } catch(e) { console.log(''); }")
  if [ -n "$EPIC_KEY" ]; then
    jira issue move "$EPIC_KEY" "Done" 2>/dev/null || true
    echo "Jira Epic ${EPIC_KEY} → Done"
  fi
fi
```

</step>

Config Schema

The Jira mapping is stored in .planning/config.json (per-project, alongside other GSD config):

{
  "mode": "yolo",
  "parallelization": true,
  "jira": {
    "epic": "MGMT-12345",
    "phases": {
      "1": "MGMT-12346",
      "2": "MGMT-12347",
      "3": "MGMT-12348"
    }
  }
}

Usage

Workflow A: New milestone (auto-creates Jira issues)

/gsd:new-milestone          # Creates Epic automatically
/gsd:plan-phase 1           # Creates Task MGMT-XXXXX under Epic
/gsd:execute-phase 1        # Moves Task to "In Progress", commits use MGMT-XXXXX: ...
/gsd:complete-milestone     # Moves Epic to "Done"

Workflow B: Link existing Jira issues first

/jira-sync link-epic MGMT-12345          # Link your existing epic
/jira-sync link-phase 1 MGMT-12346      # Link existing ticket to phase 1
/jira-sync link-phase 2 MGMT-12347      # Link existing ticket to phase 2
/jira-sync status                         # Verify mapping

/gsd:plan-phase 3                        # Phase 3 has no link → auto-creates Task
/gsd:execute-phase 1                     # Uses MGMT-12346 in commits

Workflow C: Check mapping

/jira-sync status
# Shows table of all phases with Jira keys, summaries, and statuses

Safety

  • All Jira operations check command -v jira first — no errors if CLI isn't installed
  • All Jira transitions use || true — GSD workflows never fail due to Jira errors
  • Duplicate detection — won't create a second ticket if phase is already mapped
  • If no Jira mapping exists, commits fall back to default GSD format

Maintenance

After GSD updates

/gsd:update                  # GSD backs up modified files automatically
/gsd:reapply-patches         # Restores your Jira patches

If /gsd:reapply-patches doesn't work (e.g., GSD restructured the files), re-apply the patches manually using this guide.

Files modified

File Type Location
jira-sync.md New command ~/.claude/commands/gsd/jira-sync.md
jira-sync.md New workflow ~/.claude/get-shit-done/workflows/jira-sync.md
new-milestone.md Patched ~/.claude/get-shit-done/workflows/
plan-phase.md Patched ~/.claude/get-shit-done/workflows/
execute-phase.md Patched ~/.claude/get-shit-done/workflows/
execute-plan.md Patched ~/.claude/get-shit-done/workflows/
complete-milestone.md Patched ~/.claude/get-shit-done/workflows/

Customization

Different Jira project

The integration defaults to the MGMT project and OSAC label. To change:

  • Project prefix: Search for MGMT- in the workflow files and replace with your project key
  • Label: Search for -l OSAC and replace with your label
  • Jira server: This is configured in jira-cli, not in these files

Different ticket key format

The grep -oE 'MGMT-[0-9]+' pattern extracts ticket keys from Jira CLI output. Update this regex if your project uses a different key format.

Link GSD milestones and phases to existing Jira epics and tickets, or view current mapping. Manages the jira section in .planning/config.json.

Parse Subcommand

Extract subcommand and arguments from $ARGUMENTS:

  • link-epic <EPIC-KEY> — Link existing Jira epic to current milestone
  • link-phase <PHASE-NUMBER> <ISSUE-KEY> — Link existing Jira ticket to a phase
  • status — Show current Jira mapping
  • unlink — Remove all Jira mappings
  • (no args) — Show status

Subcommand: link-epic

  1. Validate the epic key format (MGMT-NNNNN):
EPIC_KEY="$1"
if [[ ! "$EPIC_KEY" =~ ^MGMT-[0-9]+$ ]]; then
  echo "Error: Expected format MGMT-NNNNN, got: $EPIC_KEY"
  exit 1
fi
  1. Verify the epic exists in Jira:
jira issue view "$EPIC_KEY" --plain 2>&1

If error: "Epic $EPIC_KEY not found in Jira."

  1. Read current milestone from PROJECT.md:
MILESTONE=$(grep -A1 "## Current Milestone" .planning/PROJECT.md 2>/dev/null | tail -1 | sed 's/\*\*Goal:\*\*//' | tr -d '*')

If no current milestone found: "No active milestone. Run /gsd:new-milestone first."

  1. Store mapping:
node -e "
const fs = require('fs');
const cfg = JSON.parse(fs.readFileSync('.planning/config.json', 'utf8'));
if (!cfg.jira) cfg.jira = {};
cfg.jira.epic = '${EPIC_KEY}';
if (!cfg.jira.phases) cfg.jira.phases = {};
fs.writeFileSync('.planning/config.json', JSON.stringify(cfg, null, 2) + '\n');
"
  1. Report:
Linked ${EPIC_KEY} to current milestone
Epic: ${EPIC_KEY} — [epic summary from jira view]
Milestone: [current milestone from PROJECT.md]

Phase tasks will be created under this epic during /gsd:plan-phase.
Use /jira-sync link-phase to link existing tickets to phases.

Subcommand: link-phase

  1. Parse args: PHASE_NUMBER and ISSUE_KEY
PHASE_NUMBER="$1"
ISSUE_KEY="$2"

if [ -z "$PHASE_NUMBER" ] || [ -z "$ISSUE_KEY" ]; then
  echo "Usage: /jira-sync link-phase <phase-number> <ISSUE-KEY>"
  echo "Example: /jira-sync link-phase 3 MGMT-12346"
  exit 1
fi

if [[ ! "$ISSUE_KEY" =~ ^MGMT-[0-9]+$ ]]; then
  echo "Error: Expected format MGMT-NNNNN, got: $ISSUE_KEY"
  exit 1
fi
  1. Verify the issue exists:
jira issue view "$ISSUE_KEY" --plain 2>&1
  1. Verify the phase exists in the roadmap:
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" roadmap get-phase "${PHASE_NUMBER}"

If phase not found: "Phase ${PHASE_NUMBER} not found in roadmap."

  1. Store mapping:
node -e "
const fs = require('fs');
const cfg = JSON.parse(fs.readFileSync('.planning/config.json', 'utf8'));
if (!cfg.jira) cfg.jira = { epic: '', phases: {} };
if (!cfg.jira.phases) cfg.jira.phases = {};
cfg.jira.phases['${PHASE_NUMBER}'] = '${ISSUE_KEY}';
fs.writeFileSync('.planning/config.json', JSON.stringify(cfg, null, 2) + '\n');
"
  1. Report:
Linked Phase ${PHASE_NUMBER} to ${ISSUE_KEY}
Phase: ${PHASE_NUMBER} — [phase name from roadmap]
Ticket: ${ISSUE_KEY} — [issue summary from jira view]

Commits for this phase will use: ${ISSUE_KEY}: <description>

Subcommand: status

  1. Read config:
node -e "
const fs = require('fs');
try {
  const cfg = JSON.parse(fs.readFileSync('.planning/config.json', 'utf8'));
  console.log(JSON.stringify(cfg.jira || null));
} catch(e) { console.log('null'); }
"
  1. If no jira section:
No Jira mapping configured.

Use /jira-sync link-epic MGMT-XXXXX to link an epic to the current milestone.
Or start a new milestone with /gsd:new-milestone (auto-creates epic).
  1. If jira section exists, display mapping table:
## Jira Mapping

**Epic:** ${epic_key} — [summary from jira view, if reachable]

| Phase | Jira Key | Summary | Status |
|-------|----------|---------|--------|
| 1     | MGMT-123 | [from jira] | [from jira] |
| 2     | MGMT-124 | [from jira] | [from jira] |
| 3     | (not linked) | — | — |

Commit format: MGMT-XXXXX: <description>

For each mapped phase, fetch status from Jira:

jira issue view "$KEY" --plain 2>/dev/null | grep -E "^(Summary|Status):"

For unmapped phases (in roadmap but not in jira.phases), show "(not linked)".

Subcommand: unlink

  1. Remove jira section from config:
node -e "
const fs = require('fs');
const cfg = JSON.parse(fs.readFileSync('.planning/config.json', 'utf8'));
delete cfg.jira;
fs.writeFileSync('.planning/config.json', JSON.stringify(cfg, null, 2) + '\n');
"
  1. Report: "Jira mapping removed. Commits will use default format: {type}({phase}-{plan}): description"

<success_criteria>

  • link-epic validates epic exists in Jira before storing
  • link-phase validates both phase and issue exist before storing
  • status shows all phases (mapped and unmapped) with live Jira data
  • unlink cleanly removes mapping without affecting other config
  • All operations are idempotent (re-linking overwrites previous mapping) </success_criteria>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment