Jira Sprint Distribution Rules
Companion to ROADMAP.md. Automation rules and manual procedures for distributing GEHR epics and stories into Jira sprints based on the roadmap slice mapping.
Jira Project:
GEHRonghasitech.atlassian.net
1. Sprint Structure
Sprint Naming Convention
GEHR-S{slice}-Sprint-{N}
Examples:
GEHR-S0-Sprint-1throughGEHR-S0-Sprint-6(M0: Platform Foundation)GEHR-S1-Sprint-1throughGEHR-S1-Sprint-6(M1: Core Clinical)GEHR-S2-Sprint-1throughGEHR-S2-Sprint-6(M2: Orders & Billing)GEHR-S3-Sprint-1throughGEHR-S3-Sprint-6(M3: Integrated Care)GEHR-S4-Sprint-1throughGEHR-S4-Sprint-6(M4: Full Platform)GEHR-S5-Sprint-1throughGEHR-S5-Sprint-6(M5: National Scale)
Sprint Duration
- 2 weeks per sprint
- 6 sprints per milestone (3 months)
- 36 total sprints across M0–M5
Jira Board Configuration
- Board type: Scrum
- Estimation: Story Points
- Sprint capacity: configurable per team (see team-capacity-model.md)
2. Label Taxonomy for Automation
Required Labels on All Issues
Every epic and story in GEHR must have these labels for automation to work:
| Label | Format | Example | Purpose |
|---|---|---|---|
slice | slice-S{N} | slice-S0, slice-S1 | Maps issue to milestone slice |
module | mod-{name} | mod-iam, mod-registration | Maps to owning microservice |
track | track-{type} | track-baseline, track-enhancement | Baseline vs enhancement |
priority-group | pg-{N} | pg-1, pg-2, pg-3 | Sprint ordering within slice |
team | team-{name} | team-platform, team-clinical | Owning team |
gate | gate-{code} | gate-offline, gate-ai, gate-security | Cross-cutting concern |
Priority Groups (Sprint Assignment Order)
| Priority Group | Label | Description | Typical Sprint |
|---|---|---|---|
| PG-1 | pg-1 | Foundation / blockers for other stories | Sprint 1–2 |
| PG-2 | pg-2 | Core features / main vertical slice | Sprint 2–4 |
| PG-3 | pg-3 | Depth, polish, integration, testing | Sprint 4–6 |
3. Module → Slice → Sprint Mapping Table
Use this table to bulk-label existing backlog issues. This is the authoritative mapping for Jira automation.
S0 — Platform Foundation (Sprints 1–6)
| Module | Epic Prefix | Slice Label | Priority Group | Target Sprint | Team Label |
|---|---|---|---|---|---|
| iam | IAM-EPIC | slice-S0 | pg-1 | Sprint 1 | team-platform |
| tenant | TEN-EPIC | slice-S0 | pg-1 | Sprint 1–2 | team-platform |
| hierarchy | HIER-EPIC | slice-S0 | pg-1 | Sprint 1–2 | team-platform |
| licensing | LICN-EPIC | slice-S0 | pg-2 | Sprint 2–3 | team-platform |
| access-policy | ACPOL-EPIC-01–04 | slice-S0 | pg-2 | Sprint 2–3 | team-platform |
| audit | AUD-EPIC | slice-S0 | pg-1 | Sprint 1–3 | team-security |
| config-resolver | CFG-EPIC | slice-S0 | pg-2 | Sprint 2–3 | team-platform |
| fhir-gateway | FHIR-EPIC-01 | slice-S0 | pg-2 | Sprint 2–3 | team-platform |
| terminology | TERM-EPIC-01 | slice-S0 | pg-2 | Sprint 3 | team-platform |
| ai-orchestrator | AIO-EPIC-01 | slice-S0 | pg-2 | Sprint 3–4 | team-ai |
| platform-admin | PADM-EPIC-01–03 | slice-S0 | pg-3 | Sprint 3–5 | team-platform |
| desktop-electron | DESK-EPIC-01 | slice-S0 | pg-1 | Sprint 1–2 | team-desktop |
| desktop-electron | DESK-EPIC-02 | slice-S0 | pg-2 | Sprint 2–4 | team-desktop |
S1 — Core Clinical (Sprints 7–12)
| Module | Epic Prefix | Slice Label | Priority Group | Target Sprint | Team Label |
|---|---|---|---|---|---|
| registration | REG-EPIC | slice-S1 | pg-1 | Sprint 7–8 | team-clinical |
| provider-directory | PROV-EPIC | slice-S1 | pg-1 | Sprint 7 | team-clinical |
| facility-management | FAC-EPIC | slice-S1 | pg-1 | Sprint 7 | team-clinical |
| scheduling | SCHED-EPIC | slice-S1 | pg-2 | Sprint 8–9 | team-clinical |
| patient-chart | PCHART-EPIC | slice-S1 | pg-2 | Sprint 8–9 | team-clinical |
| clinical-notes | CNOTE-EPIC | slice-S1 | pg-2 | Sprint 9–10 | team-clinical |
| vitals | VIT-EPIC | slice-S1 | pg-2 | Sprint 8–9 | team-clinical |
| problem-list | PROB-EPIC | slice-S1 | pg-2 | Sprint 9 | team-clinical |
| allergies | ALG-EPIC | slice-S1 | pg-2 | Sprint 9 | team-clinical |
| medication-management | MED-EPIC | slice-S1 | pg-2 | Sprint 9–10 | team-clinical |
| desktop-electron | DESK-EPIC-03 | slice-S1 | pg-2 | Sprint 8–11 | team-desktop |
| ai-orchestrator | AIO-EPIC-02 | slice-S1 | pg-3 | Sprint 10–11 | team-ai |
S2 — Orders & Billing (Sprints 13–18)
| Module | Epic Prefix | Slice Label | Priority Group | Target Sprint | Team Label |
|---|---|---|---|---|---|
| orders-cpoe | ORD-EPIC | slice-S2 | pg-1 | Sprint 13–15 | team-diagnostics |
| results | RES-EPIC | slice-S2 | pg-2 | Sprint 14–16 | team-diagnostics |
| terminology | TERM-EPIC-02–06 | slice-S2 | pg-1 | Sprint 13–14 | team-platform |
| billing | BILL-EPIC-01–02 | slice-S2 | pg-2 | Sprint 15–17 | team-finance |
| document-management | DOC-EPIC-01–08 | slice-S2 | pg-2 | Sprint 14–17 | team-clinical |
S3 — Integrated Care (Sprints 19–24)
| Module | Epic Prefix | Slice Label | Priority Group | Target Sprint | Team Label |
|---|---|---|---|---|---|
| laboratory-lis | LAB-EPIC | slice-S3 | pg-1 | Sprint 19–22 | team-diagnostics |
| pharmacy | PHARM-EPIC | slice-S3 | pg-1 | Sprint 19–21 | team-pharmacy |
| ghasi-e-prescribing-gateway | EPRESC-EPIC | slice-S3 | pg-2 | Sprint 20–22 | team-interop |
| digital-communication | DCOM-EPIC | slice-S3 | pg-2 | Sprint 19–22 | team-engagement |
| patient-portal-api | Portal EPICs | slice-S3 | pg-2 | Sprint 20–23 | team-engagement |
| insurance | INS-EPIC-01–04 | slice-S3 | pg-3 | Sprint 21–23 | team-finance |
| desktop-electron | DESK-EPIC-04,05 | slice-S3 | pg-3 | Sprint 22–24 | team-desktop |
S4 — Full Platform (Sprints 25–30)
| Module | Epic Prefix | Slice Label | Priority Group | Target Sprint | Team Label |
|---|---|---|---|---|---|
| radiology-pacs | RAD-EPIC | slice-S4 | pg-1 | Sprint 25–28 | team-diagnostics |
| health-population | HPOP-EPIC | slice-S4 | pg-1 | Sprint 25–28 | team-pophealth |
| billing | BILL-EPIC-03–05 | slice-S4 | pg-2 | Sprint 25–28 | team-finance |
| claims | CLM-EPIC | slice-S4 | pg-2 | Sprint 26–29 | team-finance |
| insurance | INS-EPIC-05–08 | slice-S4 | pg-2 | Sprint 27–29 | team-finance |
| immunizations | IMMZ-EPIC | slice-S4 | pg-2 | Sprint 26–29 | team-clinical |
| care-plans | CP-EPIC | slice-S4 | pg-3 | Sprint 27–29 | team-clinical |
| hl7v2-interop | HL7-EPIC | slice-S4 | pg-3 | Sprint 27–29 | team-interop |
| ai-orchestrator | AIO-EPIC-03 | slice-S4 | pg-3 | Sprint 28–30 | team-ai |
| access-policy | ACPOL-EPIC-05–07 | slice-S4 | pg-3 | Sprint 28–29 | team-platform |
| desktop-electron | DESK-EPIC-06,08,09,10 | slice-S4 | pg-3 | Sprint 27–30 | team-desktop |
S5 — National Scale (Sprints 31–36)
| Module | Epic Prefix | Slice Label | Priority Group | Target Sprint | Team Label |
|---|---|---|---|---|---|
| All (hardening) | Various | slice-S5 | pg-2 | Sprint 33–36 | Various |
| platform-admin | PADM-EPIC-04–07 | slice-S5 | pg-2 | Sprint 31–34 | team-platform |
| desktop-electron | DESK-EPIC-11 | slice-S5 | pg-3 | Sprint 34–36 | team-desktop |
4. Jira Automation Rules
Rule 1: Auto-assign sprint based on slice + priority group labels
Trigger: Issue label changed (or issue created with labels)
Condition: Issue has both slice-S{N} and pg-{M} labels
Action: Move issue to the correct sprint
{
"name": "GEHR: Auto-assign sprint from slice + priority group",
"trigger": "label_changed",
"conditions": [
{
"type": "label_matches",
"pattern": "slice-S\\d+"
},
{
"type": "label_matches",
"pattern": "pg-\\d+"
}
],
"actions": [
{
"type": "move_to_sprint",
"sprint_mapping": {
"slice-S0 + pg-1": "GEHR-S0-Sprint-1",
"slice-S0 + pg-2": "GEHR-S0-Sprint-3",
"slice-S0 + pg-3": "GEHR-S0-Sprint-5",
"slice-S1 + pg-1": "GEHR-S1-Sprint-1",
"slice-S1 + pg-2": "GEHR-S1-Sprint-3",
"slice-S1 + pg-3": "GEHR-S1-Sprint-5",
"slice-S2 + pg-1": "GEHR-S2-Sprint-1",
"slice-S2 + pg-2": "GEHR-S2-Sprint-3",
"slice-S2 + pg-3": "GEHR-S2-Sprint-5",
"slice-S3 + pg-1": "GEHR-S3-Sprint-1",
"slice-S3 + pg-2": "GEHR-S3-Sprint-3",
"slice-S3 + pg-3": "GEHR-S3-Sprint-5",
"slice-S4 + pg-1": "GEHR-S4-Sprint-1",
"slice-S4 + pg-2": "GEHR-S4-Sprint-3",
"slice-S4 + pg-3": "GEHR-S4-Sprint-5",
"slice-S5 + pg-1": "GEHR-S5-Sprint-1",
"slice-S5 + pg-2": "GEHR-S5-Sprint-3",
"slice-S5 + pg-3": "GEHR-S5-Sprint-5"
}
}
]
}
Rule 2: Auto-label stories from parent epic
Trigger: Issue linked to epic (parent link set)
Condition: Parent epic has slice-S{N} label
Action: Copy slice, module, team, and track labels to child story
{
"name": "GEHR: Inherit labels from parent epic",
"trigger": "issue_linked",
"conditions": [
{
"type": "link_type",
"value": "is child of"
},
{
"type": "parent_has_label",
"pattern": "slice-S\\d+"
}
],
"actions": [
{
"type": "copy_labels_from_parent",
"labels": ["slice-*", "mod-*", "team-*", "track-*"]
}
]
}
Rule 3: Auto-set component from module label
Trigger: Label mod-{name} added to issue
Action: Set Jira component to matching module name
{
"name": "GEHR: Set component from module label",
"trigger": "label_changed",
"conditions": [
{
"type": "label_matches",
"pattern": "mod-.*"
}
],
"actions": [
{
"type": "set_component",
"component_from_label": "mod-{name} → {name}"
}
]
}
Rule 4: Milestone gate reminder
Trigger: Sprint completed
Condition: Sprint name matches GEHR-S{N}-Sprint-6 (last sprint of slice)
Action: Create "Milestone Gate Review" task and notify stakeholders
{
"name": "GEHR: Milestone gate review at slice end",
"trigger": "sprint_completed",
"conditions": [
{
"type": "sprint_name_matches",
"pattern": "GEHR-S\\d+-Sprint-6"
}
],
"actions": [
{
"type": "create_issue",
"summary": "Milestone M{N} Gate Review",
"issue_type": "Task",
"labels": ["gate-review", "milestone-M{N}"],
"description": "Review all readiness gates per slice-release-readiness.md. Verify F/N/A/O/S/T/V/D dimensions."
},
{
"type": "send_notification",
"to": ["architecture-team", "security-team", "product-team"],
"message": "Milestone M{N} gate review is due. Please complete readiness checklist."
}
]
}
Rule 5: Flag stories without required labels
Trigger: Issue created or moved to sprint
Condition: Issue is Story type AND missing any of: slice-*, mod-*, team-*, pg-*
Action: Flag issue + add comment
{
"name": "GEHR: Flag unlabeled stories",
"trigger": "issue_moved_to_sprint",
"conditions": [
{
"type": "issue_type",
"value": "Story"
},
{
"type": "missing_labels",
"required_patterns": ["slice-S\\d+", "mod-.*", "team-.*", "pg-\\d+"]
}
],
"actions": [
{
"type": "flag_issue"
},
{
"type": "add_comment",
"body": "⚠️ This story is missing required labels for sprint automation. Please add: slice-S{N}, mod-{module}, team-{team}, pg-{priority}. See docs/roadmap/jira-sprint-distribution-rules.md for the mapping table."
}
]
}
Rule 6: Cross-cutting gate label triggers checklist
Trigger: Label gate-offline or gate-ai or gate-security added
Action: Add relevant checklist items to the issue
{
"name": "GEHR: Add gate checklist on label",
"trigger": "label_changed",
"conditions": [
{
"type": "label_matches",
"pattern": "gate-.*"
}
],
"actions": [
{
"type": "add_checklist",
"mapping": {
"gate-offline": [
"Offline E2E test passes (airplane mode)",
"Sync round-trip verified",
"Conflict resolution tested",
"Stale-data indicator visible",
"Idempotency verified",
"SQLite encryption verified"
],
"gate-ai": [
"Provenance tag on AI outputs",
"Safety classifier active",
"PII redaction before cloud calls",
"Per-tenant budget respected",
"Graceful degradation tested",
"Prompt regression tests pass"
],
"gate-security": [
"Tenant isolation test green",
"PHI encryption verified",
"Audit events emitting",
"No hardcoded secrets",
"Rate limiting configured",
"ModuleEntitlementGuard active"
]
}
}
]
}
5. Bulk Labeling Script (PowerShell)
Use this script to bulk-apply labels to existing backlog issues based on the mapping table above. Run after creating sprints in Jira.
# bulk-label-gehr-issues.ps1
# Prerequisites: Jira REST API token, jq installed
# Usage: ./bulk-label-gehr-issues.ps1
$JiraBase = "https://ghasitech.atlassian.net"
$Project = "GEHR"
$Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${env:JIRA_EMAIL}:${env:JIRA_API_TOKEN}"))
# Module → Slice → Priority Group → Team mapping
$Mappings = @(
# S0 - Platform Foundation
@{ Module = "iam"; EpicPrefix = "IAM-EPIC"; Slice = "S0"; PG = "1"; Team = "platform" }
@{ Module = "tenant"; EpicPrefix = "TEN-EPIC"; Slice = "S0"; PG = "1"; Team = "platform" }
@{ Module = "hierarchy"; EpicPrefix = "HIER-EPIC"; Slice = "S0"; PG = "1"; Team = "platform" }
@{ Module = "licensing"; EpicPrefix = "LICN-EPIC"; Slice = "S0"; PG = "2"; Team = "platform" }
@{ Module = "access-policy"; EpicPrefix = "ACPOL-EPIC"; Slice = "S0"; PG = "2"; Team = "platform" }
@{ Module = "audit"; EpicPrefix = "AUD-EPIC"; Slice = "S0"; PG = "1"; Team = "security" }
@{ Module = "config-resolver"; EpicPrefix = "CFG-EPIC"; Slice = "S0"; PG = "2"; Team = "platform" }
@{ Module = "ai-orchestrator"; EpicPrefix = "AIO-EPIC-01"; Slice = "S0"; PG = "2"; Team = "ai" }
@{ Module = "platform-admin"; EpicPrefix = "PADM-EPIC"; Slice = "S0"; PG = "3"; Team = "platform" }
# S1 - Core Clinical
@{ Module = "registration"; EpicPrefix = "REG-EPIC"; Slice = "S1"; PG = "1"; Team = "clinical" }
@{ Module = "provider-directory"; EpicPrefix = "PROV-EPIC"; Slice = "S1"; PG = "1"; Team = "clinical" }
@{ Module = "facility-management"; EpicPrefix = "FAC-EPIC"; Slice = "S1"; PG = "1"; Team = "clinical" }
@{ Module = "scheduling"; EpicPrefix = "SCHED-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "patient-chart"; EpicPrefix = "PCHART-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "clinical-notes"; EpicPrefix = "CNOTE-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "vitals"; EpicPrefix = "VIT-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "problem-list"; EpicPrefix = "PROB-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "allergies"; EpicPrefix = "ALG-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "medication-management"; EpicPrefix = "MED-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "ai-orchestrator"; EpicPrefix = "AIO-EPIC-02"; Slice = "S1"; PG = "3"; Team = "ai" }
# S2 - Orders & Billing
@{ Module = "orders-cpoe"; EpicPrefix = "ORD-EPIC"; Slice = "S2"; PG = "1"; Team = "diagnostics" }
@{ Module = "results"; EpicPrefix = "RES-EPIC"; Slice = "S2"; PG = "2"; Team = "diagnostics" }
@{ Module = "terminology"; EpicPrefix = "TERM-EPIC"; Slice = "S2"; PG = "1"; Team = "platform" }
@{ Module = "billing"; EpicPrefix = "BILL-EPIC-01"; Slice = "S2"; PG = "2"; Team = "finance" }
@{ Module = "billing"; EpicPrefix = "BILL-EPIC-02"; Slice = "S2"; PG = "2"; Team = "finance" }
@{ Module = "document-management"; EpicPrefix = "DOC-EPIC"; Slice = "S2"; PG = "2"; Team = "clinical" }
# S3 - Integrated Care
@{ Module = "laboratory-lis"; EpicPrefix = "LAB-EPIC"; Slice = "S3"; PG = "1"; Team = "diagnostics" }
@{ Module = "pharmacy"; EpicPrefix = "PHARM-EPIC"; Slice = "S3"; PG = "1"; Team = "pharmacy" }
@{ Module = "ghasi-e-prescribing-gateway"; EpicPrefix = "EPRESC-EPIC"; Slice = "S3"; PG = "2"; Team = "interop" }
@{ Module = "digital-communication"; EpicPrefix = "DCOM-EPIC"; Slice = "S3"; PG = "2"; Team = "engagement" }
@{ Module = "insurance"; EpicPrefix = "INS-EPIC"; Slice = "S3"; PG = "3"; Team = "finance" }
# S4 - Full Platform
@{ Module = "radiology-pacs"; EpicPrefix = "RAD-EPIC"; Slice = "S4"; PG = "1"; Team = "diagnostics" }
@{ Module = "health-population"; EpicPrefix = "HPOP-EPIC"; Slice = "S4"; PG = "1"; Team = "pophealth" }
@{ Module = "billing"; EpicPrefix = "BILL-EPIC-03"; Slice = "S4"; PG = "2"; Team = "finance" }
@{ Module = "claims"; EpicPrefix = "CLM-EPIC"; Slice = "S4"; PG = "2"; Team = "finance" }
@{ Module = "immunizations"; EpicPrefix = "IMMZ-EPIC"; Slice = "S4"; PG = "2"; Team = "clinical" }
@{ Module = "care-plans"; EpicPrefix = "CP-EPIC"; Slice = "S4"; PG = "3"; Team = "clinical" }
@{ Module = "ai-orchestrator"; EpicPrefix = "AIO-EPIC-03"; Slice = "S4"; PG = "3"; Team = "ai" }
)
foreach ($m in $Mappings) {
$labels = @("slice-$($m.Slice)", "mod-$($m.Module)", "team-$($m.Team)", "pg-$($m.PG)", "track-baseline")
# Find epics matching prefix in summary
$jql = "project = $Project AND issuetype = Epic AND summary ~ `"$($m.EpicPrefix)`""
$searchUrl = "$JiraBase/rest/api/3/search?jql=$([uri]::EscapeDataString($jql))&maxResults=100"
$response = Invoke-RestMethod -Uri $searchUrl -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
}
foreach ($issue in $response.issues) {
$existingLabels = $issue.fields.labels
$newLabels = ($existingLabels + $labels) | Sort-Object -Unique
$body = @{
fields = @{
labels = $newLabels
}
} | ConvertTo-Json -Depth 3
$updateUrl = "$JiraBase/rest/api/3/issue/$($issue.key)"
Invoke-RestMethod -Uri $updateUrl -Method Put -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
} -Body $body
Write-Host "Labeled $($issue.key) ($($issue.fields.summary)) with: $($labels -join ', ')"
# Also label child stories
$childJql = "project = $Project AND issuetype = Story AND parent = $($issue.key)"
$childUrl = "$JiraBase/rest/api/3/search?jql=$([uri]::EscapeDataString($childJql))&maxResults=100"
$childResponse = Invoke-RestMethod -Uri $childUrl -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
}
foreach ($child in $childResponse.issues) {
$childExisting = $child.fields.labels
$childNew = ($childExisting + $labels) | Sort-Object -Unique
$childBody = @{
fields = @{
labels = $childNew
}
} | ConvertTo-Json -Depth 3
$childUpdateUrl = "$JiraBase/rest/api/3/issue/$($child.key)"
Invoke-RestMethod -Uri $childUpdateUrl -Method Put -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
} -Body $childBody
Write-Host " Labeled child $($child.key) with: $($labels -join ', ')"
}
Start-Sleep -Milliseconds 200 # Rate limiting
}
}
6. Sprint Creation Script (PowerShell)
Create all 36 sprints in Jira before running the labeling script.
# create-gehr-sprints.ps1
# Prerequisites: Jira REST API token, board ID known
# Usage: ./create-gehr-sprints.ps1 -BoardId <your-board-id>
param(
[Parameter(Mandatory=$true)]
[int]$BoardId
)
$JiraBase = "https://ghasitech.atlassian.net"
$Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${env:JIRA_EMAIL}:${env:JIRA_API_TOKEN}"))
# Define start date (adjust to your actual project start)
$ProjectStart = Get-Date "2026-05-01"
$SprintDuration = 14 # days
$Slices = @(
@{ Name = "S0"; Label = "Platform Foundation"; Milestone = "M0" }
@{ Name = "S1"; Label = "Core Clinical"; Milestone = "M1" }
@{ Name = "S2"; Label = "Orders & Billing"; Milestone = "M2" }
@{ Name = "S3"; Label = "Integrated Care"; Milestone = "M3" }
@{ Name = "S4"; Label = "Full Platform"; Milestone = "M4" }
@{ Name = "S5"; Label = "National Scale"; Milestone = "M5" }
)
$sprintIndex = 0
foreach ($slice in $Slices) {
for ($i = 1; $i -le 6; $i++) {
$startDate = $ProjectStart.AddDays($sprintIndex * $SprintDuration)
$endDate = $startDate.AddDays($SprintDuration)
$sprintName = "GEHR-$($slice.Name)-Sprint-$i"
$goal = "$($slice.Milestone): $($slice.Label) - Sprint $i of 6"
$body = @{
name = $sprintName
startDate = $startDate.ToString("yyyy-MM-dd")
endDate = $endDate.ToString("yyyy-MM-dd")
originBoardId = $BoardId
goal = $goal
} | ConvertTo-Json
$createUrl = "$JiraBase/rest/agile/1.0/sprint"
$response = Invoke-RestMethod -Uri $createUrl -Method Post -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
} -Body $body
Write-Host "Created sprint: $sprintName (ID: $($response.id)) [$($startDate.ToString('MMM dd')) - $($endDate.ToString('MMM dd'))]"
$sprintIndex++
Start-Sleep -Milliseconds 200
}
}
7. Move Issues to Sprints Script (PowerShell)
After labeling and sprint creation, move issues to their target sprints.
# move-to-sprints.ps1
# Moves labeled issues to their target sprints
$JiraBase = "https://ghasitech.atlassian.net"
$Project = "GEHR"
$Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${env:JIRA_EMAIL}:${env:JIRA_API_TOKEN}"))
# First, get all sprint IDs
param(
[Parameter(Mandatory=$true)]
[int]$BoardId
)
$sprintsUrl = "$JiraBase/rest/agile/1.0/board/$BoardId/sprint?maxResults=50"
$sprints = (Invoke-RestMethod -Uri $sprintsUrl -Headers @{
Authorization = "Basic $Auth"
}).values
# Build sprint name → ID lookup
$sprintLookup = @{}
foreach ($s in $sprints) {
$sprintLookup[$s.name] = $s.id
}
# Slice + PG → Sprint name mapping
$SprintMap = @{
"slice-S0:pg-1" = "GEHR-S0-Sprint-1"
"slice-S0:pg-2" = "GEHR-S0-Sprint-3"
"slice-S0:pg-3" = "GEHR-S0-Sprint-5"
"slice-S1:pg-1" = "GEHR-S1-Sprint-1"
"slice-S1:pg-2" = "GEHR-S1-Sprint-3"
"slice-S1:pg-3" = "GEHR-S1-Sprint-5"
"slice-S2:pg-1" = "GEHR-S2-Sprint-1"
"slice-S2:pg-2" = "GEHR-S2-Sprint-3"
"slice-S2:pg-3" = "GEHR-S2-Sprint-5"
"slice-S3:pg-1" = "GEHR-S3-Sprint-1"
"slice-S3:pg-2" = "GEHR-S3-Sprint-3"
"slice-S3:pg-3" = "GEHR-S3-Sprint-5"
"slice-S4:pg-1" = "GEHR-S4-Sprint-1"
"slice-S4:pg-2" = "GEHR-S4-Sprint-3"
"slice-S4:pg-3" = "GEHR-S4-Sprint-5"
"slice-S5:pg-1" = "GEHR-S5-Sprint-1"
"slice-S5:pg-2" = "GEHR-S5-Sprint-3"
"slice-S5:pg-3" = "GEHR-S5-Sprint-5"
}
foreach ($entry in $SprintMap.GetEnumerator()) {
$parts = $entry.Key -split ":"
$sliceLabel = $parts[0]
$pgLabel = $parts[1]
$sprintName = $entry.Value
$sprintId = $sprintLookup[$sprintName]
if (-not $sprintId) {
Write-Warning "Sprint not found: $sprintName"
continue
}
# Find issues with both labels
$jql = "project = $Project AND labels = `"$sliceLabel`" AND labels = `"$pgLabel`" AND sprint is EMPTY"
$searchUrl = "$JiraBase/rest/api/3/search?jql=$([uri]::EscapeDataString($jql))&maxResults=200&fields=key,summary"
$response = Invoke-RestMethod -Uri $searchUrl -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
}
if ($response.issues.Count -eq 0) {
Write-Host "No unassigned issues for $sliceLabel + $pgLabel"
continue
}
$issueKeys = $response.issues | ForEach-Object { $_.key }
# Move to sprint
$moveBody = @{
issues = $issueKeys
} | ConvertTo-Json
$moveUrl = "$JiraBase/rest/agile/1.0/sprint/$sprintId/issue"
Invoke-RestMethod -Uri $moveUrl -Method Post -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
} -Body $moveBody
Write-Host "Moved $($issueKeys.Count) issues to $sprintName : $($issueKeys -join ', ')"
Start-Sleep -Milliseconds 300
}
8. Jira Automation Rules (Import-Ready JSON)
These rules can be imported directly into Jira Automation (Project Settings → Automation → Import).
Rule: Sprint overflow redistribution
When a sprint is overfull (>80% of capacity), move lowest-priority stories to the next sprint.
{
"name": "GEHR: Sprint overflow redistribution",
"trigger": {
"type": "scheduled",
"cron": "0 0 9 * * MON"
},
"conditions": [
{
"type": "jql",
"jql": "project = GEHR AND sprint in openSprints() AND issuetype = Story"
}
],
"actions": [
{
"type": "comment",
"body": "Sprint capacity check: review if this sprint exceeds team capacity. Move overflow items to next sprint."
}
]
}
Rule: Slice completion notification
{
"name": "GEHR: Slice completion notification",
"trigger": {
"type": "jql_condition_met",
"jql": "project = GEHR AND labels = 'slice-S{N}' AND status != Done",
"condition": "count = 0"
},
"actions": [
{
"type": "send_email",
"to": "asmar.momand@gmail.com",
"subject": "GEHR: Slice S{N} complete!",
"body": "All issues in slice S{N} are resolved. Ready for milestone gate review."
}
]
}
9. Execution Checklist
Run these steps in order to set up Jira for the roadmap:
- [ ] Create sprints — Run
create-gehr-sprints.ps1with your board ID - [ ] Verify sprints — Check Jira board shows 36 sprints in correct order
- [ ] Bulk-label epics — Run
bulk-label-gehr-issues.ps1to apply slice/module/team/pg labels - [ ] Verify labels — Spot-check 10 issues across different modules
- [ ] Move to sprints — Run
move-to-sprints.ps1to distribute labeled issues - [ ] Import automation rules — Import the JSON rules into Jira Automation
- [ ] Remaining modules — Push the 12 unpushed modules (allergies, billing, care-plans, claims, clinical-notes, iam, insurance, licensing, orders-cpoe, radiology-pacs, results, registration enhancement) first, then label and assign
- [ ] Set sprint capacities — Configure per-team sprint capacity based on team-capacity-model.md
- [ ] Review board — Walk through sprint 1 with team leads; adjust assignments
- [ ] Start S0-Sprint-1 — Activate the first sprint
10. Handling Special Cases
Stories spanning multiple sprints
- Split into sub-tasks, each fitting within a single sprint.
- Parent story stays in the start sprint; sub-tasks distribute across sprints.
Cross-cutting stories (offline, security, AI)
- Add
gate-offline,gate-ai, orgate-securitylabels. - These stories may span teams — assign to the primary team and add watchers for secondary teams.
Enhancement track vs baseline track
- All
track-enhancementstories are assigned to S4 or S5 by default. - Exceptions require product owner approval and explicit sprint assignment.
Stories not yet in Jira (12 unpushed modules)
- Push remaining modules using the existing
push-unified-to-jira-rest.mjsscript. - Then run the bulk-labeling script to assign labels.
- Then run the sprint-move script.
Desktop-electron stories
- DESK-EPIC-01, 02 → S0 (Desktop team)
- DESK-EPIC-03 → S1 (Desktop team)
- DESK-EPIC-04, 05 → S3 (Desktop team)
- DESK-EPIC-06, 08, 09, 10 → S4 (Desktop team)
- DESK-EPIC-11 → S5 (Desktop team)
- Label each with both
mod-desktop-electronand the correspondingslice-S{N}.