@juangcarmona/lighthouse-sdlc 0.18.6 → 0.18.7
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/package.json +1 -1
- package/src/pr-state-actions.mjs +14 -3
- package/src/pr-state.mjs +20 -2
package/package.json
CHANGED
package/src/pr-state-actions.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { checkImplementationPullRequest } from './proposal-review.mjs';
|
|
3
|
-
import { authorizedCiCallback, isCurrentPullRequestEvent, productOnlyCheckpoint, isStaleProductDecision, projectImplementationPrState, reconcileImplementationPr, scheduledImplementationPulls, setPullRequestDraft } from './pr-state.mjs';
|
|
3
|
+
import { authorizedCiCallback, draftIntentMarker, isCurrentPullRequestEvent, pendingDraftIntent, productOnlyCheckpoint, isStaleProductDecision, projectImplementationPrState, reconcileImplementationPr, scheduledImplementationPulls, setPullRequestDraft } from './pr-state.mjs';
|
|
4
4
|
import { handoff } from './handoff.mjs';
|
|
5
5
|
import { productProposalProof } from './product-change.mjs';
|
|
6
6
|
|
|
@@ -113,8 +113,19 @@ async function main() {
|
|
|
113
113
|
}
|
|
114
114
|
const hold = comments.some(comment => comment.user?.login === 'github-actions[bot]' && comment.body?.includes(`<!-- lighthouse-pr-hold:${pr.number}:${pr.head.sha} -->`));
|
|
115
115
|
const auto = (['workflow_run', 'schedule'].includes(env.GITHUB_EVENT_NAME) || ciCallback) && !hold;
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
await reconcileImplementationPr(api, repo, pr, issue, comments, requiredChecks(profile), (current, draft) => setPullRequestDraft(env.GH_TOKEN, current, draft), { auto, beforeDraft: async () => {
|
|
117
|
+
const marker = draftIntentMarker(pr, env.GITHUB_RUN_ID);
|
|
118
|
+
if (!(await commentsFor(api, target.issue)).some(comment => comment.user?.login === 'github-actions[bot]' && comment.user.type === 'Bot' && comment.body?.includes(marker))) await api(`issues/${target.issue}/comments`, 'POST', { body: marker });
|
|
119
|
+
} });
|
|
120
|
+
const fresh = await api(`pulls/${number}`);
|
|
121
|
+
if (fresh.state !== 'open' || !fresh.draft || fresh.head.sha !== pr.head.sha) continue;
|
|
122
|
+
const currentComments = await commentsFor(api, target.issue), notice = pendingDraftIntent(currentComments, fresh);
|
|
123
|
+
if (!notice) continue;
|
|
124
|
+
const currentIssue = await api(`issues/${target.issue}`);
|
|
125
|
+
if (currentIssue.state !== 'open') continue;
|
|
126
|
+
const current = await reconcileImplementationPr(api, repo, fresh, currentIssue, currentComments, requiredChecks(profile), (currentPr, draft) => setPullRequestDraft(env.GH_TOKEN, currentPr, draft));
|
|
127
|
+
const confirmed = await api(`pulls/${number}`);
|
|
128
|
+
if (current.status === 'draft' && current.reason !== 'PR is a draft' && confirmed.state === 'open' && confirmed.draft && confirmed.head.sha === fresh.head.sha && confirmed.head.ref === fresh.head.ref && confirmed.base.ref === fresh.base.ref && confirmed.body === fresh.body && !(await commentsFor(api, target.issue)).some(comment => comment.user?.login === 'github-actions[bot]' && comment.user.type === 'Bot' && comment.body?.includes(notice))) await api(`issues/${target.issue}/comments`, 'POST', { body: handoff({ status: 'blocked', stage: 'implement', happened: `PR #${pr.number} remains a draft: ${current.reason}.`, action: 'The PR owner should resolve the stated blocker.', next: 'The workflow rechecks readiness when the relevant checks or PR state change.', link: `[Open PR](${pr.html_url})`, details: notice }) });
|
|
118
129
|
}
|
|
119
130
|
}
|
|
120
131
|
main().catch(error => { console.error(`::error::${error.message}`); process.exitCode = 1; });
|
package/src/pr-state.mjs
CHANGED
|
@@ -3,6 +3,23 @@ import { checkCommit } from './completion.mjs';
|
|
|
3
3
|
import { validateProductCheckpoint, productProposalProof } from './product-change.mjs';
|
|
4
4
|
|
|
5
5
|
const marker = /<!-- lighthouse-pr-state:(\{[^\n]*\}) -->/;
|
|
6
|
+
const draftIntent = /<!-- lighthouse-pr-draft-intent:([1-9][0-9]*):([a-f0-9]{40}):([1-9][0-9]*) -->/;
|
|
7
|
+
|
|
8
|
+
export const draftIntentMarker = (pr, run) => {
|
|
9
|
+
if (!Number.isSafeInteger(pr?.number) || pr.number < 1 || !/^[a-f0-9]{40}$/.test(pr.head?.sha ?? '') || !/^[1-9][0-9]*$/.test(String(run ?? ''))) throw Error('Invalid draft handoff identity');
|
|
10
|
+
return `<!-- lighthouse-pr-draft-intent:${pr.number}:${pr.head.sha}:${run} -->`;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function pendingDraftIntent(comments, pr) {
|
|
14
|
+
for (const comment of [...comments].reverse()) {
|
|
15
|
+
if (comment.user?.login !== 'github-actions[bot]' || comment.user.type !== 'Bot') continue;
|
|
16
|
+
const match = comment.body?.match(draftIntent);
|
|
17
|
+
if (!match || Number(match[1]) !== pr.number || match[2] !== pr.head.sha) continue;
|
|
18
|
+
const done = `<!-- lighthouse-pr-draft-outcome:${match[1]}:${match[2]}:${match[3]} -->`;
|
|
19
|
+
return comments.some(item => item.user?.login === 'github-actions[bot]' && item.user.type === 'Bot' && item.body?.includes(done)) ? null : done;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
6
23
|
|
|
7
24
|
export function isCurrentPullRequestEvent(action, pr) {
|
|
8
25
|
if (action === 'converted_to_draft') return pr?.state === 'open' && pr.draft === true;
|
|
@@ -76,7 +93,7 @@ export async function projectImplementationPrState(api, issue, draft) {
|
|
|
76
93
|
if (!current.labels.some(item => item.name === wanted)) await api(`issues/${issue}/labels`, 'POST', { labels: [wanted] });
|
|
77
94
|
}
|
|
78
95
|
|
|
79
|
-
export async function reconcileImplementationPr(api, repo, pr, issue, comments, requiredChecks, setDraft, { auto = false } = {}) {
|
|
96
|
+
export async function reconcileImplementationPr(api, repo, pr, issue, comments, requiredChecks, setDraft, { auto = false, beforeDraft = async () => {} } = {}) {
|
|
80
97
|
const target = checkImplementationPullRequest(pr, repo);
|
|
81
98
|
if (target.issue !== issue.number || issue.state !== 'open') return { status: 'ignored', reason: 'Issue or PR changed' };
|
|
82
99
|
const proof = implementationPrProof(comments, pr);
|
|
@@ -90,11 +107,12 @@ export async function reconcileImplementationPr(api, repo, pr, issue, comments,
|
|
|
90
107
|
return { status: 'ready', reason: 'Candidate and required checks passed' };
|
|
91
108
|
}
|
|
92
109
|
if (!pr.draft && !complete) {
|
|
110
|
+
await beforeDraft();
|
|
93
111
|
try { await setDraft(pr, true); }
|
|
94
112
|
catch (error) { if (!(await api(`pulls/${pr.number}`)).draft) throw error; }
|
|
95
113
|
await projectImplementationPrState(api, issue.number, true);
|
|
96
114
|
return { status: 'draft', reason: reserved ? 'Implementation or owner work is still active' : !proof?.complete ? 'Implementation is incomplete' : 'Required checks have not passed' };
|
|
97
115
|
}
|
|
98
116
|
await projectImplementationPrState(api, issue.number, pr.draft);
|
|
99
|
-
return { status: pr.draft ? 'draft' : 'ready', reason: pr.draft ? 'PR is a draft' : 'Candidate and required checks passed' };
|
|
117
|
+
return { status: pr.draft ? 'draft' : 'ready', reason: pr.draft && !complete ? reserved ? 'Implementation or owner work is still active' : !proof?.complete ? 'Implementation is incomplete' : 'Required checks have not passed' : pr.draft ? 'PR is a draft' : 'Candidate and required checks passed' };
|
|
100
118
|
}
|