@juangcarmona/lighthouse-sdlc 0.17.1 → 0.18.1
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 +15 -3
- package/capabilities.json +2 -1
- package/epic-delivery.md +35 -0
- package/github-actions.md +21 -3
- package/host-contract.md +2 -0
- package/package.json +4 -1
- package/schemas/product-change.schema.json +23 -4
- package/schemas/stage-result.schema.json +23 -1
- package/skills/product-change/SKILL.md +3 -3
- package/skills/refine/SKILL.md +3 -2
- package/src/completion-actions.mjs +49 -12
- package/src/completion.mjs +1 -1
- package/src/contracts.mjs +10 -0
- package/src/decompose.mjs +5 -2
- package/src/epic-dispatch.mjs +59 -0
- package/src/epic-github.mjs +111 -0
- package/src/epic-journal.mjs +104 -0
- package/src/epic-merge.mjs +91 -0
- package/src/epic-native.mjs +465 -0
- package/src/epic.mjs +86 -0
- package/src/handoff.mjs +97 -0
- package/src/host-contract.mjs +22 -6
- package/src/implementation.mjs +33 -5
- package/src/install-workflows.mjs +1 -1
- package/src/issue-actions.mjs +368 -78
- package/src/opencode-execute.mjs +20 -0
- package/src/pr-state-actions.mjs +21 -7
- package/src/pr-state.mjs +18 -1
- package/src/product-change-actions.mjs +158 -46
- package/src/product-change.mjs +92 -11
- package/src/proposal-review.mjs +16 -6
- package/src/proposal.mjs +13 -1
- package/src/stacked.mjs +14 -3
- package/src/stage-dispatch.mjs +66 -19
- package/src/stage-transition.mjs +151 -2
- package/src/triage.mjs +4 -2
- package/src/unattended-actions.mjs +39 -12
- package/src/unattended.mjs +1 -1
- package/src/worker-maintenance.mjs +15 -3
- package/src/worker-recovery.mjs +209 -8
- package/workflows/lighthouse-sdlc-apply-product-change.lock.yml +97 -11
- package/workflows/lighthouse-sdlc-apply-product-change.md +75 -6
- package/workflows/lighthouse-sdlc-apply-review.lock.yml +33 -2
- package/workflows/lighthouse-sdlc-apply-review.md +29 -1
- package/workflows/lighthouse-sdlc-epic.yml +79 -0
- package/workflows/lighthouse-sdlc-implement.lock.yml +38 -2
- package/workflows/lighthouse-sdlc-implement.md +33 -2
- package/workflows/lighthouse-sdlc-integrate.lock.yml +4 -1
- package/workflows/lighthouse-sdlc-integrate.md +3 -0
- package/workflows/lighthouse-sdlc-product-change.lock.yml +39 -4
- package/workflows/lighthouse-sdlc-product-change.md +39 -6
- package/workflows/lighthouse-sdlc-propose.lock.yml +13 -1
- package/workflows/lighthouse-sdlc-propose.md +11 -1
- package/workflows/lighthouse-sdlc-refine.lock.yml +29 -2
- package/workflows/lighthouse-sdlc-refine.md +25 -2
- package/workflows/lighthouse-sdlc-review.lock.yml +4 -1
- package/workflows/lighthouse-sdlc-review.md +3 -0
- package/workflows/lighthouse-sdlc-router.yml +47 -3
- package/workflows/lighthouse-sdlc-triage.lock.yml +30 -3
- package/workflows/lighthouse-sdlc-triage.md +26 -3
- package/workflows/lighthouse-worker-maintenance.yml +70 -1
package/src/worker-recovery.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { digest } from './host-contract.mjs';
|
|
2
|
-
import { issueComments, readDispatchReceipt, loadDispatch, workerFor, stagePath, recordDispatch } from './stage-dispatch.mjs';
|
|
1
|
+
import { digest, validateStageEnvelope } from './host-contract.mjs';
|
|
2
|
+
import { issueComments, readDispatchReceipt, loadDispatch, workerFor, stagePath, recordDispatch, claimDispatch, checkDispatchClaim, finishDispatch } from './stage-dispatch.mjs';
|
|
3
|
+
import { publishObservedStop, handoff, pendingQuestion, resultHandoffState, visibleNextAction, productApplyObservation } from './handoff.mjs';
|
|
3
4
|
import { readActionsJson } from './actions-artifact.mjs';
|
|
4
5
|
|
|
5
6
|
export const maintenancePath = '.github/workflows/lighthouse-worker-maintenance.yml';
|
|
6
7
|
export const retryLimit = 2;
|
|
7
8
|
export const retryDelayMs = 30 * 60 * 1000;
|
|
8
9
|
export const recoveryId = (repository, rootId, attempt) => digest(`${repository}:${rootId}:retry:${attempt}`);
|
|
10
|
+
const publishStep = job => job?.steps?.find(step => ['Recheck authority and publish the validated result', 'Recheck authority and publish'].includes(step.name));
|
|
9
11
|
const identity = r => ({ issue: r.request.issue, worker: workerFor(r.request), runId: String(r.run.id), rootId: r.request.recovery?.rootId ?? r.request.id, attempt: r.request.recovery?.attempt ?? 0 });
|
|
10
12
|
const ownerAnswers = comments => comments.filter(c => c.user?.login === c.owner && !c.body.includes('<!-- lighthouse-run:') && !c.body.includes('<!-- lighthouse-actions:'));
|
|
11
13
|
const intent = (issue, comments, owner) => digest(JSON.stringify({ title: issue.title, body: issue.body, comments: ownerAnswers(comments.map(c => ({ ...c, owner }))).map(c => ({ id: c.id, body: c.body, updated_at: c.updated_at })) }));
|
|
@@ -21,15 +23,16 @@ export function recoveryDecision({ request, receipt, run, jobs, issue, comments,
|
|
|
21
23
|
if (run.run_attempt !== 1) return result('uncertain', 'execution-rerun', 'Inspect all attempts; a rerun does not reclaim the original request.');
|
|
22
24
|
if (String(receipt.workerRunId) !== String(run.id)) return result('uncertain', 'dispatch-unconfirmed', 'Reconcile the request with the observed execution.');
|
|
23
25
|
if (receipt.status === 'waiting') return result('waiting-human', 'human-decision', 'Answer the recorded question; no automatic retry.');
|
|
26
|
+
if (receipt.status === 'blocked') return result('blocked', 'stage-blocked', 'Resolve the recorded blocker before requesting an authorized continuation.');
|
|
24
27
|
if (receipt.status === 'completed' && run.conclusion === 'success') return result('succeeded', 'worker-completed', 'No recovery needed.');
|
|
25
28
|
if (issue.state !== 'open' || issue.user?.login !== owner || issue.pull_request || issue.labels.some(l => ['future', 'needs-info', 'waiting-review', 'human-working', 'agent-working'].includes(l.name))) return result('blocked', 'issue-paused', 'Inspect the issue decision or reservation before requesting work.');
|
|
26
29
|
if (request.sourceRevision !== revision || request.workflowRevision !== workflowRevision || request.taskDigest !== intent(issue, comments, owner)) return result('blocked', 'authority-changed', 'Request fresh work against the current source and owner intent.');
|
|
27
30
|
if (!['refine', 'propose', 'implement'].includes(request.stage) || !['failure', 'timed_out'].includes(run.conclusion)) return result('uncertain', 'manual-recovery', 'Inspect the outcome and request a new authorized execution if needed.');
|
|
28
31
|
const preparation = jobs.find(j => j.name === 'Authorize and reserve issue');
|
|
29
32
|
const publication = jobs.find(j => j.name === 'Validate, publish and release');
|
|
30
|
-
const
|
|
33
|
+
const publish = publishStep(publication);
|
|
31
34
|
const failedExecution = jobs.some(j => ['pre_activation', 'activation', 'agent'].includes(j.name) && ['failure', 'timed_out'].includes(j.conclusion));
|
|
32
|
-
if (preparation?.conclusion !== 'success' || !failedExecution ||
|
|
35
|
+
if (preparation?.conclusion !== 'success' || !failedExecution || publish?.conclusion !== 'skipped') return result('uncertain', 'publication-uncertain', 'Inspect publication and remote effects before creating another request.');
|
|
33
36
|
if (base.attempt >= retryLimit) return result('exhausted', 'worker-technical-failure', 'Retry budget exhausted. Inspect the failure and make a new owner decision.');
|
|
34
37
|
const ended = Date.parse(run.updated_at);
|
|
35
38
|
if (!Number.isFinite(ended) || !Number.isFinite(now)) return result('uncertain', 'execution-time-unknown', 'Inspect the execution timestamps.');
|
|
@@ -55,11 +58,194 @@ export async function observeDispatch(api, repository, owner, comment, issue, co
|
|
|
55
58
|
const run = await api(`actions/runs/${receipt.workerRunId}`);
|
|
56
59
|
const dispatch = await loadDispatch(api, repository, owner, issue.number, comment.id, run, readArtifact);
|
|
57
60
|
const jobs = await api(`actions/runs/${run.id}/attempts/${run.run_attempt}/jobs?per_page=100`);
|
|
58
|
-
if (jobs.total_count > 100) throw Error('Execution job inventory exceeds recovery bounds');
|
|
61
|
+
if (!Array.isArray(jobs.jobs) || !Number.isSafeInteger(jobs.total_count) || jobs.total_count !== jobs.jobs.length || jobs.total_count > 100) throw Error('Execution job inventory is incomplete or exceeds recovery bounds');
|
|
59
62
|
const repo = await api('');
|
|
60
63
|
const revision = (await api(`commits/${encodeURIComponent(repo.default_branch)}`)).sha;
|
|
61
64
|
const workflowRevision = (await api(`commits/${encodeURIComponent(dispatch.request.ref)}`)).sha;
|
|
62
|
-
|
|
65
|
+
const agent = jobs.jobs.find(job => job.name === 'agent');
|
|
66
|
+
const publishJob = jobs.jobs.find(job => job.name === 'Validate, publish and release');
|
|
67
|
+
const publish = publishStep(publishJob);
|
|
68
|
+
const cleanup = publishJob?.steps?.find(step => ['Release the issue reservation', 'Release reservation'].includes(step.name));
|
|
69
|
+
const prepare = jobs.jobs.find(job => job.name === 'Authorize and reserve issue');
|
|
70
|
+
const unpublished = publish?.conclusion === 'skipped' || publishJob?.conclusion === 'skipped';
|
|
71
|
+
const notStarted = !agent || agent.conclusion === 'skipped';
|
|
72
|
+
const activationFailed = jobs.jobs.some(job => ['pre_activation', 'activation'].includes(job.name) && ['failure', 'timed_out'].includes(job.conclusion));
|
|
73
|
+
const observation = publish?.conclusion === 'success' && ['failure', 'timed_out', 'cancelled'].includes(cleanup?.conclusion) ? 'cleanup-failure'
|
|
74
|
+
: ['completed', 'waiting', 'blocked'].includes(receipt.status) ? null
|
|
75
|
+
: workerFor(dispatch.request) === 'apply-product-change' ? productApplyObservation({ prepare: jobs.jobs.find(job => job.name === 'Recheck product approval')?.conclusion, apply: jobs.jobs.find(job => job.name === 'Apply with the pinned ProductShape provider')?.conclusion, publish: jobs.jobs.find(job => job.name === 'Recheck and publish provider output')?.conclusion })
|
|
76
|
+
: agent?.conclusion === 'success' && (!publishJob || ['failure', 'timed_out', 'cancelled', 'skipped'].includes(publish?.conclusion) || ['failure', 'timed_out', 'cancelled', 'skipped'].includes(publishJob.conclusion)) ? 'publication-unknown'
|
|
77
|
+
: ['failure', 'timed_out'].includes(prepare?.conclusion) && notStarted ? 'preparation-failure'
|
|
78
|
+
: prepare?.conclusion === 'success' && activationFailed && notStarted && unpublished ? 'activation-failure'
|
|
79
|
+
: unpublished && ['failure', 'timed_out'].includes(agent?.conclusion) ? 'failure'
|
|
80
|
+
: unpublished && agent?.conclusion === 'cancelled' ? 'cancelled'
|
|
81
|
+
: run.conclusion === 'cancelled' && notStarted && (unpublished || !publishJob) ? 'pre-agent-cancelled' : null;
|
|
82
|
+
const event = dispatch.request.event;
|
|
83
|
+
const answered = dispatch.request.eventName === 'issue_comment' && event?.action === 'created' && event.issue?.number === issue.number && !event.issue.pull_request && event.comment?.user?.login === owner && event.comment.user.type === 'User' && typeof event.comment.body === 'string' && !!event.comment.body.trim() && !dispatch.request.feedbackKey;
|
|
84
|
+
const stop = run.status === 'completed' && observation ? { run: String(run.id), attempt: String(run.run_attempt), observation, answerRetained: answered } : null;
|
|
85
|
+
return { ...recoveryDecision({ ...dispatch, run, jobs: jobs.jobs, issue, comments, owner, revision, workflowRevision, now }), dispatch, stop, run, jobs: jobs.jobs, revision, workflowRevision };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Reporting is read-only so it can observe an active worker without acquiring its lease.
|
|
89
|
+
export async function observeRecovery(api, repository, owner, issueNumber, now = Date.now(), readArtifact = readActionsJson) {
|
|
90
|
+
const issue = await api(`issues/${issueNumber}`), comments = await issueComments(api, issueNumber);
|
|
91
|
+
const entries = comments.filter(c => c.user?.login === 'github-actions[bot]' && c.user.type === 'Bot' && c.body?.includes('<!-- lighthouse-dispatch:')).map(comment => ({ comment, receipt: readDispatchReceipt(comment, repository, issueNumber) }));
|
|
92
|
+
const unattended = comments.filter(c => c.user?.login === 'github-actions[bot]' && c.user.type === 'Bot' && c.body?.includes('<!-- lighthouse-unattended:')).at(-1);
|
|
93
|
+
if (unattended && (!entries.length || unattended.id > entries.at(-1).comment.id)) return [];
|
|
94
|
+
const latest = new Map();
|
|
95
|
+
for (const entry of entries) latest.set(entry.receipt.recovery?.rootId ?? entry.receipt.id, entry);
|
|
96
|
+
const observations = [];
|
|
97
|
+
for (const { comment } of latest.values()) {
|
|
98
|
+
const { dispatch, stop, run, jobs, revision, workflowRevision, ...observation } = await observeDispatch(api, repository, owner, comment, issue, comments, now, readArtifact);
|
|
99
|
+
if (observation.state === 'stalled' || observation.code === 'dispatch-unconfirmed') observations.push(observation);
|
|
100
|
+
}
|
|
101
|
+
return observations;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function reservationEvent(api, issue) {
|
|
105
|
+
let latest;
|
|
106
|
+
for (let page = 1; page <= 10; page++) {
|
|
107
|
+
const events = await api(`issues/${issue}/events?per_page=100&page=${page}`);
|
|
108
|
+
if (!Array.isArray(events)) throw Error('Issue event inventory unavailable');
|
|
109
|
+
for (const event of events) if (['labeled', 'unlabeled'].includes(event.event) && event.label?.name === 'agent-working') latest = event;
|
|
110
|
+
if (events.length < 100) return latest;
|
|
111
|
+
}
|
|
112
|
+
throw Error('Issue event history exceeds recovery bounds');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Release only a retained reservation from a completed, unpublished read-only worker.
|
|
116
|
+
export async function reconcileAbandonedReservation(api, repository, owner, issue, comments, observation, readArtifact = readActionsJson) {
|
|
117
|
+
const { dispatch, run, jobs, revision, workflowRevision } = observation;
|
|
118
|
+
if (!dispatch || !run || !['triage', 'refine'].includes(dispatch.request.stage) || workerFor(dispatch.request) !== dispatch.request.stage || run.status !== 'completed' || run.run_attempt !== 1 || !['failure', 'cancelled', 'timed_out'].includes(run.conclusion)) return false;
|
|
119
|
+
if (issue.state !== 'open' || issue.pull_request || issue.user?.login !== owner || !issue.labels.some(l => l.name === 'agent-working') || issue.labels.some(l => ['human-working', 'future'].includes(l.name)) || dispatch.request.sourceRevision !== revision || dispatch.request.workflowRevision !== workflowRevision || dispatch.request.taskDigest !== intent(issue, comments, owner)) return false;
|
|
120
|
+
if (comments.some(c => c.user?.login === 'github-actions[bot]' && c.user.type === 'Bot' && c.body?.includes(`<!-- lighthouse-stop:${run.id}:${run.run_attempt} -->`))) return false;
|
|
121
|
+
const prepare = jobs.find(job => job.name === 'Authorize and reserve issue');
|
|
122
|
+
const agent = jobs.find(job => job.name === 'agent');
|
|
123
|
+
const publish = jobs.find(job => job.name === 'Validate, publish and release');
|
|
124
|
+
const step = publishStep(publish);
|
|
125
|
+
if (prepare?.conclusion !== 'success' || !['failure', 'timed_out', 'cancelled', 'skipped'].includes(agent?.conclusion) || !(publish?.conclusion === 'skipped' || step?.conclusion === 'skipped')) return false;
|
|
126
|
+
let snapshot;
|
|
127
|
+
try { snapshot = await readArtifact(api, repository, run.id, `lighthouse-input-${issue.number}-${run.run_attempt}`, 'request.json'); } catch { return false; }
|
|
128
|
+
if (snapshot?.runId !== digest(`${repository}:${issue.number}:${dispatch.request.stage}:${run.id}:${run.run_attempt}`) || snapshot.issue !== issue.number || snapshot.stage !== dispatch.request.stage || String(snapshot.workflowRunId) !== String(run.id) || snapshot.sourceRevision !== revision || snapshot.workflowRevision !== workflowRevision || snapshot.taskDigest !== dispatch.request.taskDigest || snapshot.dispatch?.commentId !== dispatch.commentId || snapshot.dispatch?.requestId !== dispatch.request.id || snapshot.dispatch?.digest !== dispatch.receipt.digest) return false;
|
|
129
|
+
const event = await reservationEvent(api, issue.number);
|
|
130
|
+
const start = Date.parse(run.run_started_at), end = Date.parse(run.updated_at), at = Date.parse(event?.created_at);
|
|
131
|
+
if (!Number.isSafeInteger(event?.id) || event.id < 1 || !Number.isFinite(start) || !Number.isFinite(end) || !Number.isFinite(at) || at < start || at > end || event.event !== 'labeled' || event.actor?.login !== 'github-actions[bot]' || event.actor.type !== 'Bot') return false;
|
|
132
|
+
const claim = { commentId: dispatch.commentId, requestId: dispatch.request.id, digest: dispatch.receipt.digest };
|
|
133
|
+
await checkDispatchClaim(api, repository, issue.number, claim, run.id);
|
|
134
|
+
const fresh = await api(`issues/${issue.number}`), current = await issueComments(api, issue.number), repo = await api('');
|
|
135
|
+
if (fresh.state !== 'open' || fresh.pull_request || fresh.user?.login !== owner || !fresh.labels.some(l => l.name === 'agent-working') || fresh.labels.some(l => ['human-working', 'future'].includes(l.name)) || intent(fresh, current, owner) !== dispatch.request.taskDigest || (await api(`commits/${encodeURIComponent(repo.default_branch)}`)).sha !== revision || (await api(`commits/${encodeURIComponent(dispatch.request.ref)}`)).sha !== workflowRevision) return false;
|
|
136
|
+
if (current.some(c => Number(c.id) > Number(dispatch.commentId) && c.user?.login === 'github-actions[bot]' && c.user.type === 'Bot' && c.body?.includes('<!-- lighthouse-dispatch:'))) return false;
|
|
137
|
+
if (current.some(c => c.user?.login === 'github-actions[bot]' && c.user.type === 'Bot' && c.body?.includes(`<!-- lighthouse-stop:${run.id}:${run.run_attempt} -->`))) return false;
|
|
138
|
+
if ((await reservationEvent(api, issue.number))?.id !== event.id) return false;
|
|
139
|
+
await checkDispatchClaim(api, repository, issue.number, claim, run.id);
|
|
140
|
+
await api(`issues/${issue.number}/labels/agent-working`, 'DELETE');
|
|
141
|
+
issue.labels = fresh.labels.filter(l => l.name !== 'agent-working');
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// A uniquely authenticated preparation failure can be recorded as failed only
|
|
146
|
+
// when the agent and publisher were skipped. It never authorizes a new dispatch.
|
|
147
|
+
export async function reconcilePreparationFailure(api, repository, owner, issueNumber, commentId, runId, readArtifact = readActionsJson) {
|
|
148
|
+
const run = await api(`actions/runs/${runId}`);
|
|
149
|
+
if (run.status !== 'completed' || !['failure', 'timed_out'].includes(run.conclusion) || run.run_attempt !== 1) return false;
|
|
150
|
+
const dispatch = await loadDispatch(api, repository, owner, issueNumber, commentId, run, readArtifact);
|
|
151
|
+
if (dispatch.receipt.workerRunId && String(dispatch.receipt.workerRunId) !== String(run.id) || !['requested', 'running', 'unknown', 'failed'].includes(dispatch.receipt.status) || dispatch.request.worker || !['triage', 'refine'].includes(dispatch.request.stage)) return false;
|
|
152
|
+
const inventory = await api(`actions/runs/${run.id}/attempts/${run.run_attempt}/jobs?per_page=100`);
|
|
153
|
+
if (!Array.isArray(inventory.jobs) || inventory.total_count !== inventory.jobs.length || inventory.total_count > 100) return false;
|
|
154
|
+
const prepare = inventory.jobs.find(job => job.name === 'Authorize and reserve issue');
|
|
155
|
+
const agent = inventory.jobs.find(job => job.name === 'agent');
|
|
156
|
+
const publish = inventory.jobs.find(job => job.name === 'Validate, publish and release');
|
|
157
|
+
if (!['failure', 'timed_out'].includes(prepare?.conclusion) || agent?.conclusion !== 'skipped' || publish?.conclusion !== 'skipped') return false;
|
|
158
|
+
const issue = await api(`issues/${issueNumber}`), comments = await issueComments(api, issueNumber), repo = await api('');
|
|
159
|
+
const revision = (await api(`commits/${encodeURIComponent(repo.default_branch)}`)).sha;
|
|
160
|
+
const workflowRevision = (await api(`commits/${encodeURIComponent(dispatch.request.ref)}`)).sha;
|
|
161
|
+
if (issue.state !== 'open' || issue.pull_request || issue.user?.login !== owner || issue.labels.some(label => ['agent-working', 'human-working', 'future', 'needs-info', 'waiting-review'].includes(label.name)) || dispatch.request.sourceRevision !== revision || dispatch.request.workflowRevision !== workflowRevision || intent(issue, comments, owner) !== dispatch.request.taskDigest || comments.some(comment => Number(comment.id) > Number(commentId) && comment.user?.login === 'github-actions[bot]' && comment.user.type === 'Bot' && comment.body?.includes('<!-- lighthouse-dispatch:'))) return false;
|
|
162
|
+
if (!dispatch.receipt.workerRunId && !await claimDispatch(api, repository, dispatch, run.id)) return false;
|
|
163
|
+
await finishDispatch(api, repository, issueNumber, commentId, run.id, 'failed');
|
|
164
|
+
await publishObservedStop(api, { repository, issue: issueNumber, stage: dispatch.request.stage, run: String(run.id), attempt: String(run.run_attempt), sourceRevision: dispatch.request.sourceRevision, dispatch: { commentId, requestId: dispatch.request.id, digest: dispatch.receipt.digest }, observation: 'preparation-failure' });
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Restore only a completed, read-only stage whose authority and label still match.
|
|
169
|
+
export async function reconcilePublishedStage(api, repository, owner, issue, comments, observation, readArtifact = readActionsJson) {
|
|
170
|
+
const { dispatch, run, jobs = [], revision, workflowRevision } = observation;
|
|
171
|
+
if (!dispatch || !run) return 'not-applicable';
|
|
172
|
+
const publication = jobs.find(j => j.name === 'Validate, publish and release');
|
|
173
|
+
const publish = publishStep(publication);
|
|
174
|
+
const failedPublication = run.conclusion === 'failure' && jobs.find(j => j.name === 'agent')?.conclusion === 'success' && publish?.conclusion === 'failure';
|
|
175
|
+
const interrupted = failedPublication && ['running', 'unknown'].includes(dispatch.receipt.status);
|
|
176
|
+
const completed = (run.conclusion === 'success' || failedPublication) && ['completed', 'waiting', 'blocked'].includes(dispatch.receipt.status);
|
|
177
|
+
if (run.status !== 'completed' || (!completed && !interrupted) || run.run_attempt !== 1 || !['triage', 'refine'].includes(dispatch.request.stage) || dispatch.request.worker) return 'not-applicable';
|
|
178
|
+
const id = digest(`${repository}:${issue.number}:${dispatch.request.stage}:${run.id}:${run.run_attempt}`);
|
|
179
|
+
const marker = `<!-- lighthouse-actions:${id} -->`;
|
|
180
|
+
if (!interrupted && comments.some(c => c.user?.login === 'github-actions[bot]' && c.user.type === 'Bot' && c.body?.includes(marker))) return 'present';
|
|
181
|
+
const incompatibleStage = l => ['triage', 'refine', 'propose', 'implement'].includes(l.name) && (!interrupted || l.name !== dispatch.request.stage);
|
|
182
|
+
if (issue.state !== 'open' || issue.pull_request || issue.user?.login !== owner || issue.labels.some(l => ['agent-working', 'human-working'].includes(l.name) || incompatibleStage(l)) || dispatch.request.sourceRevision !== revision || dispatch.request.workflowRevision !== workflowRevision || dispatch.request.taskDigest !== intent(issue, comments, owner)) return 'uncertain';
|
|
183
|
+
let snapshot, envelope;
|
|
184
|
+
try {
|
|
185
|
+
snapshot = await readArtifact(api, repository, run.id, `lighthouse-input-${issue.number}-${run.run_attempt}`, 'request.json');
|
|
186
|
+
envelope = await readArtifact(api, repository, run.id, `lighthouse-result-${issue.number}-${run.run_attempt}`, 'envelope.json');
|
|
187
|
+
} catch { return 'uncertain'; }
|
|
188
|
+
if (snapshot?.runId !== id || snapshot.issue !== issue.number || snapshot.stage !== dispatch.request.stage || String(snapshot.workflowRunId) !== String(run.id) || snapshot.sourceRevision !== revision || snapshot.workflowRevision !== workflowRevision || snapshot.taskDigest !== dispatch.request.taskDigest || snapshot.autoImplement !== (dispatch.request.autoImplement === true) || snapshot.dispatch?.requestId !== dispatch.request.id || snapshot.dispatch?.digest !== dispatch.receipt.digest || snapshot.dispatch?.commentId !== dispatch.commentId || validateStageEnvelope(envelope, snapshot).length) return 'uncertain';
|
|
189
|
+
const result = envelope.result;
|
|
190
|
+
if (!['passed', 'blocked'].includes(result.status)) return 'uncertain';
|
|
191
|
+
const question = result.status === 'blocked' ? pendingQuestion(result.output?.handoff?.question) : null;
|
|
192
|
+
const expectedStatus = result.status === 'passed' ? 'completed' : question ? 'waiting' : 'blocked';
|
|
193
|
+
if (!interrupted && dispatch.receipt.status !== expectedStatus) return 'uncertain';
|
|
194
|
+
const projection = resultHandoffState({ status: result.status, stage: snapshot.stage, question });
|
|
195
|
+
if (!projection.label || (!issue.labels.some(l => l.name === projection.label) && !(interrupted && issue.labels.some(l => l.name === snapshot.stage))) || issue.labels.some(l => ['ready', 'triaged', 'needs-info', 'waiting-review', 'execution-failed', 'pr-draft', 'pr-ready'].includes(l.name) && l.name !== projection.label)) return 'uncertain';
|
|
196
|
+
let action = question ? `Owner: ${question} Reply here.` : 'None.';
|
|
197
|
+
let next = question ? `The answer will be checked against this question before ${snapshot.stage} continues.` : 'Any continuation follows the configured authorization.';
|
|
198
|
+
if (snapshot.stage === 'refine' && result.status === 'passed') {
|
|
199
|
+
const paused = issue.labels.some(l => l.name === 'future');
|
|
200
|
+
action = !snapshot.autoImplement ? 'Owner: review the refinement and request the next authorized stage through the configured workflow.' : paused ? 'Owner: remove the future label when implementation may resume.' : 'None.';
|
|
201
|
+
next = !snapshot.autoImplement ? 'No automatic continuation is configured for this refinement.' : paused ? 'The workflow will recheck the authorized implementation request.' : 'The workflow will request implementation under the configured continuation policy; the owner can pause it with the future label.';
|
|
202
|
+
} else if (result.status === 'blocked' && !question) action = visibleNextAction(result.nextAction);
|
|
203
|
+
if (interrupted && snapshot.autoImplement && result.status === 'passed') { action = 'Owner: inspect the restored result and request the next authorized stage.'; next = 'The interrupted execution did not confirm an automatic continuation.'; }
|
|
204
|
+
const body = handoff({ status: projection.status, stage: snapshot.stage, happened: `A previous handoff was restored from retained execution evidence.\n\n${envelope.effect.body}`, action, next, details: `Outcome: ${result.status}\n\n[Workflow run](https://github.com/${repository}/actions/runs/${run.id}) · source \`${snapshot.sourceRevision}\`\n\n${marker}\n<!-- lighthouse-handoff:v1 -->\n<!-- lighthouse-stage:${snapshot.stage}:${result.status} -->${question ? '\n<!-- lighthouse-question -->' : ''}` });
|
|
205
|
+
await checkDispatchClaim(api, repository, issue.number, { commentId: dispatch.commentId, requestId: dispatch.request.id, digest: dispatch.receipt.digest }, run.id);
|
|
206
|
+
const fresh = await api(`issues/${issue.number}`), freshComments = await issueComments(api, issue.number);
|
|
207
|
+
const currentRepo = await api(''), currentRevision = (await api(`commits/${encodeURIComponent(currentRepo.default_branch)}`)).sha;
|
|
208
|
+
if (currentRevision !== revision || (await api(`commits/${encodeURIComponent(dispatch.request.ref)}`)).sha !== workflowRevision || fresh.state !== 'open' || fresh.user?.login !== owner || fresh.pull_request || fresh.labels.some(l => ['agent-working', 'human-working'].includes(l.name) || incompatibleStage(l)) || (!fresh.labels.some(l => l.name === projection.label) && !(interrupted && fresh.labels.some(l => l.name === snapshot.stage))) || fresh.labels.some(l => ['ready', 'triaged', 'needs-info', 'waiting-review', 'execution-failed', 'pr-draft', 'pr-ready'].includes(l.name) && l.name !== projection.label) || intent(fresh, freshComments, owner) !== dispatch.request.taskDigest) return 'uncertain';
|
|
209
|
+
if (freshComments.some(c => c.user?.login === 'github-actions[bot]' && Number(c.id) > Number(dispatch.commentId) && c.body?.includes('<!-- lighthouse-dispatch:'))) return 'uncertain';
|
|
210
|
+
if (interrupted && fresh.labels.some(l => l.name === snapshot.stage)) {
|
|
211
|
+
if (!fresh.labels.some(l => l.name === projection.label)) {
|
|
212
|
+
await checkDispatchClaim(api, repository, issue.number, { commentId: dispatch.commentId, requestId: dispatch.request.id, digest: dispatch.receipt.digest }, run.id);
|
|
213
|
+
await api(`issues/${issue.number}/labels`, 'POST', { labels: [projection.label] });
|
|
214
|
+
}
|
|
215
|
+
await checkDispatchClaim(api, repository, issue.number, { commentId: dispatch.commentId, requestId: dispatch.request.id, digest: dispatch.receipt.digest }, run.id);
|
|
216
|
+
await api(`issues/${issue.number}/labels/${snapshot.stage}`, 'DELETE');
|
|
217
|
+
}
|
|
218
|
+
if (freshComments.some(c => c.user?.login === 'github-actions[bot]' && c.body?.includes(marker))) {
|
|
219
|
+
if (interrupted) await finishDispatch(api, repository, issue.number, dispatch.commentId, run.id, question ? 'question' : result.status);
|
|
220
|
+
return 'present';
|
|
221
|
+
}
|
|
222
|
+
await checkDispatchClaim(api, repository, issue.number, { commentId: dispatch.commentId, requestId: dispatch.request.id, digest: dispatch.receipt.digest }, run.id);
|
|
223
|
+
await api(`issues/${issue.number}/comments`, 'POST', { body });
|
|
224
|
+
if (interrupted) await finishDispatch(api, repository, issue.number, dispatch.commentId, run.id, question ? 'question' : result.status);
|
|
225
|
+
return 'restored';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Restore an uncertain handoff only for the latest authenticated unattended attempt.
|
|
229
|
+
export async function reconcileUnattendedStop(api, repository, owner, issue, comments) {
|
|
230
|
+
if (issue.state !== 'open' || issue.pull_request || issue.user?.login !== owner) return 'not-applicable';
|
|
231
|
+
const relevant = comments.filter(c => c.user?.login === 'github-actions[bot]' && c.user.type === 'Bot' && /<!-- lighthouse-(?:unattended:|unattended-recovery:|dispatch:|actions:|stop:)/.test(c.body ?? ''));
|
|
232
|
+
const last = relevant.at(-1);
|
|
233
|
+
const heading = last?.body?.match(/^(?:## )?(?:Working|Requested|Queued) · (ff|hotfix)\b/);
|
|
234
|
+
const runMatch = last?.body?.match(/\[Execution\]\(https:\/\/github\.com\/[^/]+\/[^/]+\/actions\/runs\/([1-9][0-9]*)\)/);
|
|
235
|
+
if (!heading || !runMatch || !last.body.includes(`https://github.com/${repository}/actions/runs/${runMatch[1]}`)) return 'not-applicable';
|
|
236
|
+
const route = heading[1], runId = runMatch[1];
|
|
237
|
+
const marker = `<!-- lighthouse-unattended:${digest(JSON.stringify({ repository, issue: issue.number, route, run: runId }))} -->`;
|
|
238
|
+
if (!last.body.includes(marker)) return 'uncertain';
|
|
239
|
+
const run = await api(`actions/runs/${runId}`);
|
|
240
|
+
if (run?.repository?.full_name !== repository || run.path !== '.github/workflows/lighthouse-sdlc-unattended.yml' || run.event !== 'workflow_dispatch' || run.actor?.login !== owner || run.triggering_actor?.login !== owner || run.display_title !== `${route} #${issue.number} · ${owner}` || run.run_attempt !== 1) return 'uncertain';
|
|
241
|
+
if (run.status !== 'completed' || !['success', 'failure', 'timed_out', 'cancelled'].includes(run.conclusion)) return 'not-applicable';
|
|
242
|
+
const fresh = await api(`issues/${issue.number}`), latest = await issueComments(api, issue.number);
|
|
243
|
+
const current = latest.filter(c => c.user?.login === 'github-actions[bot]' && c.user.type === 'Bot' && /<!-- lighthouse-(?:unattended:|unattended-recovery:|dispatch:|actions:|stop:)/.test(c.body ?? '')).at(-1);
|
|
244
|
+
if (fresh.state !== 'open' || fresh.user?.login !== owner || current?.id !== last.id) return 'uncertain';
|
|
245
|
+
const reservation = fresh.labels.some(l => l.name === 'agent-working') ? ' The agent-working reservation remains; verify ownership before releasing it.' : '';
|
|
246
|
+
const body = handoff({ status: run.conclusion === 'success' ? 'unknown' : run.conclusion === 'cancelled' ? 'cancelled' : 'failed', stage: route, happened: `The unattended execution ended ${run.conclusion}, but its terminal handoff is missing. Publication and integration effects have not been confirmed.`, action: `Workflow maintainer: inspect the execution, artifacts, issue and any candidate PR before resuming.${reservation}`, next: 'Do not retry or release a reservation until the observed effects and current ownership are reconciled.', link: `[Execution](https://github.com/${repository}/actions/runs/${runId})`, details: `<!-- lighthouse-unattended-recovery:${runId}:1 -->` });
|
|
247
|
+
await api(`issues/${issue.number}/comments`, 'POST', { body });
|
|
248
|
+
return 'restored';
|
|
63
249
|
}
|
|
64
250
|
|
|
65
251
|
// Called again by the worker, before claiming a maintenance-created retry.
|
|
@@ -76,7 +262,7 @@ export async function verifyRecoveryOrigin(api, repository, owner, dispatch, par
|
|
|
76
262
|
if (JSON.stringify(originalFields(r)) !== JSON.stringify(originalFields(prior.request))) throw Error('Recovery changed the original authorization');
|
|
77
263
|
if (!revalidate) return;
|
|
78
264
|
const jobs = await api(`actions/runs/${priorRun.id}/attempts/${priorRun.run_attempt}/jobs?per_page=100`);
|
|
79
|
-
if (jobs.total_count > 100) throw Error('Execution job inventory exceeds recovery bounds');
|
|
265
|
+
if (!Array.isArray(jobs.jobs) || !Number.isSafeInteger(jobs.total_count) || jobs.total_count !== jobs.jobs.length || jobs.total_count > 100) throw Error('Execution job inventory is incomplete or exceeds recovery bounds');
|
|
80
266
|
const issue = await api(`issues/${r.issue}`), comments = await issueComments(api, r.issue), repo = await api('');
|
|
81
267
|
const revision = (await api(`commits/${encodeURIComponent(repo.default_branch)}`)).sha;
|
|
82
268
|
const workflowRevision = (await api(`commits/${encodeURIComponent(r.ref)}`)).sha;
|
|
@@ -86,15 +272,30 @@ export async function verifyRecoveryOrigin(api, repository, owner, dispatch, par
|
|
|
86
272
|
|
|
87
273
|
export async function prepareRecovery(api, repository, owner, issueNumber, parent, enabled, now = Date.now(), readArtifact = readActionsJson) {
|
|
88
274
|
const issue = await api(`issues/${issueNumber}`), comments = await issueComments(api, issueNumber);
|
|
275
|
+
await reconcileUnattendedStop(api, repository, owner, issue, comments);
|
|
89
276
|
const entries = comments.filter(c => c.user?.login === 'github-actions[bot]' && c.body.includes('<!-- lighthouse-dispatch:')).map(comment => ({ comment, receipt: readDispatchReceipt(comment, repository, issueNumber) }));
|
|
277
|
+
const unattended = comments.filter(c => c.user?.login === 'github-actions[bot]' && c.user.type === 'Bot' && c.body?.includes('<!-- lighthouse-unattended:')).at(-1);
|
|
278
|
+
if (unattended && (!entries.length || unattended.id > entries.at(-1).comment.id)) return { observations: [] };
|
|
90
279
|
const latest = new Map();
|
|
91
280
|
for (const entry of entries) latest.set(entry.receipt.recovery?.rootId ?? entry.receipt.id, entry);
|
|
92
281
|
const observations = [];
|
|
93
282
|
const currentRoot = entries.at(-1)?.receipt.recovery?.rootId ?? entries.at(-1)?.receipt.id;
|
|
94
283
|
for (const { comment } of latest.values()) {
|
|
95
284
|
const observation = await observeDispatch(api, repository, owner, comment, issue, comments, now, readArtifact);
|
|
96
|
-
const { dispatch, ...publicObservation } = observation;
|
|
285
|
+
const { dispatch, stop, run, jobs, revision, workflowRevision, ...publicObservation } = observation;
|
|
286
|
+
if (observation.rootId === currentRoot) {
|
|
287
|
+
if (await reconcileAbandonedReservation(api, repository, owner, issue, comments, observation, readArtifact)) Object.assign(publicObservation, { state: 'reconciled', code: 'reservation-released', nextAction: 'Follow the retained outcome before requesting further work.' });
|
|
288
|
+
const preparationRun = observation.observedRunId ?? run?.id;
|
|
289
|
+
if (preparationRun && await reconcilePreparationFailure(api, repository, owner, issueNumber, comment.id, preparationRun, readArtifact)) { observation.stop = null; Object.assign(publicObservation, { state: 'reconciled', code: 'preparation-failed', nextAction: 'Inspect the confirmed failed execution before requesting new work.' }); }
|
|
290
|
+
const reconciliation = await reconcilePublishedStage(api, repository, owner, issue, comments, observation, readArtifact);
|
|
291
|
+
if (reconciliation === 'restored' || reconciliation === 'present') {
|
|
292
|
+
if (observation.stop?.observation !== 'cleanup-failure') observation.stop = null;
|
|
293
|
+
if (run?.conclusion === 'failure') Object.assign(publicObservation, { state: 'reconciled', code: 'handoff-restored', nextAction: 'Follow the restored handoff.' });
|
|
294
|
+
}
|
|
295
|
+
if (reconciliation === 'uncertain') Object.assign(publicObservation, { state: 'uncertain', code: 'handoff-unconfirmed', nextAction: 'Inspect the retained result and current issue state before restoring its handoff.' });
|
|
296
|
+
}
|
|
97
297
|
observations.push(publicObservation);
|
|
298
|
+
if (observation.stop && observation.rootId === currentRoot) await publishObservedStop(api, { repository, issue: issueNumber, stage: dispatch.request.stage, ...observation.stop, sourceRevision: dispatch.request.sourceRevision, dispatch: { commentId: dispatch.commentId, requestId: dispatch.request.id, digest: dispatch.receipt.digest } });
|
|
98
299
|
if (observation.state !== 'retryable' || enabled !== true || observation.rootId !== currentRoot) continue;
|
|
99
300
|
const attempt = observation.attempt + 1;
|
|
100
301
|
const request = { ...dispatch.request, id: recoveryId(repository, observation.rootId, attempt), routerRunId: String(parent.id), routerAttempt: parent.run_attempt, routerRevision: parent.head_sha, routerRef: parent.head_branch, routerEventName: parent.event, recovery: { rootId: observation.rootId, previousComment: comment.id, attempt } };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"
|
|
1
|
+
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6e978e33b557d99c929fbd67778258832f1938ec206c0624ee4cf9f5da7f88ff","body_hash":"900316ff8535f6f9b38fea05138e4e8aa8ed64b3d4844ebad51810faf4bddea5","compiler_version":"v0.93.2","agent_id":"copilot","engine_versions":{"copilot":"1.0.101"},"agent_image_runner":"[\"self-hosted\",\"linux\",\"lighthouse-workflow\"]"}
|
|
2
2
|
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/checkout","sha":"fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09","version":"v5"},{"repo":"actions/download-artifact","sha":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"949feb2413d6458794dcd2491c4babbbce0c15c1","version":"v7.1.0"},{"repo":"actions/setup-node","sha":"a0853c24544627f65ddf259abe73b1d18a591444","version":"v5"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"},{"repo":"github/gh-aw-actions/setup","sha":"d9128eb3ea40c18b77c76184eda03c0ff2608d79","version":"v0.93.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.70","digest":"sha256:b15ab48cbbab02904eb9bbd359a398db59690eebbea1e6934d47ed2bb407dd34","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.70@sha256:b15ab48cbbab02904eb9bbd359a398db59690eebbea1e6934d47ed2bb407dd34"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.70","digest":"sha256:9ac8f9c7294e892c916de3da9d2588dfd8cd4931e5c39b00143266d7ed9fd52a","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.70@sha256:9ac8f9c7294e892c916de3da9d2588dfd8cd4931e5c39b00143266d7ed9fd52a"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.70","digest":"sha256:b5bc53f4becfd96e926e63c83831f1b3c9512f713c83161d2b8c9b7204b3e917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.70@sha256:b5bc53f4becfd96e926e63c83831f1b3c9512f713c83161d2b8c9b7204b3e917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.37","digest":"sha256:9c7f242f6dec2d7d7b55d4c9e07872d59b85377c6cb3de46c915a5d1ffb91442","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.37@sha256:9c7f242f6dec2d7d7b55d4c9e07872d59b85377c6cb3de46c915a5d1ffb91442"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:41835613992df88f51e923bad486f96dfb2e6cda29f13e217df41833284ae382","pinned_image":"ghcr.io/github/gh-aw-node@sha256:41835613992df88f51e923bad486f96dfb2e6cda29f13e217df41833284ae382"},{"image":"ghcr.io/github/github-mcp-server:v2.0.2","digest":"sha256:ffced0d76e77428532a2ced185516992d77fd146d69eec2f244e6237d1a97796","pinned_image":"ghcr.io/github/github-mcp-server:v2.0.2@sha256:ffced0d76e77428532a2ced185516992d77fd146d69eec2f244e6237d1a97796"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["noop","report_missing_data","report_missing_tool"]}],"threat_detection":{"mode":"disabled"},"admission":false}
|
|
3
3
|
# This file was automatically generated by gh-aw (v0.93.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
|
|
4
4
|
#
|
|
@@ -112,7 +112,7 @@ jobs:
|
|
|
112
112
|
needs:
|
|
113
113
|
- pre_activation
|
|
114
114
|
- prepare
|
|
115
|
-
if: needs.pre_activation.outputs.activated == 'true' && (
|
|
115
|
+
if: needs.pre_activation.outputs.activated == 'true' && (false)
|
|
116
116
|
runs-on: ubuntu-24.04
|
|
117
117
|
# Permissions for the activation job (workflow permissions default to none).
|
|
118
118
|
permissions:
|
|
@@ -454,9 +454,10 @@ jobs:
|
|
|
454
454
|
needs:
|
|
455
455
|
- activation
|
|
456
456
|
- apply
|
|
457
|
+
- finalize
|
|
457
458
|
- prepare
|
|
458
459
|
- publish
|
|
459
|
-
if:
|
|
460
|
+
if: needs.activation.outputs.daily_ai_credits_exceeded != 'true'
|
|
460
461
|
runs-on:
|
|
461
462
|
- self-hosted
|
|
462
463
|
- linux
|
|
@@ -1260,10 +1261,13 @@ jobs:
|
|
|
1260
1261
|
GH_HOST="${GITHUB_SERVER_URL#https://}"
|
|
1261
1262
|
GH_HOST="${GH_HOST#http://}"
|
|
1262
1263
|
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
|
|
1263
|
-
-
|
|
1264
|
+
- name: Load the approved source without credentials
|
|
1265
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
1264
1266
|
with:
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
+
name: lighthouse-product-apply-source-${{ inputs.issue }}-${{ github.run_attempt }}
|
|
1268
|
+
path: ${{ runner.temp }}/product-source
|
|
1269
|
+
- name: Restore the approved source
|
|
1270
|
+
run: tar -xzf "$RUNNER_TEMP/product-source/lighthouse-product-source.tar.gz" -C "$GITHUB_WORKSPACE"
|
|
1267
1271
|
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
|
1268
1272
|
with:
|
|
1269
1273
|
path: .lighthouse-harness
|
|
@@ -1277,6 +1281,7 @@ jobs:
|
|
|
1277
1281
|
run: |
|
|
1278
1282
|
npm ci --ignore-scripts
|
|
1279
1283
|
npm install --no-save --package-lock=false --ignore-scripts @prodshape/integration-openspec@0.6.6
|
|
1284
|
+
working-directory: .lighthouse-harness
|
|
1280
1285
|
- name: Load the approval checkpoint
|
|
1281
1286
|
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
1282
1287
|
with:
|
|
@@ -1294,6 +1299,13 @@ jobs:
|
|
|
1294
1299
|
${{ runner.temp }}/lighthouse-${{ github.run_id }}-${{ github.run_attempt }}/apply-result.json
|
|
1295
1300
|
${{ runner.temp }}/lighthouse-${{ github.run_id }}-${{ github.run_attempt }}/affected-citations.json
|
|
1296
1301
|
retention-days: ${{ vars.GH_AW_DEFAULT_ARTIFACT_RETENTION_DAYS || '30' }}
|
|
1302
|
+
- name: Preserve the bounded apply proof
|
|
1303
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
1304
|
+
with:
|
|
1305
|
+
if-no-files-found: error
|
|
1306
|
+
name: lighthouse-product-apply-proof-${{ inputs.issue }}-${{ github.run_attempt }}
|
|
1307
|
+
path: ${{ runner.temp }}/lighthouse-${{ github.run_id }}-${{ github.run_attempt }}/apply-proof.json
|
|
1308
|
+
retention-days: ${{ vars.GH_AW_DEFAULT_ARTIFACT_RETENTION_DAYS || '30' }}
|
|
1297
1309
|
|
|
1298
1310
|
conclusion:
|
|
1299
1311
|
name: conclusion
|
|
@@ -1301,6 +1313,7 @@ jobs:
|
|
|
1301
1313
|
- activation
|
|
1302
1314
|
- agent
|
|
1303
1315
|
- apply
|
|
1316
|
+
- finalize
|
|
1304
1317
|
- prepare
|
|
1305
1318
|
- publish
|
|
1306
1319
|
- safe_outputs
|
|
@@ -1319,9 +1332,9 @@ jobs:
|
|
|
1319
1332
|
cancel-in-progress: false
|
|
1320
1333
|
queue: max
|
|
1321
1334
|
env:
|
|
1322
|
-
GH_AW_JOB_DISPLAY_NAMES: "{\"activation\":\"activation\",\"agent\":\"agent\",\"apply\":\"Apply with the pinned ProductShape provider\",\"conclusion\":\"conclusion\",\"pre_activation\":\"pre_activation\",\"prepare\":\"Recheck product approval\",\"publish\":\"Recheck and publish provider output\",\"safe_outputs\":\"safe_outputs\"}"
|
|
1323
|
-
GH_AW_JOB_RESULTS: "{\"activation\":{\"result\":\"${{ needs.activation.result }}\"},\"agent\":{\"result\":\"${{ needs.agent.result }}\"},\"apply\":{\"result\":\"${{ needs.apply.result }}\"},\"prepare\":{\"result\":\"${{ needs.prepare.result }}\"},\"publish\":{\"result\":\"${{ needs.publish.result }}\"},\"safe_outputs\":{\"result\":\"${{ needs.safe_outputs.result }}\"}}"
|
|
1324
|
-
GH_AW_JOB_TIMING_ELIGIBLE: "{\"activation\":true,\"agent\":true,\"apply\":true,\"conclusion\":true,\"pre_activation\":true,\"prepare\":true,\"publish\":true,\"safe_outputs\":true}"
|
|
1335
|
+
GH_AW_JOB_DISPLAY_NAMES: "{\"activation\":\"activation\",\"agent\":\"agent\",\"apply\":\"Apply with the pinned ProductShape provider\",\"conclusion\":\"conclusion\",\"finalize\":\"Report an interrupted product apply\",\"pre_activation\":\"pre_activation\",\"prepare\":\"Recheck product approval\",\"publish\":\"Recheck and publish provider output\",\"safe_outputs\":\"safe_outputs\"}"
|
|
1336
|
+
GH_AW_JOB_RESULTS: "{\"activation\":{\"result\":\"${{ needs.activation.result }}\"},\"agent\":{\"result\":\"${{ needs.agent.result }}\"},\"apply\":{\"result\":\"${{ needs.apply.result }}\"},\"finalize\":{\"result\":\"${{ needs.finalize.result }}\"},\"prepare\":{\"result\":\"${{ needs.prepare.result }}\"},\"publish\":{\"result\":\"${{ needs.publish.result }}\"},\"safe_outputs\":{\"result\":\"${{ needs.safe_outputs.result }}\"}}"
|
|
1337
|
+
GH_AW_JOB_TIMING_ELIGIBLE: "{\"activation\":true,\"agent\":true,\"apply\":true,\"conclusion\":true,\"finalize\":true,\"pre_activation\":true,\"prepare\":true,\"publish\":true,\"safe_outputs\":true}"
|
|
1325
1338
|
GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }}
|
|
1326
1339
|
outputs:
|
|
1327
1340
|
noop_comment_body: ${{ steps.noop.outputs.noop_comment_body }}
|
|
@@ -1592,9 +1605,52 @@ jobs:
|
|
|
1592
1605
|
overwrite: true
|
|
1593
1606
|
retention-days: ${{ vars.GH_AW_DEFAULT_ARTIFACT_RETENTION_DAYS || '0' }}
|
|
1594
1607
|
|
|
1608
|
+
finalize:
|
|
1609
|
+
name: Report an interrupted product apply
|
|
1610
|
+
needs:
|
|
1611
|
+
- apply
|
|
1612
|
+
- prepare
|
|
1613
|
+
- publish
|
|
1614
|
+
if: always()
|
|
1615
|
+
runs-on: ubuntu-24.04
|
|
1616
|
+
# Permissions for the finalize job (workflow permissions default to none).
|
|
1617
|
+
permissions:
|
|
1618
|
+
actions: read
|
|
1619
|
+
contents: read
|
|
1620
|
+
issues: write
|
|
1621
|
+
pull-requests: write
|
|
1622
|
+
env:
|
|
1623
|
+
LIGHTHOUSE_APPLY_RESULT: ${{ needs.apply.result }}
|
|
1624
|
+
LIGHTHOUSE_ISSUE: ${{ inputs.issue }}
|
|
1625
|
+
LIGHTHOUSE_PREPARE_RESULT: ${{ needs.prepare.result }}
|
|
1626
|
+
LIGHTHOUSE_PUBLISH_RESULT: ${{ needs.publish.result }}
|
|
1627
|
+
steps:
|
|
1628
|
+
- name: Configure GH_HOST for enterprise compatibility
|
|
1629
|
+
id: ghes-host-config
|
|
1630
|
+
shell: bash
|
|
1631
|
+
run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input.
|
|
1632
|
+
# Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
|
|
1633
|
+
# GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
|
|
1634
|
+
GH_HOST="${GITHUB_SERVER_URL#https://}"
|
|
1635
|
+
GH_HOST="${GH_HOST#http://}"
|
|
1636
|
+
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
|
|
1637
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
|
1638
|
+
with:
|
|
1639
|
+
persist-credentials: false
|
|
1640
|
+
ref: __LIGHTHOUSE_REF__
|
|
1641
|
+
repository: juangcarmona/lighthouse
|
|
1642
|
+
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
|
1643
|
+
with:
|
|
1644
|
+
node-version: 24
|
|
1645
|
+
- name: Publish the authenticated outcome
|
|
1646
|
+
run: node src/packages/sdlc/src/issue-actions.mjs product-apply-cleanup
|
|
1647
|
+
env:
|
|
1648
|
+
GH_TOKEN: ${{ github.token }}
|
|
1649
|
+
|
|
1595
1650
|
pre_activation:
|
|
1596
1651
|
name: pre_activation
|
|
1597
1652
|
needs: prepare
|
|
1653
|
+
if: false
|
|
1598
1654
|
runs-on: ubuntu-24.04
|
|
1599
1655
|
env:
|
|
1600
1656
|
GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }}
|
|
@@ -1626,7 +1682,7 @@ jobs:
|
|
|
1626
1682
|
permissions:
|
|
1627
1683
|
actions: read
|
|
1628
1684
|
contents: read
|
|
1629
|
-
issues:
|
|
1685
|
+
issues: write
|
|
1630
1686
|
pull-requests: read
|
|
1631
1687
|
env:
|
|
1632
1688
|
LIGHTHOUSE_DISPATCH_REQUIRED: true
|
|
@@ -1668,6 +1724,30 @@ jobs:
|
|
|
1668
1724
|
name: lighthouse-product-apply-input-${{ inputs.issue }}-${{ github.run_attempt }}
|
|
1669
1725
|
path: ${{ runner.temp }}/lighthouse-${{ github.run_id }}-${{ github.run_attempt }}/product-apply.json
|
|
1670
1726
|
retention-days: ${{ vars.GH_AW_DEFAULT_ARTIFACT_RETENTION_DAYS || '14' }}
|
|
1727
|
+
- name: Check out the approved private source without persisted credentials
|
|
1728
|
+
if: steps.prepare.outputs.eligible == 'true'
|
|
1729
|
+
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
|
1730
|
+
with:
|
|
1731
|
+
fetch-depth: 0
|
|
1732
|
+
path: product-source
|
|
1733
|
+
persist-credentials: false
|
|
1734
|
+
ref: ${{ steps.prepare.outputs.head }}
|
|
1735
|
+
- name: Preserve the approved source for the credential-free provider
|
|
1736
|
+
if: steps.prepare.outputs.eligible == 'true'
|
|
1737
|
+
run: |
|
|
1738
|
+
test "$(git -C product-source rev-parse HEAD)" = "$APPROVED_HEAD"
|
|
1739
|
+
if git -C product-source config --local --name-only --get-regexp '^http\..*\.extraheader$' >/dev/null; then exit 1; fi
|
|
1740
|
+
tar -czf "$RUNNER_TEMP/lighthouse-product-source.tar.gz" -C product-source .
|
|
1741
|
+
env:
|
|
1742
|
+
APPROVED_HEAD: ${{ steps.prepare.outputs.head }}
|
|
1743
|
+
- name: Upload the approved source
|
|
1744
|
+
if: steps.prepare.outputs.eligible == 'true'
|
|
1745
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
1746
|
+
with:
|
|
1747
|
+
if-no-files-found: error
|
|
1748
|
+
name: lighthouse-product-apply-source-${{ inputs.issue }}-${{ github.run_attempt }}
|
|
1749
|
+
path: ${{ runner.temp }}/lighthouse-product-source.tar.gz
|
|
1750
|
+
retention-days: ${{ vars.GH_AW_DEFAULT_ARTIFACT_RETENTION_DAYS || '14' }}
|
|
1671
1751
|
|
|
1672
1752
|
publish:
|
|
1673
1753
|
name: Recheck and publish provider output
|
|
@@ -1678,7 +1758,7 @@ jobs:
|
|
|
1678
1758
|
runs-on: ubuntu-24.04
|
|
1679
1759
|
# Permissions for the publish job (workflow permissions default to none).
|
|
1680
1760
|
permissions:
|
|
1681
|
-
actions:
|
|
1761
|
+
actions: write
|
|
1682
1762
|
contents: write
|
|
1683
1763
|
issues: write
|
|
1684
1764
|
pull-requests: write
|
|
@@ -1717,6 +1797,12 @@ jobs:
|
|
|
1717
1797
|
with:
|
|
1718
1798
|
name: lighthouse-product-apply-result-${{ inputs.issue }}-${{ github.run_attempt }}
|
|
1719
1799
|
path: ${{ runner.temp }}/lighthouse-${{ github.run_id }}-${{ github.run_attempt }}
|
|
1800
|
+
- name: Load the bounded apply proof
|
|
1801
|
+
if: needs.apply.result == 'success'
|
|
1802
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
1803
|
+
with:
|
|
1804
|
+
name: lighthouse-product-apply-proof-${{ inputs.issue }}-${{ github.run_attempt }}
|
|
1805
|
+
path: ${{ runner.temp }}/lighthouse-${{ github.run_id }}-${{ github.run_attempt }}
|
|
1720
1806
|
- name: Stop when the provider apply did not complete
|
|
1721
1807
|
if: needs.apply.result != 'success'
|
|
1722
1808
|
run: exit 1
|