@@ -11,7 +11,7 @@ const { dependencyStatus } = require("./work_queue_graph.cjs");
1111const { appendCommit, newRequest, proposedCommitId, replayTransactions } = require ( "./work_queue_replay.cjs" ) ;
1212const { readCheckedQueue, writeCheckedFiles } = require ( "./work_queue_checked_transport.cjs" ) ;
1313const { withProjectionLocks, projectionNonce } = require ( "./work_queue_issue_coordination.cjs" ) ;
14- const { MAX_PROJECTION_TARGETS , LABELS_PAGE_SIZE , discoverTarget, preflightIssues, mutateIssues, issueResource, ISSUE_SELECTION , issueReadQuery } = require ( "./work_queue_issue_api.cjs" ) ;
14+ const { MAX_PROJECTION_TARGETS , LABELS_PAGE_SIZE , STATUSES , statusLabelName , ensureStatusLabel , discoverTarget, preflightIssues, mutateIssues, issueResource, ISSUE_SELECTION , issueReadQuery } = require ( "./work_queue_issue_api.cjs" ) ;
1515const { wrapGithubClient } = require ( "./work_queue_issue_pacing.cjs" ) ;
1616const { isStagedMode } = require ( "./safe_output_helpers.cjs" ) ;
1717const { validateJournal } = require ( "./work_queue_issue_journal.cjs" ) ;
@@ -20,9 +20,9 @@ const { issueBody, renderSummary, renderClaim, runLink } = require("./work_queue
2020function issuesConfiguration ( value ) {
2121 if ( value === undefined || value === false ) return null ;
2222 if ( value === true ) return { label : "work" } ;
23- closed ( value , [ ] , [ "label" , "status-field" ] , "work-queue issues" ) ;
23+ closed ( value , [ ] , [ "label" ] , "work-queue issues" ) ;
2424 for ( const [ key , field ] of Object . entries ( value ) ) {
25- if ( typeof field !== "string" || ! field . trim ( ) || field . includes ( "${{" ) || Buffer . byteLength ( field ) > 256 || / [ \x00 - \x1f \x7f ] / . test ( field ) )
25+ if ( typeof field !== "string" || ! field . trim ( ) || field . includes ( "${{" ) || Buffer . byteLength ( field ) > 33 || / [ \x00 - \x1f \x7f ] / . test ( field ) )
2626 throw queueError ( "projection_invalid" , `${ SAFE_OUTPUT_E001 } : ${ key } must be a nonblank bounded literal` ) ;
2727 }
2828 return { label : "work" , ...value } ;
@@ -77,7 +77,7 @@ function jobDiagnostics(value) {
7777 . map ( ( [ name , job ] ) => `Native job \`${ name } \`: **${ job . result } **. This does not establish Work cancellation or Result.` ) ;
7878}
7979
80- function summaryBody ( state , work , field , at , diagnostics = [ ] , branch = "work-queue" , run = undefined ) {
80+ function summaryBody ( state , work , at , diagnostics = [ ] , branch = "work-queue" , run = undefined ) {
8181 const lines = [ ...diagnostics ] ;
8282 const claimHistory = [ ] ;
8383 const outcomes = [ ] ;
@@ -112,8 +112,7 @@ function summaryBody(state, work, field, at, diagnostics = [], branch = "work-qu
112112 diagnostics : lines . join ( "\n" ) ,
113113 } ,
114114 origin ,
115- workIssueStatus ( state , work , at ) ,
116- ! ! field
115+ workIssueStatus ( state , work , at )
117116 ) ;
118117}
119118
@@ -207,20 +206,23 @@ async function projectBatch(options, initial, origin, assignment, config, target
207206 const journals = new Map ( ) ;
208207 const pending = [ ] ;
209208 const discovery = new Map ( ) ;
209+ const statusLabels = new Map ( ) ;
210210 for ( const target of targets ) {
211211 const path = journalPath ( target . work_id ) ;
212212 const journal = current . journal . get ( path ) || { version : 1 , work_id : target . work_id , comments : { } } ;
213213 const repository = target . resource ?. repository || origin . repository ;
214214 validateJournal ( journal , current . state , target . work_id , repository ) ;
215215 journals . set ( path , structuredClone ( journal ) ) ;
216+ const work = current . state . works . get ( target . work_id ) ;
216217 if ( ! discovery . has ( repository ) ) discovery . set ( repository , await discoverTarget ( options . githubClient , repository , config , current . nativeResponse ?. [ issueRead . repositoryAliases . get ( repository ) ] ) ) ;
217- if ( discovery . get ( repository ) . fieldPending ) pending . push ( { work_id : target . work_id , reason : `queue committed; field sync pending: ${ discovery . get ( repository ) . fieldPending } ` } ) ;
218+ const status = workIssueStatus ( current . state , work , options . now ?? Date . now ( ) ) ;
219+ const statusKey = `${ repository } :${ status } ` ;
220+ if ( ! statusLabels . has ( statusKey ) ) statusLabels . set ( statusKey , await ensureStatusLabel ( options . githubClient , discovery . get ( repository ) . repositoryId , config , status ) ) ;
218221 if ( ! target . resource && journal . create ?. resource ) {
219222 assertProjectionAuthority ( current . state , journal . create . origin , target . work_id , journal . create . ref , journal . create . resource . repository , journal . create . authority_claim_id ) ;
220223 target . resource = journal . create . resource ;
221224 }
222225 target . comments = { } ;
223- const work = current . state . works . get ( target . work_id ) ;
224226 if ( work . issue_summary ) target . comments . summary = work . issue_summary ;
225227 for ( const claimId of target . claim_ids ) {
226228 const claim = current . state . claims . get ( claimId ) ;
@@ -231,7 +233,7 @@ async function projectBatch(options, initial, origin, assignment, config, target
231233 }
232234 }
233235 const samePreflight = targets . every ( ( target , index ) => canonical ( target . resource || null ) === canonical ( preflight [ index ] . resource || null ) && canonical ( target . comments ) === canonical ( preflight [ index ] . comments ) ) ;
234- const issues = await preflightIssues ( options . githubClient , targets , { fields : ! ! config [ "status-field" ] , response : samePreflight ? current . nativeResponse : undefined } ) ;
236+ const issues = await preflightIssues ( options . githubClient , targets , { response : samePreflight ? current . nativeResponse : undefined } ) ;
235237 for ( const target of targets ) {
236238 const receipt = journals . get ( journalPath ( target . work_id ) ) . create ;
237239 if ( receipt ?. node_id && receipt . node_id !== issues . get ( target . work_id ) ?. id ) throw queueError ( "projection_journal_invalid" , "native Issue receipt node identity disagrees with preflight" ) ;
@@ -265,10 +267,9 @@ async function projectBatch(options, initial, origin, assignment, config, target
265267 repositoryId : discovered . repositoryId ,
266268 title : `Work ${ work . work_id } ` ,
267269 body : issueBody ( current . state , work , origin , config , current . branch ) ,
268- labelIds : [ discovered . labelId ] ,
269- ...( discovered . field ? { issueFields : [ { fieldId : discovered . field . id , singleSelectOptionId : discovered . field . options . get ( status ) } ] } : { } ) ,
270+ labelIds : [ discovered . labelId , statusLabels . get ( `${ origin . repository } :${ status } ` ) ] ,
270271 } ,
271- selection : `issue { ${ config [ "status-field" ] ? ISSUE_SELECTION : ISSUE_SELECTION . replace ( / i s s u e F i e l d V a l u e s [ \s \S ] * $ / , "" ) } }` ,
272+ selection : `issue { ${ ISSUE_SELECTION } }` ,
272273 } ) ;
273274 }
274275 if ( creates . length ) {
@@ -300,16 +301,9 @@ async function projectBatch(options, initial, origin, assignment, config, target
300301 journal . create . resource = resource ;
301302 journal . create . node_id = issue . id ;
302303 issues . set ( target . work_id , { ...issue , comments : new Map ( ) } ) ;
303- const discovered = discovery . get ( resource . repository ) ;
304- if (
305- discovered . field &&
306- ( ! issue . viewerCanSetFields ||
307- issue . issueFieldValues ?. pageInfo ?. hasNextPage !== false ||
308- ! issue . issueFieldValues . nodes ?. some (
309- value => value . field ?. id === discovered . field . id && value . optionId === discovered . field . options . get ( workIssueStatus ( current . state , current . state . works . get ( target . work_id ) , options . now ?? Date . now ( ) ) )
310- ) )
311- )
312- pending . push ( { work_id : target . work_id , reason : "queue committed; field sync pending: initial field application was not verified" } ) ;
304+ const status = workIssueStatus ( current . state , current . state . works . get ( target . work_id ) , options . now ?? Date . now ( ) ) ;
305+ if ( issue . labels ?. pageInfo ?. hasNextPage !== false || ! issue . labels . nodes ?. some ( label => label . id === statusLabels . get ( `${ resource . repository } :${ status } ` ) ) )
306+ pending . push ( { work_id : target . work_id , reason : "queue committed; initial status label application was not verified" } ) ;
313307 operations . push ( bindingOperation ( target , origin . ref , resource ) ) ;
314308 }
315309 current = await publishProjection ( options , current , actor , journals , operations ) ;
@@ -326,12 +320,12 @@ async function projectBatch(options, initial, origin, assignment, config, target
326320 if ( canonical ( work . backing_issue || work . issue_link || null ) !== canonical ( target . resource ) ) throw queueError ( "projection_scope_changed" , "new native receipt has no checked backing binding" ) ;
327321 }
328322 }
329- return projectLinkedIssues ( options , current , actor , origin , config , targets , journals , issues , discovery , pending , creates . length > 0 ) ;
323+ return projectLinkedIssues ( options , current , actor , origin , config , targets , journals , issues , discovery , statusLabels , pending , creates . length > 0 ) ;
330324 } ;
331325 return resources . length ? ( options . withProjectionLocks || withProjectionLocks ) ( { ...options , repositoryId : current . repositoryId , head : current . sha , keys : resources } , project ) : project ( ) ;
332326}
333327
334- async function projectLinkedIssues ( options , current , actor , origin , config , targets , journals , issues , discovery , pending , created ) {
328+ async function projectLinkedIssues ( options , current , actor , origin , config , targets , journals , issues , discovery , statusLabels , pending , created ) {
335329 const edits = [ ] ;
336330 const newComments = [ ] ;
337331 const bindings = [ ] ;
@@ -342,33 +336,30 @@ async function projectLinkedIssues(options, current, actor, origin, config, targ
342336 const journal = journals . get ( journalPath ( target . work_id ) ) ;
343337 assertProjectionAuthority ( current . state , actor , work . work_id , origin . ref , target . resource . repository , target . authority_claim_id ) ;
344338 const discovered = discovery . get ( target . resource . repository ) ;
345- if ( ! issue . labels . nodes . some ( label => label . id === discovered . labelId ) )
339+ const status = workIssueStatus ( current . state , work , options . now ?? Date . now ( ) ) ;
340+ const desiredStatusId = statusLabels . get ( `${ target . resource . repository } :${ status } ` ) ;
341+ const missing = [ discovered . labelId , desiredStatusId ] . filter ( id => ! issue . labels . nodes . some ( label => label . id === id ) ) ;
342+ if ( missing . length )
346343 edits . push ( {
347344 key : target . work_id ,
348345 name : "addLabelsToLabelable" ,
349346 type : "AddLabelsToLabelableInput" ,
350- input : { labelableId : issue . id , labelIds : [ discovered . labelId ] } ,
347+ input : { labelableId : issue . id , labelIds : missing } ,
351348 selection : `labelable { id ... on Issue { labels(first:${ LABELS_PAGE_SIZE } ) { nodes { id } pageInfo { hasNextPage } } } }` ,
349+ expectedLabels : missing ,
350+ } ) ;
351+ const obsolete = issue . labels . nodes . filter ( label => STATUSES . some ( candidate => candidate !== status && label . name === statusLabelName ( config , candidate ) ) ) . map ( label => label . id ) ;
352+ if ( obsolete . length )
353+ edits . push ( {
354+ key : target . work_id ,
355+ name : "removeLabelsFromLabelable" ,
356+ type : "RemoveLabelsFromLabelableInput" ,
357+ input : { labelableId : issue . id , labelIds : obsolete } ,
358+ selection : `labelable { id ... on Issue { labels(first:${ LABELS_PAGE_SIZE } ) { nodes { id } pageInfo { hasNextPage } } } }` ,
359+ removedLabels : obsolete ,
352360 } ) ;
353- const status = workIssueStatus ( current . state , work , options . now ?? Date . now ( ) ) ;
354- if ( discovered . field ) {
355- if ( ! issue . viewerCanSetFields ) {
356- pending . push ( { work_id : target . work_id , reason : "queue committed; field sync pending: projector token cannot set fields" } ) ;
357- } else if ( ! issue . issueFieldValues . nodes . some ( value => value . field ?. id === discovered . field . id && value . optionId === discovered . field . options . get ( status ) ) ) {
358- edits . push ( {
359- key : target . work_id ,
360- name : "setIssueFieldValue" ,
361- type : "SetIssueFieldValueInput" ,
362- input : { issueId : issue . id , issueFields : [ { fieldId : discovered . field . id , singleSelectOptionId : discovered . field . options . get ( status ) } ] } ,
363- selection : `issue { id issueFieldValues(first:${ MAX_PROJECTION_TARGETS } ) { nodes { ... on IssueFieldSingleSelectValue { optionId field { ... on IssueFieldSingleSelect { id } } } } pageInfo { hasNextPage } } }` ,
364- expected : { field : discovered . field . id , option : discovered . field . options . get ( status ) } ,
365- } ) ;
366- }
367- }
368361 const diagnostics = jobDiagnostics ( options . jobResults ?? process . env . GH_AW_WORK_QUEUE_JOB_RESULTS ) ;
369- if ( discovered . fieldPending ) diagnostics . push ( `Native status field sync pending: ${ discovered . fieldPending } .` ) ;
370- else if ( discovered . field && ! issue . viewerCanSetFields ) diagnostics . push ( "Native status field sync pending: projector token cannot set fields." ) ;
371- const bodies = new Map ( [ [ "summary" , summaryBody ( current . state , work , ! ! config [ "status-field" ] , options . now ?? Date . now ( ) , diagnostics , current . branch , origin ) ] ] ) ;
362+ const bodies = new Map ( [ [ "summary" , summaryBody ( current . state , work , options . now ?? Date . now ( ) , diagnostics , current . branch , origin ) ] ] ) ;
372363 for ( const claimId of target . claim_ids ) bodies . set ( claimId , claimBody ( current . state , current . state . claims . get ( claimId ) , options . now ?? Date . now ( ) , diagnostics ) ) ;
373364 for ( const [ key , body ] of bodies ) {
374365 const comment = issue . comments . get ( key ) ;
@@ -416,17 +407,14 @@ async function projectLinkedIssues(options, current, actor, origin, config, targ
416407 recordUncertainReceipt ( result , batch , pending , "Issue closure returned no exact native receipt" ) ;
417408 continue ;
418409 }
419- if ( result . operation . name === "addLabelsToLabelable" && ( result . value . labelable ?. id !== result . operation . input . labelableId || ! result . value . labelable ?. labels ?. nodes ?. some ( label => label . id === result . operation . input . labelIds [ 0 ] ) ) )
420- pending . push ( { work_id : result . operation . key , reason : "queue committed; tracking label application was not verified" } ) ;
421- if ( result . operation . expected ) {
422- const values = result . value . issue ?. issueFieldValues ;
423- if (
424- result . value . issue ?. id !== result . operation . input . issueId ||
425- values ?. pageInfo ?. hasNextPage !== false ||
426- ! values . nodes ?. some ( value => value . field ?. id === result . operation . expected . field && value . optionId === result . operation . expected . option )
427- )
428- pending . push ( { work_id : result . operation . key , reason : "queue committed; field sync pending: mutation did not apply the selected option" } ) ;
429- }
410+ if (
411+ ( result . operation . expectedLabels || result . operation . removedLabels ) &&
412+ ( result . value . labelable ?. id !== result . operation . input . labelableId ||
413+ result . value . labelable ?. labels ?. pageInfo ?. hasNextPage !== false ||
414+ result . operation . expectedLabels ?. some ( id => ! result . value . labelable . labels . nodes . some ( label => label . id === id ) ) ||
415+ result . operation . removedLabels ?. some ( id => result . value . labelable . labels . nodes . some ( label => label . id === id ) ) )
416+ )
417+ pending . push ( { work_id : result . operation . key , reason : "queue committed; status label synchronization was not verified" } ) ;
430418 if ( result . operation . handle ) {
431419 const node = result . value . commentEdge ?. node ;
432420 const target = targets . find ( target => target . work_id === result . operation . key ) ;
0 commit comments