@@ -829,11 +829,51 @@ async function findIssueTypeId(
829829 return match ?. id ?? null ;
830830}
831831
832+ // =============================================================================
833+ // Roadmap Labeling
834+ // =============================================================================
835+
836+ function sanitizeRoadmapLabel ( title : string ) : string {
837+ return title
838+ . toLowerCase ( )
839+ . replace ( / [ ^ a - z 0 - 9 ] + / g, "-" )
840+ . replace ( / ^ - | - $ / g, "" ) ;
841+ }
842+
843+ async function addLabelToIssue (
844+ auth : JiraAuth ,
845+ issueKey : string ,
846+ label : string ,
847+ ) : Promise < boolean > {
848+ const result = await jiraFetch ( auth , "PUT" , `/issue/${ issueKey } ` , {
849+ update : { labels : [ { add : label } ] } ,
850+ } ) ;
851+ return result . ok ;
852+ }
853+
854+ async function applyRoadmapLabel (
855+ auth : JiraAuth ,
856+ config : JiraConfig ,
857+ issueKey : string ,
858+ roadmapTitle : string | undefined ,
859+ ) : Promise < void > {
860+ if ( ! config . labelRoadmapSource || ! roadmapTitle ) return ;
861+ const label = sanitizeRoadmapLabel ( roadmapTitle ) ;
862+ if ( ! label ) return ;
863+ const ok = await addLabelToIssue ( auth , issueKey , label ) ;
864+ if ( ! ok ) {
865+ log . jira . warn ( `Failed to apply roadmap label "${ label } " to ${ issueKey } ` ) ;
866+ }
867+ }
868+
832869/**
833870 * Sync a Milestone → Jira Epic
834871 * Creates or updates the Epic in Jira.
835872 */
836- export async function syncMilestone ( milestone : Milestone ) : Promise < void > {
873+ export async function syncMilestone (
874+ milestone : Milestone ,
875+ roadmapTitle ?: string ,
876+ ) : Promise < void > {
837877 const ctx = await resolveConfigAndAuth ( ) ;
838878 if ( ! ctx ?. config . syncRoadmaps ) return ;
839879
@@ -842,9 +882,10 @@ export async function syncMilestone(milestone: Milestone): Promise<void> {
842882 await updateMilestoneSyncStatus ( milestone . id , "pending" ) ;
843883
844884 try {
845- if ( milestone . jiraEpicKey ) {
885+ const existingEpicKey = milestone . jiraEpicKey ;
886+ if ( existingEpicKey ) {
846887 // Update existing
847- const success = await updateIssue ( auth , milestone . jiraEpicKey , {
888+ const success = await updateIssue ( auth , existingEpicKey , {
848889 summary : milestone . title ,
849890 ...( milestone . description
850891 ? {
@@ -855,8 +896,9 @@ export async function syncMilestone(milestone: Milestone): Promise<void> {
855896
856897 if ( success ) {
857898 await updateMilestoneSyncStatus ( milestone . id , "synced" ) ;
899+ await applyRoadmapLabel ( auth , config , existingEpicKey , roadmapTitle ) ;
858900 log . jira . info (
859- `Updated Epic ${ milestone . jiraEpicKey } for milestone ${ milestone . id } ` ,
901+ `Updated Epic ${ existingEpicKey } for milestone ${ milestone . id } ` ,
860902 ) ;
861903 } else {
862904 await updateMilestoneSyncStatus (
@@ -900,6 +942,7 @@ export async function syncMilestone(milestone: Milestone): Promise<void> {
900942 issue . key ,
901943 issue . id ,
902944 ) ;
945+ await applyRoadmapLabel ( auth , config , issue . key , roadmapTitle ) ;
903946 log . jira . info (
904947 `Created Epic ${ issue . key } for milestone ${ milestone . id } ` ,
905948 ) ;
@@ -933,6 +976,7 @@ export async function syncMilestone(milestone: Milestone): Promise<void> {
933976export async function syncInitiative (
934977 initiative : Initiative ,
935978 parentEpicKey ?: string ,
979+ roadmapTitle ?: string ,
936980) : Promise < void > {
937981 const ctx = await resolveConfigAndAuth ( ) ;
938982 if ( ! ctx ?. config . syncRoadmaps ) return ;
@@ -945,7 +989,8 @@ export async function syncInitiative(
945989 config . initiativePriorityMapping [ initiative . priority ] ?? undefined ;
946990
947991 try {
948- if ( initiative . jiraIssueKey ) {
992+ const existingIssueKey = initiative . jiraIssueKey ;
993+ if ( existingIssueKey ) {
949994 // Update existing
950995 const fields : Record < string , unknown > = {
951996 summary : initiative . title ,
@@ -957,12 +1002,13 @@ export async function syncInitiative(
9571002 fields . priority = { id : priorityId } ;
9581003 }
9591004
960- const success = await updateIssue ( auth , initiative . jiraIssueKey , fields ) ;
1005+ const success = await updateIssue ( auth , existingIssueKey , fields ) ;
9611006
9621007 if ( success ) {
9631008 await updateInitiativeSyncStatus ( initiative . id , "synced" ) ;
1009+ await applyRoadmapLabel ( auth , config , existingIssueKey , roadmapTitle ) ;
9641010 log . jira . info (
965- `Updated Story ${ initiative . jiraIssueKey } for initiative ${ initiative . id } ` ,
1011+ `Updated Story ${ existingIssueKey } for initiative ${ initiative . id } ` ,
9661012 ) ;
9671013 } else {
9681014 await updateInitiativeSyncStatus (
@@ -1008,6 +1054,7 @@ export async function syncInitiative(
10081054 issue . key ,
10091055 issue . id ,
10101056 ) ;
1057+ await applyRoadmapLabel ( auth , config , issue . key , roadmapTitle ) ;
10111058 log . jira . info (
10121059 `Created Story ${ issue . key } for initiative ${ initiative . id } ` ,
10131060 ) ;
@@ -1043,6 +1090,7 @@ export async function syncInitiative(
10431090export async function syncWorkflow (
10441091 workflow : Workflow ,
10451092 initiativeJira ?: { key : string ; id ?: string } ,
1093+ roadmapTitle ?: string ,
10461094) : Promise < void > {
10471095 const ctx = await resolveConfigAndAuth ( ) ;
10481096 if ( ! ctx ?. config . syncWorkflows ) return ;
@@ -1072,6 +1120,8 @@ export async function syncWorkflow(
10721120 } else {
10731121 await updateWorkflowSyncStatus ( workflow . id , "synced" ) ;
10741122 }
1123+ // Label the adopted issue (idempotent if initiative sync already labelled it)
1124+ await applyRoadmapLabel ( auth , config , initiativeJira . key , roadmapTitle ) ;
10751125 } else if ( workflow . jiraIssueKey ) {
10761126 // Standalone workflow — update existing Story
10771127 const fields : Record < string , unknown > = {
@@ -1343,6 +1393,7 @@ export async function syncArtifactComment(
13431393export async function syncPulses (
13441394 plan : Plan ,
13451395 parentTaskKey : string ,
1396+ roadmapTitle ?: string ,
13461397) : Promise < void > {
13471398 const ctx = await resolveConfigAndAuth ( ) ;
13481399 if ( ! ctx ?. config . syncWorkflows ) return ;
@@ -1401,6 +1452,8 @@ export async function syncPulses(
14011452 await repos . pulses . updateJiraIssueId ( existingPulse . id , issue . key ) ;
14021453 }
14031454
1455+ await applyRoadmapLabel ( auth , config , issue . key , roadmapTitle ) ;
1456+
14041457 // Assign sub-task to the authenticated user
14051458 try {
14061459 const accountId = await getCurrentUserAccountId ( auth ) ;
0 commit comments