@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juangcarmona/lighthouse-sdlc",
3
- "version": "0.18.6",
3
+ "version": "0.18.7",
4
4
  "type": "module",
5
5
  "description": "Provider-neutral SDLC skills, commands and route contracts for Lighthouse adopters",
6
6
  "repository": {
@@ -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
- const result = await reconcileImplementationPr(api, repo, pr, issue, comments, requiredChecks(profile), (current, draft) => setPullRequestDraft(env.GH_TOKEN, current, draft), { auto });
117
- if (event.action === 'ready_for_review' && result.status === 'draft') await api(`issues/${target.issue}/comments`, 'POST', { body: handoff({ status: 'blocked', stage: 'implement', happened: `PR #${pr.number} remains a draft: ${result.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})` }) });
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
  }