agents-relay 2.0.67 → 2.0.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -20
- package/dist/adapters.js +39 -3
- package/dist/blockers.js +17 -4
- package/dist/cli-blocker.js +12 -30
- package/dist/cli-help.js +22 -17
- package/dist/cli-job-create.js +77 -0
- package/dist/cli-local-merge-config.js +11 -0
- package/dist/cli-projection.js +23 -55
- package/dist/cli-review.js +7 -24
- package/dist/cli-support.js +81 -8
- package/dist/cli-worker-task.js +63 -0
- package/dist/cli.js +223 -185
- package/dist/dashboard-html.js +15 -12
- package/dist/dashboard.js +27 -2
- package/dist/e2e-injection.js +76 -0
- package/dist/github-auth.js +4 -3
- package/dist/github-binding-order.js +15 -0
- package/dist/github-client.js +7 -2
- package/dist/github-pr-archive.js +22 -0
- package/dist/github-webhook.js +43 -10
- package/dist/job-actions.js +29 -0
- package/dist/local-authority-store.js +288 -0
- package/dist/local-commit-events.js +2 -0
- package/dist/local-git-merge.js +124 -0
- package/dist/local-job-path.js +7 -0
- package/dist/local-job-registry.js +29 -0
- package/dist/local-managed-store.js +21 -0
- package/dist/local-merge-delivery.js +53 -0
- package/dist/local-merge-lock.js +103 -0
- package/dist/local-merge-projection.js +60 -0
- package/dist/managed-worker-input.js +13 -6
- package/dist/manual-plan.js +29 -7
- package/dist/no-github.js +11 -0
- package/dist/planned-review-head.js +7 -8
- package/dist/planner-runtime.js +182 -41
- package/dist/planner.js +35 -2
- package/dist/pool.js +89 -136
- package/dist/projected-store.js +20 -3
- package/dist/projection-outbox.js +3 -1
- package/dist/publication-store.js +37 -0
- package/dist/reconciler-cancellation.js +21 -0
- package/dist/reconciler-review.js +99 -29
- package/dist/reconciler-runtime.js +72 -35
- package/dist/reconciler.js +124 -85
- package/dist/relay-store.js +108 -6
- package/dist/relayd-control.js +184 -0
- package/dist/relayd-dashboard-actions.js +108 -0
- package/dist/relayd-utilities.js +107 -0
- package/dist/relayd.js +304 -299
- package/dist/review-generation-fence.js +12 -0
- package/dist/runtime-status-events.js +21 -0
- package/dist/runtime-status.js +21 -7
- package/dist/scheduled-jobs.js +43 -36
- package/dist/service-external-actions.js +27 -0
- package/dist/service-publication.js +29 -0
- package/dist/store-github.js +17 -6
- package/dist/task-actions.js +59 -1
- package/dist/task-update.js +1 -1
- package/dist/troubleshooter.js +21 -2
- package/dist/unblock.js +47 -15
- package/dist/worker-rejection.js +11 -0
- package/dist/workflow/contracts.js +10 -1
- package/dist/workflow/definitions-v2.js +2 -0
- package/dist/workflow/job-reconcile-v2.js +9 -60
- package/dist/workflow/runtime.js +18 -3
- package/dist/workflow-effect-failures.js +30 -0
- package/dist/workspace.js +95 -6
- package/package.json +6 -3
- package/skills/agents-relay/agents/planner.agent.md +3 -0
- package/skills/agents-relay/contracts/orchestrator-workflows.md +4 -7
- package/workflow/job-lifecycle.mmd +19 -60
- package/workflow/review-lifecycle.mmd +5 -8
package/dist/dashboard-html.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
const
|
|
3
|
-
export const dashboardVersion = packageVersion;
|
|
2
|
+
export const dashboardVersion = createRequire(import.meta.url)('../package.json').version;
|
|
4
3
|
export const dashboardHtml = `<!doctype html>
|
|
5
4
|
<html lang="en" data-dashboard-contract="safe-dom,named-relay-events,snapshot-events,bounded-event-history">
|
|
6
5
|
<head>
|
|
@@ -87,7 +86,11 @@ export const dashboardHtml = `<!doctype html>
|
|
|
87
86
|
.agent-arrow { color:var(--muted); font-weight:800; }
|
|
88
87
|
.agent-graph-source { margin:.1rem 0 0; padding:.45rem; border-radius:.35rem; background:var(--surface); color:var(--muted); font:.7rem/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; white-space:pre; overflow:auto; }
|
|
89
88
|
.event-list { display:grid; gap:.45rem; }
|
|
90
|
-
.event-item { padding
|
|
89
|
+
.event-item { padding:.34rem .42rem .42rem; border-left:3px solid transparent; border-bottom:1px solid var(--border); border-radius:.28rem; }
|
|
90
|
+
.event-item.event-error { border-left-color:var(--bad); background:#fff4f4; }
|
|
91
|
+
.event-item.event-error .event-kind,.event-item.event-error .event-message { color:var(--bad); }
|
|
92
|
+
.event-item.event-warn { border-left-color:var(--warn); background:#fff9eb; }
|
|
93
|
+
.event-item.event-warn .event-kind { color:var(--warn); }
|
|
91
94
|
.event-meta { display:flex; flex-wrap:wrap; gap:.4rem; color:var(--muted); font-size:.68rem; }
|
|
92
95
|
.event-kind { color:var(--accent); font-weight:800; }
|
|
93
96
|
.event-message { margin:.12rem 0 0; overflow-wrap:anywhere; }
|
|
@@ -176,7 +179,6 @@ export const dashboardHtml = `<!doctype html>
|
|
|
176
179
|
.tagline { display:none; }
|
|
177
180
|
}
|
|
178
181
|
@media (prefers-reduced-motion:reduce) { .skeleton { animation:none; } .usage-fill { transition:none; } }
|
|
179
|
-
|
|
180
182
|
.router-rules-workspace{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;align-items:start;min-width:0}.router-rules-pane{min-width:0;max-width:100%;overflow:hidden}.rules-editor .rules-textarea{min-height:420px}.audit-log{display:flex;flex-direction:column;gap:8px;max-height:500px;overflow:auto;min-width:0}.audit-entry{border:1px solid var(--border);border-radius:10px;padding:10px;background:var(--surface)}.audit-entry-head{display:flex;justify-content:space-between;gap:8px;align-items:center}.audit-entry pre{white-space:pre-wrap;word-break:break-word;margin:8px 0 0;font-size:11px}.router-test-panel{margin-top:16px;border-top:1px solid var(--border);padding-top:12px}.router-test-results{margin-top:8px;display:flex;flex-direction:column;gap:6px;max-height:480px;overflow:auto;min-width:0}.router-test-results pre{white-space:pre;overflow:auto;max-width:100%;font-size:11px;line-height:1.45;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:12px}.router-test-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px;padding:7px 0;border-bottom:1px solid var(--border)}@media(max-width:900px){.router-rules-workspace{grid-template-columns:1fr}.router-rules-pane{max-height:none}.router-test-results{max-height:420px}}@media(max-width:650px){#rules-view .panel-head .subtle{display:none}#rules-view .panel-body{max-height:none;overflow:visible;padding:.6rem}#rules-view .router-rules-workspace{gap:.9rem}#rules-view .rules-help{font-size:.74rem;line-height:1.4}#rules-view .rules-textarea{min-height:52vh;padding:.65rem;font-size:12px}#rules-view .rules-footer{align-items:stretch}#rules-view .rules-footer>.subtle{width:100%}#rules-view .rules-footer>div{width:100%;display:grid;grid-template-columns:1fr 1fr;gap:.5rem}#rules-view .rules-save{min-height:44px;width:100%;padding:.55rem .7rem}#rules-view .router-rules-pane>.panel-head{padding:.55rem 0}#rules-view .audit-log{max-height:none}#rules-view .audit-entry-head{align-items:flex-start}#rules-view .audit-entry-head .subtle{display:block;flex:0 0 auto}#rules-view .router-test-panel{min-width:0;max-width:100%;overflow:hidden}#rules-view .router-test-results{max-height:none;width:100%;max-width:100%;overflow-x:hidden}#rules-view .router-test-results>*{min-width:0;max-width:100%}#rules-view .router-test-results pre{width:100%;box-sizing:border-box;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-size:11px;padding:.65rem}#rules-view .router-test-row{grid-template-columns:1fr}}
|
|
181
183
|
</style>
|
|
182
184
|
</head>
|
|
@@ -263,7 +265,6 @@ export const dashboardHtml = `<!doctype html>
|
|
|
263
265
|
// link.textContent = 'PR #' + job.prNumber;
|
|
264
266
|
// while (eventFeed.children.length > MAX_EVENTS)
|
|
265
267
|
// function usageBar(label, window)
|
|
266
|
-
|
|
267
268
|
const MAX_EVENTS=60;
|
|
268
269
|
const DASHBOARD_VERSION=${JSON.stringify(dashboardVersion)};
|
|
269
270
|
const jobsTab=document.querySelector('#jobs-tab');
|
|
@@ -325,7 +326,6 @@ let eventTaskId=selectedTaskId;
|
|
|
325
326
|
const collapsedTasks=new Set();
|
|
326
327
|
const expandedTasks=new Set();
|
|
327
328
|
const displayedEventIds=new Set();
|
|
328
|
-
|
|
329
329
|
function value(input){return input===null||input===undefined||input===''?'—':String(input);}
|
|
330
330
|
function text(input){return document.createTextNode(value(input));}
|
|
331
331
|
function element(name,className,child){const node=document.createElement(name);if(className)node.className=className;if(child)node.append(child);return node;}
|
|
@@ -345,7 +345,7 @@ function cadence(job){const schedule=job?.schedule;if(!schedule?.cron)return '
|
|
|
345
345
|
function safePrUrl(repository,prNumber){if(typeof repository!=='string'||!Number.isInteger(prNumber)||prNumber<=0)return null;const parts=repository.split('/');if(parts.length!==2)return null;const [owner,repo]=parts;if(!owner||!repo||owner==='.'||owner==='..'||repo==='.'||repo==='..')return null;if(!/^[A-Za-z0-9-]+$/.test(owner)||!/^[A-Za-z0-9._-]+$/.test(repo))return null;return 'https://github.com/'+owner+'/'+repo+'/pull/'+prNumber;}
|
|
346
346
|
function safeCommitUrl(repository,sha){if(typeof repository!=='string'||typeof sha!=='string'||!/^[0-9a-f]{7,64}$/i.test(sha))return null;const parts=repository.split('/');if(parts.length!==2||!parts[0]||!parts[1])return null;return 'https://github.com/'+parts[0]+'/'+parts[1]+'/commit/'+sha;}
|
|
347
347
|
function commitField(label,job,sha){const box=element('div','detail-group');box.append(element('p','detail-label',text(label)));const line=element('p','detail-value');const url=safeCommitUrl(job?.repository,sha);if(url){const link=document.createElement('a');link.href=url;link.target='_blank';link.rel='noopener noreferrer';link.textContent=sha.slice(0,7);line.append(link);}else line.append(text(sha||'—'));box.append(line);return box;}
|
|
348
|
-
function prField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('GitHub PR')));const line=element('p','detail-value');const url=safePrUrl(job.repository,
|
|
348
|
+
function prField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('GitHub PR')));const line=element('p','detail-value');const prNumber=Number(job.externalPrNumber||job.prNumber||0);const url=safePrUrl(job.repository,prNumber);if(url){const link=document.createElement('a');link.href=url;link.target='_blank';link.rel='noopener noreferrer';link.textContent='PR #'+prNumber;line.append(link);}else line.append(text('unavailable'));box.append(line);return box;}
|
|
349
349
|
function chatField(task){const box=element('div','detail-group');box.append(element('p','detail-label',text('Chat')));const line=element('p','detail-value');if(task.adapter==='chatgpt'&&typeof task.threadId==='string'&&task.threadId){const link=document.createElement('a');link.href='https://chatgpt.com/c/'+encodeURIComponent(task.threadId);link.target='_blank';link.rel='noopener noreferrer';link.textContent='Open chat';line.append(link);}else line.append(text('—'));box.append(line);return box;}
|
|
350
350
|
function taskCommentField(task){const box=element('div','detail-group');box.append(element('p','detail-label',text('PR comment')));const line=element('p','detail-value');if(typeof task.taskUrl==='string'&&task.taskUrl.startsWith('https://github.com/')){const link=document.createElement('a');link.className='task-link';link.href=task.taskUrl;link.target='_blank';link.rel='noopener noreferrer';link.textContent='Open task comment';line.append(link);}else line.append(text('—'));box.append(line);return box;}
|
|
351
351
|
function parseAgentGraphSource(source){if(typeof source!=='string'||!source.trim())return null;const agents=new Map(),edges=[];for(const raw of source.split(/\\r?\\n/)){const line=raw.trim();if(!line||line.startsWith('%%')||line.startsWith('flowchart '))continue;let match=line.match(/^([A-Za-z_][A-Za-z0-9_.-]*)(?:\\[agent:([A-Za-z_][A-Za-z0-9_.-]*)\\])?\\s*-->\\s*([A-Za-z_][A-Za-z0-9_.-]*)(?:\\[agent:([A-Za-z_][A-Za-z0-9_.-]*)\\])?$/);if(match){agents.set(match[1],match[2]||agents.get(match[1])||match[1]);agents.set(match[3],match[4]||agents.get(match[3])||match[3]);edges.push([match[1],match[3]]);continue;}match=line.match(/^([A-Za-z_][A-Za-z0-9_.-]*)\\[agent:([A-Za-z_][A-Za-z0-9_.-]*)\\]$/);if(match){agents.set(match[1],match[2]);continue;}return null;}const incoming=new Map([...agents.keys()].map(id=>[id,0]));for(const [,to] of edges)incoming.set(to,(incoming.get(to)||0)+1);const remaining=new Map(incoming),waves=[];let ready=[...remaining].filter(([,degree])=>degree===0).map(([id])=>id).sort();while(ready.length){waves.push(ready);const next=[];for(const id of ready)for(const [from,to] of edges)if(from===id){const degree=(remaining.get(to)||0)-1;remaining.set(to,degree);if(degree===0)next.push(to);}ready=[...new Set(next)].sort();}return {agents,waves};}
|
|
@@ -388,7 +388,7 @@ function jobChatRef(job){return repoName(job?.repository)+'/pr#'+value(job?.prNu
|
|
|
388
388
|
function chatShortcutUrl(prompt){return 'https://chatgpt.com/?prompt='+encodeURIComponent(prompt);}
|
|
389
389
|
const CHAT_SHORTCUT_TARGET='agents-relay-shortcut';
|
|
390
390
|
function chatShortcutField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('ChatGPT shortcut')));const select=document.createElement('select');select.className='job-filter job-chat-shortcut-select';select.setAttribute('aria-label','ChatGPT shortcut');select.append(new Option('',''));for(const item of parseChatShortcuts(chatShortcutText()))select.append(new Option(item.name,item.prompt));select.value='';select.addEventListener('change',()=>{if(!select.value)return;const prompt=select.value.replaceAll('%1',jobChatRef(job));window.open(chatShortcutUrl(prompt),CHAT_SHORTCUT_TARGET,'noopener');select.value='';});box.append(select);return box;}
|
|
391
|
-
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected repo.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));row.addEventListener('click',()=>selectJob(job));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job);});select.append(element('span','job-title',text(repoName(job.repository)+'/PR#'+value(job.prNumber)+': '+value(job.title||job.id))));const states=element('span','job-status-line');states.append(element('span','badge '+value(job.state),text(value(job.state))),element('span','badge '+value(job.runtimeStatus||'IDLE'),text(value(job.runtimeStatus||'IDLE'))));if(job.
|
|
391
|
+
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected repo.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));row.addEventListener('click',()=>selectJob(job));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job);});select.append(element('span','job-title',text(repoName(job.repository)+'/'+(Number(job.externalPrNumber||job.prNumber||0)>0?'PR#'+value(job.externalPrNumber||job.prNumber):'Job '+value(job.id))+': '+value(job.title||job.id))));const states=element('span','job-status-line');states.append(element('span','badge '+value(job.state),text(value(job.state))),element('span','badge '+value(job.runtimeStatus||'IDLE'),text(value(job.runtimeStatus||'IDLE'))));if(job.publicationFailure)states.append(element('span','badge FAILED',text('PUBLISH FAILED: '+value(job.publicationFailure.stage))));else if(job.publication?.version)states.append(element('span','badge SUCCEEDED',text('v'+job.publication.version)));else if(Number(job.externalPrNumber||0)>0&&job.githubState==='MERGED')states.append(element('span','badge MERGED',text('PR Merged')));else if(job.state==='COMPLETED'&&job.githubState==='CLOSED')states.append(element('span','badge SUCCEEDED',text('PR Archived')));if(job.executionMode==='autonomous')states.append(element('span','badge flag-autonomous',text('AUTONOMOUS')));if(job.state==='ACTIVE'&&job.reviewEnabled!==false)states.append(element('span','badge flag-review',text('REVIEW REQUIRED')));if(isCompleteUnmerged(job))states.append(element('span','badge MERGE_PENDING',text('MERGE PENDING')));const primary=element('div','job-primary');primary.append(states);const lastLine=element('div','job-last-line');lastLine.append(element('span','subtle',text(compactCounts(counts(job.tasks)))),element('span','subtle',text(relativeTime(job.updatedAt))));content.append(select,primary,lastLine);row.append(content);jobList.append(element('li','',row));}}
|
|
392
392
|
function taskTagDisplay(job,task){const display=task.tag&&job?.tag?.values?.[task.tag];return typeof display==='string'&&display?display:null;}
|
|
393
393
|
function taskSubtreeComplete(task,children){return ['SUCCEEDED','CANCELLED'].includes(task.state)&&(children.get(task.id)||[]).every(child=>taskSubtreeComplete(child,children));}
|
|
394
394
|
function taskNode(task,children,depth){const li=element('li');const nested=children.get(task.id)||[];const line=element('div','task-line');if(nested.length){const key=(selectedJob||'')+':'+task.id;const collapsed=collapsedTasks.has(key)||(!expandedTasks.has(key)&&taskSubtreeComplete(task,children));const fold=document.createElement('button');fold.type='button';fold.className='task-fold';fold.setAttribute('aria-expanded',String(!collapsed));fold.setAttribute('aria-label',(collapsed?'Expand ':'Collapse ')+task.id);fold.textContent=collapsed?'▸':'▾';line.append(fold);const ul=element('ul','tree');ul.hidden=collapsed;for(const child of nested)ul.append(taskNode(child,children,depth+1));fold.addEventListener('click',()=>{const next=!ul.hidden;ul.hidden=next;fold.setAttribute('aria-expanded',String(!next));fold.setAttribute('aria-label',(next?'Expand ':'Collapse ')+task.id);fold.textContent=next?'▸':'▾';if(next){collapsedTasks.add(key);expandedTasks.delete(key);}else{collapsedTasks.delete(key);expandedTasks.add(key);}});li.append(line,ul);}else li.append(line);const row=document.createElement('button');row.type='button';row.className='task-row';row.setAttribute('aria-current',String(task.id===selectedTaskId));row.title=task.id;row.addEventListener('click',()=>{const deselect=selectedTaskId===task.id;selectedTaskId=deselect?null:task.id;eventTaskId=deselect?null:task.id;rememberSelection();clearEvents();renderAll();void loadEventHistory();});const id=element('span','task-id',text(task.id));id.style.paddingLeft=Math.min(depth,4)*6+'px';row.append(id,element('span','badge '+value(task.state),text(task.state)));const tag=taskTagDisplay(selected(),task);if(tag){const badge=element('span','task-tag',text(tag));badge.title=(selected()?.tag?.description||'Task tag')+': '+task.tag;row.append(badge);}line.append(row);return li;}
|
|
@@ -424,7 +424,8 @@ async function refreshStatus(){try{renderRateLimitStatus(await loadJSON('/api/st
|
|
|
424
424
|
async function refresh(force=false){jobsRefresh.disabled=true;jobsRefresh.textContent='Refreshing…';try{const discovered=await loadJSON('/api/jobs'+(force?'?refresh=1':''));jobs=Array.isArray(discovered)?discovered:[];void drawRuntimeSummary();jobs.sort(compareJobs);if(selectedJob&&!jobForSelection(selectedJob)){selectedJob=null;selectedTaskId=null;}if(!selectedJob){const match=location.hash.match(/^#\\/jobs\\/(.+)$/);const fromHash=match?decodeURIComponent(match[1]):null;const fromRoute=jobForSelection(fromHash);selectedJob=jobSelectionKey(fromRoute||jobs[0]);}rememberSelection();renderAll();void loadEventHistory();connection.textContent='';connection.setAttribute('aria-label','Live durable state connected');connection.className='status-pill ok';}catch(error){connection.textContent='';connection.setAttribute('aria-label','Durable state unavailable');connection.className='status-pill bad';jobList.replaceChildren(element('li','error-state',text('Could not load jobs: '+error.message)));}finally{jobsRefresh.disabled=false;jobsRefresh.textContent='Refresh';}}
|
|
425
425
|
function renderEventScope(){const task=eventTaskId&&selected()?.tasks?.find(candidate=>candidate.id===eventTaskId);eventsAllTasks.disabled=!task;eventsAllTasks.setAttribute('aria-pressed',String(!task));eventsScope.textContent=task?'Task '+task.id+' · newest first · 60 max':'Job-wide · newest first · 60 max';}
|
|
426
426
|
function eventLabel(type){return ({'task.process.launched':'Process launched','task.started':'Worker started','task.process.exited':'Sync process exited','task.process.async_exited':'Async process exited','task.completed':'Worker completed','task.failed':'Worker failed','task.blocked':'Worker blocked','review.started':'Review started','review.approved':'Review approved','review.changes_required':'Review changes required','review.blocked':'Review blocked'})[type]||type||'Event';}
|
|
427
|
-
function
|
|
427
|
+
function eventSeverity(event,level){const type=String(event?.type||'').toLowerCase(),eventLevel=String(event?.level||level||'').toLowerCase(),outcome=String(event?.outcome||event?.status||'').toLowerCase();if(eventLevel==='error'||outcome==='failed'||type.endsWith('.failed')||type==='task.failed')return 'error';if(eventLevel==='warn'||outcome==='blocked'||type.endsWith('.blocked')||type==='task.blocked'||type==='review.blocked')return 'warn';return 'normal';}
|
|
428
|
+
function addEvent(event,level){const current=selected();if(!event||current&&event.job_id&&event.job_id!==current.id||eventTaskId&&event.task_id!==eventTaskId)return;if(event.event_id&&displayedEventIds.has(event.event_id))return;if(event.event_id)displayedEventIds.add(event.event_id);const severity=eventSeverity(event,level);const item=element('li','event-item'+(severity==='error'?' event-error':severity==='warn'?' event-warn':''));const meta=element('div','event-meta');meta.append(element('span','event-kind',text(eventLabel(event.type))),element('span','event-type',text(event.type||'event')),element('span','',text(relativeTime(event.timestamp))),element('span','',text('task: '+value(event.task_id))));item.append(meta);const message=element('p','event-message',text(event.message||'Event received'));item.append(message);eventFeed.querySelector('.empty')?.remove();eventFeed.prepend(item);while(eventFeed.children.length>MAX_EVENTS)eventFeed.lastElementChild?.remove();}
|
|
428
429
|
function resetText(windowData){if(!Number.isFinite(windowData.resetsAt))return '';const at=new Date(windowData.resetsAt*1000);const minutes=Math.max(0,Math.ceil((at.getTime()-Date.now())/60000));const relative=minutes<60?minutes+'m':minutes<1440?Math.floor(minutes/60)+'h '+minutes%60+'m':Math.floor(minutes/1440)+'d '+Math.floor((minutes%1440)/60)+'h';return 'Resets '+at.toLocaleString()+' · in '+relative;}
|
|
429
430
|
function usageBar(label,windowData){const percent=Math.max(0,Math.min(100,100-windowData.usedPercent));const row=element('div','usage-row');const heading=element('div','usage-label');heading.append(element('span','',text(label)),element('span','',text(Math.round(percent)+'%')));const track=element('div','usage-track');track.setAttribute('role','progressbar');track.setAttribute('aria-label',label+' available');track.setAttribute('aria-valuemin','0');track.setAttribute('aria-valuemax','100');track.setAttribute('aria-valuenow',String(percent));const fill=element('div','usage-fill');fill.style.width=percent+'%';track.append(fill);row.append(heading,track);const reset=resetText(windowData);if(reset)row.append(element('div','subtle',text(reset)));return row;}
|
|
430
431
|
function usageProvider(provider){const card=element('section','usage-provider');card.append(element('h3','',text(provider.label)));const usage=provider.usage||{};if(!usage.available||!usage.fiveHour||!usage.weekly){card.append(element('p','empty',text(usage.reason||'Account usage telemetry is unavailable.')));return card;}const bars=element('div','usage-bars');bars.append(usageBar('5-hour remaining',usage.fiveHour),usageBar('Weekly remaining',usage.weekly));card.append(bars);return card;}
|
|
@@ -481,12 +482,14 @@ auditRefresh.addEventListener('click',()=>void drawAudit());
|
|
|
481
482
|
shortcutsSave.addEventListener('click',saveShortcuts);
|
|
482
483
|
window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const views=['jobs','usage','graphs','rules','shortcuts'];const current=location.hash==='#/usage'?'usage':location.hash==='#/graphs'?'graphs':location.hash==='#/execution-router-rules'?'rules':location.hash==='#/shortcuts'?'shortcuts':'jobs';const next=views[(views.indexOf(current)+1)%views.length];location.hash=next==='usage'?'#/usage':next==='graphs'?'#/graphs':next==='rules'?'#/execution-router-rules':next==='shortcuts'?'#/shortcuts':(selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs');showView(next);}});
|
|
483
484
|
function parseMessage(message){try{return JSON.parse(message.data);}catch{return null;}}
|
|
484
|
-
function
|
|
485
|
+
function isFreshEnough(incoming,current){const a=Date.parse(incoming||''),b=Date.parse(current||'');return !Number.isFinite(b)||!Number.isFinite(a)||a>=b;}
|
|
486
|
+
function applyStatePatch(patch){if(!patch?.jobId)return false;const job=jobs.find(candidate=>candidate.id===patch.jobId);if(!job)return false;if(patch.jobState&&isFreshEnough(patch.jobUpdatedAt,job.updatedAt)){job['state']=patch.jobState;if(patch.jobUpdatedAt)job.updatedAt=patch.jobUpdatedAt;}if(patch.taskId&&patch.taskState){const task=(job.tasks||[]).find(candidate=>candidate.id===patch.taskId);if(task&&isFreshEnough(patch.taskUpdatedAt,task.updatedAt)){task['state']=patch.taskState;if(patch.taskUpdatedAt)task.updatedAt=patch.taskUpdatedAt;}}return true;}
|
|
487
|
+
function upsertFreshJob(incoming){if(!incoming)return;const index=jobs.findIndex(job=>jobSelectionKey(job)===jobSelectionKey(incoming));if(index<0){jobs.push(incoming);return;}if(isFreshEnough(incoming.updatedAt,jobs[index].updatedAt))jobs[index]=incoming;}
|
|
485
488
|
const stream=new EventSource('/events');
|
|
486
489
|
stream.onopen=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream connected');connection.className='status-pill ok';};
|
|
487
490
|
stream.onerror=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream reconnecting');connection.className='status-pill warn';};
|
|
488
|
-
stream.onmessage=message=>{const payload=parseMessage(message);if(payload?.job){
|
|
489
|
-
stream.addEventListener('relay',message=>{const payload=parseMessage(message);if(payload?.job)
|
|
491
|
+
stream.onmessage=message=>{const payload=parseMessage(message);if(payload?.job){upsertFreshJob(payload.job);renderAll();void drawRuntimeSummary();}};
|
|
492
|
+
stream.addEventListener('relay',message=>{const payload=parseMessage(message);if(payload?.job)upsertFreshJob(payload.job);else if(payload?.statePatch)applyStatePatch(payload.statePatch);if(payload?.event)addEvent(payload.event);renderAll();void drawRuntimeSummary();});
|
|
490
493
|
if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/graphs')showView('graphs');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else showView('jobs');
|
|
491
494
|
setInterval(()=>{if(location.hash==='#/usage')void drawUsage();},30000);
|
|
492
495
|
setInterval(()=>void refreshVersion(),30000);
|
package/dist/dashboard.js
CHANGED
|
@@ -71,7 +71,7 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
71
71
|
const discoveredJobs = async (forceRefresh = false) => {
|
|
72
72
|
try {
|
|
73
73
|
const jobs = overviewProvider
|
|
74
|
-
? (await overviewProvider(forceRefresh, forceRefresh ? 'dashboard.jobs-refresh' : 'internal.unknown')).map(item => ({ ...item.job, githubState: item.githubState, runtimeStatus: deriveJobRuntimeStatus(item.job, item.githubState), mergedAt: item.mergedAt, closedAt: item.closedAt, draft: item.draft, publication: item.publication }))
|
|
74
|
+
? (await overviewProvider(forceRefresh, forceRefresh ? 'dashboard.jobs-refresh' : 'internal.unknown')).map(item => ({ ...item.job, githubState: item.githubState, runtimeStatus: deriveJobRuntimeStatus(item.job, item.githubState), mergedAt: item.mergedAt, closedAt: item.closedAt, draft: item.draft, publication: item.publication, publicationFailure: item.publicationFailure }))
|
|
75
75
|
: jobId ? [await store.load(jobId)] : [];
|
|
76
76
|
lastKnownJobs = jobs;
|
|
77
77
|
return jobs;
|
|
@@ -202,7 +202,7 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
202
202
|
res.end('Ignored');
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
205
|
-
if (!event.repository || !webhook.repositories.has(event.repository) || (webhook.pullRequest !== undefined && event.pullRequest !== null && event.pullRequest !== webhook.pullRequest)) {
|
|
205
|
+
if (!event.repository || !webhook.repositories.has(event.repository) || (webhook.jobId !== undefined && event.jobId !== undefined && event.jobId !== null && event.jobId !== webhook.jobId) || (webhook.pullRequest !== undefined && event.pullRequest !== null && event.pullRequest !== webhook.pullRequest)) {
|
|
206
206
|
res.writeHead(202);
|
|
207
207
|
res.end('Ignored');
|
|
208
208
|
return;
|
|
@@ -238,6 +238,31 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
238
238
|
}
|
|
239
239
|
return;
|
|
240
240
|
}
|
|
241
|
+
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/control') {
|
|
242
|
+
if (req.method !== 'POST' || !resolvedActions?.control) {
|
|
243
|
+
res.writeHead(404);
|
|
244
|
+
res.end('Control API unavailable');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const chunks = [];
|
|
249
|
+
for await (const chunk of req)
|
|
250
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
251
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
252
|
+
const result = await resolvedActions.control(body);
|
|
253
|
+
if (!res.headersSent)
|
|
254
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
255
|
+
if (!res.writableEnded)
|
|
256
|
+
res.end(JSON.stringify(result));
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (!res.headersSent)
|
|
260
|
+
res.writeHead(409);
|
|
261
|
+
if (!res.writableEnded)
|
|
262
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
241
266
|
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/job-action') {
|
|
242
267
|
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
243
268
|
const requestedJobId = url.searchParams.get('id') ?? '';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const legacy = {
|
|
2
|
+
quota_exceeded: { method: 'worker.codex.launch', action: 'return', execution: 'skip', mock: 'worker_quota' },
|
|
3
|
+
model_not_found: { method: 'worker.codex.launch', action: 'return', execution: 'skip', mock: 'worker_model_not_found' },
|
|
4
|
+
provider_unavailable: { method: 'worker.codex.launch', action: 'return', execution: 'skip', mock: 'worker_provider_unavailable' },
|
|
5
|
+
github_projection_failure: { method: 'github.projectLocalMerge', action: 'throw', execution: 'skip', error: { code: 'SERVICE_UNAVAILABLE', message: 'E2E transient GitHub projection transport failure', status: 503 } },
|
|
6
|
+
};
|
|
7
|
+
export function parseTestInjection(value) {
|
|
8
|
+
if (typeof value === 'string')
|
|
9
|
+
return legacy[value];
|
|
10
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
11
|
+
return undefined;
|
|
12
|
+
const v = value;
|
|
13
|
+
if (v.method !== 'worker.codex.launch' && v.method !== 'github.projectLocalMerge' && v.method !== 'browserWorkspace.command')
|
|
14
|
+
return undefined;
|
|
15
|
+
if (v.action !== 'throw' && v.action !== 'return' && v.action !== 'pass')
|
|
16
|
+
return undefined;
|
|
17
|
+
if (v.execution !== 'skip' && v.execution !== 'after' && v.execution !== 'run')
|
|
18
|
+
return undefined;
|
|
19
|
+
const error = v.error;
|
|
20
|
+
if (v.action === 'throw' && (!error || typeof error !== 'object' || typeof error.message !== 'string' || typeof error.code !== 'string'))
|
|
21
|
+
return undefined;
|
|
22
|
+
const mock = v.mock;
|
|
23
|
+
if (v.action === 'return' && (v.method !== 'worker.codex.launch' || !['worker_quota', 'worker_model_not_found', 'worker_provider_unavailable'].includes(String(mock))))
|
|
24
|
+
return undefined;
|
|
25
|
+
if (v.method === 'worker.codex.launch' && v.action !== 'pass' && (v.action !== 'return' || v.execution !== 'skip'))
|
|
26
|
+
return undefined;
|
|
27
|
+
if (v.action === 'pass' && v.execution !== 'run')
|
|
28
|
+
return undefined;
|
|
29
|
+
if (v.action !== 'pass' && v.execution === 'run')
|
|
30
|
+
return undefined;
|
|
31
|
+
return { method: v.method, action: v.action, execution: v.execution, error: error, mock: mock };
|
|
32
|
+
}
|
|
33
|
+
export function authorizedInjection(repository, raw, env = process.env) {
|
|
34
|
+
return env.E2E_REPO && repository === env.E2E_REPO ? parseTestInjection(raw) : undefined;
|
|
35
|
+
}
|
|
36
|
+
function mockWorker(name) {
|
|
37
|
+
const message = name === 'worker_quota' ? 'rate_limit_exceeded: usage limit reached' : name === 'worker_model_not_found' ? 'model_not_found: model is unavailable' : 'provider_unavailable: provider refused the request';
|
|
38
|
+
const promise = Promise.reject(new Error(message));
|
|
39
|
+
promise.catch(() => { });
|
|
40
|
+
return { id: 'e2e-injected', launched: Promise.resolve(), promise, cancel: () => { }, processExit: { exitCode: 1, signal: null, stderr: message, terminationReason: null } };
|
|
41
|
+
}
|
|
42
|
+
/** Caller must supply a configuration already consumed durably before this invocation. */
|
|
43
|
+
export function withTestInjection(method, config, execute) {
|
|
44
|
+
if (!config || config.method !== method || config.action === 'pass')
|
|
45
|
+
return execute();
|
|
46
|
+
const apply = (original) => {
|
|
47
|
+
if (config.action === 'throw') {
|
|
48
|
+
const error = Object.assign(new Error(config.error.message), { code: config.error.code, status: config.error.status });
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
if (method === 'worker.codex.launch' && config.mock)
|
|
52
|
+
return mockWorker(config.mock);
|
|
53
|
+
return original;
|
|
54
|
+
};
|
|
55
|
+
return config.execution === 'after' ? Promise.resolve(execute()).then(apply) : apply();
|
|
56
|
+
}
|
|
57
|
+
/** Shared atomic consumer, scoped solely by E2E_REPO and exact method. */
|
|
58
|
+
export async function consumeMethodInjection(relay, repository, jobId, method) {
|
|
59
|
+
if (!process.env.E2E_REPO || process.env.E2E_REPO !== repository || !relay.mutateJob)
|
|
60
|
+
return undefined;
|
|
61
|
+
let config;
|
|
62
|
+
await relay.mutateJob(jobId, job => {
|
|
63
|
+
if (job.repository !== repository)
|
|
64
|
+
return job;
|
|
65
|
+
const task = job.tasks.find(t => authorizedInjection(job.repository, t.testInjection)?.method === method);
|
|
66
|
+
if (!task)
|
|
67
|
+
return job;
|
|
68
|
+
config = authorizedInjection(job.repository, task.testInjection);
|
|
69
|
+
delete task.testInjection;
|
|
70
|
+
task.updatedAt = new Date(Math.max(Date.now(), Date.parse(task.updatedAt) + 1)).toISOString();
|
|
71
|
+
return job;
|
|
72
|
+
});
|
|
73
|
+
if (config && (await relay.loadJob(jobId))?.tasks.some(t => authorizedInjection(repository, t.testInjection)?.method === method))
|
|
74
|
+
throw Error(`E2E injection consumption not committed: ${jobId}`);
|
|
75
|
+
return config;
|
|
76
|
+
}
|
package/dist/github-auth.js
CHANGED
|
@@ -15,17 +15,18 @@ export function githubAppConfig(args, env = process.env) {
|
|
|
15
15
|
throw new Error('GitHub App auth requires app id, installation id, and private key file');
|
|
16
16
|
return { appId, installationId, privateKeyFile };
|
|
17
17
|
}
|
|
18
|
-
export async function githubAuthContext(args, env = process.env) {
|
|
18
|
+
export async function githubAuthContext(args, env = process.env, probeLogin = true) {
|
|
19
19
|
const config = githubAppConfig(args, env);
|
|
20
20
|
let options = null;
|
|
21
21
|
if (config)
|
|
22
22
|
options = { appId: config.appId, installationId: config.installationId, privateKey: await readFile(config.privateKeyFile, 'utf8') };
|
|
23
23
|
const client = new GhApiClient(options);
|
|
24
24
|
const trustedAuthors = new Set();
|
|
25
|
-
|
|
25
|
+
if (probeLogin)
|
|
26
|
+
trustedAuthors.add(await client.currentLogin());
|
|
26
27
|
for (const author of (value(args, '--trusted-authors') || env.AGENTS_RELAY_TRUSTED_AUTHORS || '').split(',').map(item => item.trim()).filter(Boolean))
|
|
27
28
|
trustedAuthors.add(author);
|
|
28
|
-
if (options) {
|
|
29
|
+
if (options && probeLogin) {
|
|
29
30
|
const local = await client.localLogin();
|
|
30
31
|
if (local)
|
|
31
32
|
trustedAuthors.add(local);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Fresh unbound ACTIVE jobs are highest priority; terminal projection debt is last. */
|
|
2
|
+
export function orderGitHubBindingCandidates(items) {
|
|
3
|
+
return [...items].sort((a, b) => score(a.job) - score(b.job) || a.job.createdAt.localeCompare(b.job.createdAt));
|
|
4
|
+
}
|
|
5
|
+
function score(job) {
|
|
6
|
+
const bound = (job.externalPrNumber ?? (job.prNumber > 0 ? job.prNumber : 0)) > 0;
|
|
7
|
+
const terminal = ['COMPLETED', 'CANCELLED'].includes(job.state);
|
|
8
|
+
if (!terminal && !bound)
|
|
9
|
+
return 0;
|
|
10
|
+
if (!terminal && bound)
|
|
11
|
+
return 1;
|
|
12
|
+
if (terminal && bound)
|
|
13
|
+
return 2;
|
|
14
|
+
return 3;
|
|
15
|
+
}
|
package/dist/github-client.js
CHANGED
|
@@ -241,13 +241,18 @@ export class GhApiClient {
|
|
|
241
241
|
let stdout = '';
|
|
242
242
|
child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
243
243
|
child.on('error', () => resolve(null));
|
|
244
|
-
child.on('close', () => {
|
|
244
|
+
child.on('close', (code) => {
|
|
245
|
+
if (code !== 0) {
|
|
246
|
+
resolve(null);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
245
249
|
try {
|
|
246
250
|
const value = JSON.parse(stdout);
|
|
247
251
|
const hosts = value.hosts;
|
|
248
252
|
const github = hosts?.['github.com'];
|
|
249
253
|
const active = Array.isArray(github) ? github.find(item => item.active === true) : undefined;
|
|
250
|
-
|
|
254
|
+
const healthy = active && active.state !== 'error' && typeof active.error !== 'string';
|
|
255
|
+
resolve(healthy && typeof active?.login === 'string' && active.login.length > 0 ? active.login : null);
|
|
251
256
|
}
|
|
252
257
|
catch {
|
|
253
258
|
resolve(null);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** A PR is a read-only collaboration record after authoritative local completion. */
|
|
2
|
+
export async function archiveDisplayPullRequest(client, repository, number, jobId, publishedSha, state) {
|
|
3
|
+
if (state !== 'OPEN')
|
|
4
|
+
return 'already_closed';
|
|
5
|
+
const marker = `Agents Relay local completion: ${jobId}`;
|
|
6
|
+
const path = `repos/${repository}/issues/${number}/comments?per_page=100`;
|
|
7
|
+
const comments = await client.request(path);
|
|
8
|
+
if (!Array.isArray(comments))
|
|
9
|
+
throw new Error('Cannot verify PR archive comment idempotency');
|
|
10
|
+
const alreadyCommented = comments.some(item => item && typeof item === 'object'
|
|
11
|
+
&& 'body' in item && typeof item.body === 'string' && item.body.includes(marker));
|
|
12
|
+
if (!alreadyCommented) {
|
|
13
|
+
await client.request(`repos/${repository}/issues/${number}/comments`, {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
body: JSON.stringify({ body: `${marker}\nPublished local main commit: ${publishedSha}.\nThis PR is closed as a review record; GitHub did not perform a merge.` }),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
await client.request(`repos/${repository}/pulls/${number}`, {
|
|
19
|
+
method: 'PATCH', body: JSON.stringify({ state: 'closed' }),
|
|
20
|
+
});
|
|
21
|
+
return 'archived';
|
|
22
|
+
}
|
package/dist/github-webhook.js
CHANGED
|
@@ -14,7 +14,7 @@ export function parsePackagePublishFailureMarker(body) {
|
|
|
14
14
|
const data = JSON.parse(body.slice(start + prefix.length, end));
|
|
15
15
|
if (typeof data.runId !== 'string' || !/^\d+$/.test(data.runId) || !Number.isInteger(data.attempt) || Number(data.attempt) < 1 || typeof data.sha !== 'string' || !/^[a-f0-9]{7,40}$/i.test(data.sha) || typeof data.stage !== 'string' || !data.stage)
|
|
16
16
|
return null;
|
|
17
|
-
return { runId: data.runId, attempt: Number(data.attempt), sha: data.sha, stage: data.stage };
|
|
17
|
+
return { runId: data.runId, attempt: Number(data.attempt), sha: data.sha, stage: data.stage, ...(typeof data.jobId === 'string' && data.jobId ? { jobId: data.jobId } : {}) };
|
|
18
18
|
}
|
|
19
19
|
catch {
|
|
20
20
|
return null;
|
|
@@ -45,6 +45,7 @@ export function parsePackagePublicationMarker(body) {
|
|
|
45
45
|
version: value.version,
|
|
46
46
|
registry: typeof value.registry === 'string' ? value.registry : null,
|
|
47
47
|
sha: typeof value.sha === 'string' ? value.sha : null,
|
|
48
|
+
...(typeof value.jobId === 'string' && value.jobId ? { jobId: value.jobId } : {}),
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
export function verifyGitHubWebhook(secret, body, signature) {
|
|
@@ -65,14 +66,16 @@ function pullRequestFacts(payload, action) {
|
|
|
65
66
|
const sha = typeof head?.sha === 'string' ? head.sha : null;
|
|
66
67
|
if (!ref || !sha)
|
|
67
68
|
return null;
|
|
69
|
+
const updated = typeof pr.updated_at === 'string' && Number.isFinite(Date.parse(pr.updated_at))
|
|
70
|
+
? { observedAt: new Date(pr.updated_at).toISOString() } : {};
|
|
68
71
|
if (action === 'closed')
|
|
69
|
-
return { state: pr.merged === true ? 'MERGED' : 'CLOSED', head: { ref, sha } };
|
|
70
|
-
return { state: 'OPEN', head: { ref, sha } };
|
|
72
|
+
return { state: pr.merged === true ? 'MERGED' : 'CLOSED', head: { ref, sha }, ...updated };
|
|
73
|
+
return { state: 'OPEN', head: { ref, sha }, ...updated };
|
|
71
74
|
}
|
|
72
75
|
export function parseGitHubWebhook(headers, body) {
|
|
73
76
|
const deliveryId = String(headers['x-github-delivery'] ?? '');
|
|
74
77
|
const event = String(headers['x-github-event'] ?? '');
|
|
75
|
-
if (!deliveryId || (event !== 'pull_request' && event !== 'issue_comment'))
|
|
78
|
+
if (!deliveryId || (event !== 'pull_request' && event !== 'issue_comment' && event !== 'commit_comment'))
|
|
76
79
|
return null;
|
|
77
80
|
let payload;
|
|
78
81
|
try {
|
|
@@ -84,20 +87,50 @@ export function parseGitHubWebhook(headers, body) {
|
|
|
84
87
|
const repository = payload.repository;
|
|
85
88
|
const repositoryName = typeof repository?.full_name === 'string' ? repository.full_name : null;
|
|
86
89
|
const action = typeof payload.action === 'string' ? payload.action : null;
|
|
90
|
+
if (!repositoryName)
|
|
91
|
+
return null;
|
|
92
|
+
if (event === 'commit_comment') {
|
|
93
|
+
const comment = payload.comment;
|
|
94
|
+
const trustedPublisher = comment?.user?.login === 'github-actions[bot]';
|
|
95
|
+
const publication = trustedPublisher ? parsePackagePublicationMarker(comment?.body) : null;
|
|
96
|
+
const publishFailure = trustedPublisher ? parsePackagePublishFailureMarker(comment?.body) : null;
|
|
97
|
+
const jobId = publication?.jobId ?? publishFailure?.jobId;
|
|
98
|
+
if ((!publication && !publishFailure) || !jobId)
|
|
99
|
+
return null;
|
|
100
|
+
const commentTime = comment?.created_at;
|
|
101
|
+
const publicationObservedAt = typeof commentTime === 'string' && Number.isFinite(Date.parse(commentTime))
|
|
102
|
+
? new Date(commentTime).toISOString() : undefined;
|
|
103
|
+
return { deliveryId, event, action, repository: repositoryName, pullRequest: null, jobId, publication,
|
|
104
|
+
...(publicationObservedAt ? { publicationObservedAt } : {}),
|
|
105
|
+
...(publishFailure ? { publishFailure } : {}), pullRequestFacts: null };
|
|
106
|
+
}
|
|
87
107
|
const pr = payload.pull_request;
|
|
88
108
|
const issue = payload.issue;
|
|
89
|
-
const number = Number(pr?.number ?? (issue?.pull_request ? issue.number : 0));
|
|
90
|
-
if (!repositoryName || !Number.isInteger(number) || number <= 0)
|
|
91
|
-
return null;
|
|
92
109
|
if (event === 'issue_comment') {
|
|
110
|
+
const issueNumber = Number(issue?.number ?? 0);
|
|
111
|
+
if (!Number.isInteger(issueNumber) || issueNumber <= 0)
|
|
112
|
+
return null;
|
|
93
113
|
const comment = payload.comment;
|
|
94
|
-
const
|
|
95
|
-
const
|
|
114
|
+
const trustedPublisher = comment?.user?.login === 'github-actions[bot]';
|
|
115
|
+
const publication = trustedPublisher ? parsePackagePublicationMarker(comment?.body) : null;
|
|
116
|
+
const publishFailure = trustedPublisher
|
|
96
117
|
? parsePackagePublishFailureMarker(comment?.body) : null;
|
|
118
|
+
const jobId = publication?.jobId ?? publishFailure?.jobId;
|
|
97
119
|
if (!publication && !publishFailure)
|
|
98
120
|
return null;
|
|
99
|
-
|
|
121
|
+
if (!issue?.pull_request && !jobId)
|
|
122
|
+
return null;
|
|
123
|
+
const commentTime = comment?.created_at;
|
|
124
|
+
const publicationObservedAt = typeof commentTime === 'string' && Number.isFinite(Date.parse(commentTime))
|
|
125
|
+
? new Date(commentTime).toISOString() : undefined;
|
|
126
|
+
return { deliveryId, event, action, repository: repositoryName,
|
|
127
|
+
pullRequest: issue?.pull_request ? issueNumber : null, ...(jobId ? { jobId } : {}), publication,
|
|
128
|
+
...(publicationObservedAt ? { publicationObservedAt } : {}),
|
|
129
|
+
...(publishFailure ? { publishFailure } : {}), pullRequestFacts: null };
|
|
100
130
|
}
|
|
131
|
+
const number = Number(pr?.number ?? 0);
|
|
132
|
+
if (!Number.isInteger(number) || number <= 0)
|
|
133
|
+
return null;
|
|
101
134
|
const facts = pullRequestFacts(payload, action);
|
|
102
135
|
if (!facts)
|
|
103
136
|
return null;
|
package/dist/job-actions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { transitionJobEvent } from './scheduler.js';
|
|
2
2
|
import { tryApplyTaskCancelAction } from './task-actions.js';
|
|
3
|
+
import { decideJobLifecycle, dispatchJobLifecycleSync } from './workflow/lifecycle.js';
|
|
3
4
|
export const JOB_ACTIVATE_ACTION = 'job.activate';
|
|
4
5
|
export const JOB_CANCEL_ACTION = 'job.cancel';
|
|
5
6
|
async function transitionJobAction(job, event, action) {
|
|
@@ -34,8 +35,36 @@ async function cancelActiveWork(store, job, reason) {
|
|
|
34
35
|
await store.saveTask(task);
|
|
35
36
|
}
|
|
36
37
|
}
|
|
38
|
+
function cancelActiveWorkSnapshot(job, reason) {
|
|
39
|
+
for (const plan of (job.plans ?? []).filter(item => item.status === 'PLANNING')) {
|
|
40
|
+
plan.status = 'FAILED';
|
|
41
|
+
plan.error = reason;
|
|
42
|
+
plan.assessment = `Planning failed: ${reason}`;
|
|
43
|
+
plan.leaseOwner = null;
|
|
44
|
+
plan.leaseExpiresAt = null;
|
|
45
|
+
plan.executionId = null;
|
|
46
|
+
plan.updatedAt = new Date().toISOString();
|
|
47
|
+
}
|
|
48
|
+
for (const task of job.tasks)
|
|
49
|
+
tryApplyTaskCancelAction(task, reason);
|
|
50
|
+
}
|
|
37
51
|
export async function applyJobCancelAction(store, job, reason = 'Job cancelled') {
|
|
38
52
|
const from = job.state;
|
|
53
|
+
if (store.mutateJob) {
|
|
54
|
+
const updated = await store.mutateJob(job.id, current => {
|
|
55
|
+
const decision = decideJobLifecycle(current, 'cancel');
|
|
56
|
+
if (!decision.accepted)
|
|
57
|
+
throw new Error(`Job ${current.id} is not cancellable (${current.state})`);
|
|
58
|
+
dispatchJobLifecycleSync(current, 'cancel');
|
|
59
|
+
if (!decision.actions.includes('effect.cancelActiveTasks'))
|
|
60
|
+
throw new Error('Unsupported job.cancel effect');
|
|
61
|
+
cancelActiveWorkSnapshot(current, reason);
|
|
62
|
+
current.updatedAt = new Date().toISOString();
|
|
63
|
+
return current;
|
|
64
|
+
});
|
|
65
|
+
Object.assign(job, structuredClone(updated));
|
|
66
|
+
return job;
|
|
67
|
+
}
|
|
39
68
|
try {
|
|
40
69
|
await transitionJobEvent(job, 'cancel', {}, async (actionId) => {
|
|
41
70
|
if (actionId !== 'effect.cancelActiveTasks')
|