create-agentic-pdlc 2.2.1 → 2.4.0
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/.agentic-pdlc/SETUP_PROMPT.md +3 -4
- package/.agentic-pdlc/metrics/raw/2026-W18.jsonl +2 -0
- package/.agentic-pdlc/metrics/raw/2026-W21.jsonl +68 -0
- package/.agentic-pdlc/metrics/raw/2026-W22.jsonl +114 -0
- package/.agentic-setup-prompt.md +3 -4
- package/.agentic-setup.md +3 -4
- package/.github/ISSUE_TEMPLATE/bug.md +53 -0
- package/.github/ISSUE_TEMPLATE/feature.md +54 -0
- package/.github/ISSUE_TEMPLATE/task.md +33 -0
- package/.github/workflows/add-to-board.yml +1 -1
- package/.github/workflows/agent-trigger.yml +4 -4
- package/.github/workflows/agentic-metrics.yml +171 -38
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/npm-publish.yml +2 -2
- package/.github/workflows/pdlc-health-check.yml +1 -3
- package/.github/workflows/pdlc-stage-gate.yml +2 -2
- package/.github/workflows/project-automation.yml +79 -16
- package/.github/workflows/qa-agent.yml +26 -15
- package/.github/workflows/qa-gate.yml +51 -0
- package/AGENTS.md +50 -8
- package/CLAUDE.md +53 -3
- package/SETUP.md +4 -1
- package/adapters/claude-code/skill.md +44 -20
- package/adapters/hooks/pdlc-stage-gate.sh +2 -7
- package/bin/cli.js +41 -9
- package/docs/flow.md +8 -21
- package/docs/pdlc.md +24 -16
- package/docs/superpowers/plans/2026-05-28-jules-label-pat-split.md +240 -0
- package/docs/superpowers/plans/2026-05-29-agentic-pulse-rework-taxonomy.md +474 -0
- package/docs/superpowers/plans/2026-05-29-qa-gate-enforcement.md +354 -0
- package/docs/superpowers/specs/2026-05-29-agentic-pulse-rework-taxonomy-design.md +122 -0
- package/package.json +1 -1
- package/templates/.github/ISSUE_TEMPLATE/bug.md +53 -0
- package/templates/.github/ISSUE_TEMPLATE/feature.md +54 -0
- package/templates/.github/ISSUE_TEMPLATE/task.md +33 -0
- package/templates/.github/workflows/add-to-board.yml +4 -4
- package/templates/.github/workflows/agent-trigger.yml +24 -15
- package/{.agentic-pdlc/templates → templates}/.github/workflows/agentic-metrics.yml +166 -36
- package/templates/.github/workflows/ci.yml +15 -1
- package/templates/.github/workflows/pdlc-health-check.yml +1 -3
- package/templates/.github/workflows/pdlc-stage-gate.yml +2 -2
- package/templates/.github/workflows/project-automation.yml +93 -36
- package/templates/.github/workflows/qa-agent.yml +33 -17
- package/templates/.github/workflows/qa-gate.yml +51 -0
- package/templates/AGENTS.md +74 -23
- package/templates/docs/pdlc.md +24 -16
- package/.agentic-pdlc/templates/.github/CODEOWNERS +0 -5
- package/.agentic-pdlc/templates/.github/copilot-instructions.md +0 -12
- package/.agentic-pdlc/templates/.github/workflows/add-to-board.yml +0 -38
- package/.agentic-pdlc/templates/.github/workflows/agent-trigger.yml +0 -146
- package/.agentic-pdlc/templates/.github/workflows/auto-approve.yml +0 -16
- package/.agentic-pdlc/templates/.github/workflows/ci.yml +0 -40
- package/.agentic-pdlc/templates/.github/workflows/pdlc-health-check.yml +0 -123
- package/.agentic-pdlc/templates/.github/workflows/pdlc-stage-gate.yml +0 -51
- package/.agentic-pdlc/templates/.github/workflows/project-automation.yml +0 -278
- package/.agentic-pdlc/templates/.github/workflows/protect-workflows.yml +0 -21
- package/.agentic-pdlc/templates/.github/workflows/qa-agent.yml +0 -128
- package/.agentic-pdlc/templates/AGENTS.md +0 -81
- package/.agentic-pdlc/templates/docs/pdlc.md +0 -115
|
@@ -9,15 +9,20 @@ permissions:
|
|
|
9
9
|
contents: write
|
|
10
10
|
issues: write
|
|
11
11
|
|
|
12
|
+
env:
|
|
13
|
+
AGENTIC_PULSE_REVIEWERS: |
|
|
14
|
+
code_reviewer=gemini-code-assist[bot]
|
|
15
|
+
qa_agent=github-actions[bot]
|
|
16
|
+
|
|
12
17
|
jobs:
|
|
13
18
|
generate-pulse:
|
|
14
19
|
name: Generate Weekly Agentic Pulse
|
|
15
20
|
runs-on: ubuntu-latest
|
|
16
21
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
22
|
+
- uses: actions/checkout@v5.0.1
|
|
18
23
|
|
|
19
24
|
- name: Collect Stage Residence Time
|
|
20
|
-
uses: actions/github-script@
|
|
25
|
+
uses: actions/github-script@v8
|
|
21
26
|
with:
|
|
22
27
|
script: |
|
|
23
28
|
const fs = require('fs');
|
|
@@ -25,7 +30,7 @@ jobs:
|
|
|
25
30
|
const { owner, repo } = context.repo;
|
|
26
31
|
const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
27
32
|
const STAGE_LABELS = new Set([
|
|
28
|
-
'stage:
|
|
33
|
+
'stage:brainstorming', 'stage:detailing',
|
|
29
34
|
'stage:approval', 'stage:development', 'stage:testing'
|
|
30
35
|
]);
|
|
31
36
|
|
|
@@ -104,13 +109,26 @@ jobs:
|
|
|
104
109
|
git push
|
|
105
110
|
|
|
106
111
|
- name: Collect PR and Issue Insights
|
|
107
|
-
uses: actions/github-script@
|
|
112
|
+
uses: actions/github-script@v8
|
|
108
113
|
with:
|
|
109
114
|
script: |
|
|
110
115
|
const fs = require('fs');
|
|
111
116
|
const { owner, repo } = context.repo;
|
|
112
117
|
const weekKey = process.env.WEEK_KEY;
|
|
113
118
|
|
|
119
|
+
// ── Preload stage:detailing times for stage correlation ──────────
|
|
120
|
+
const detailingByIssue = {};
|
|
121
|
+
const jsonlPath = `.agentic-pdlc/metrics/raw/${weekKey}.jsonl`;
|
|
122
|
+
if (fs.existsSync(jsonlPath)) {
|
|
123
|
+
const rawLines = fs.readFileSync(jsonlPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
124
|
+
for (const line of rawLines) {
|
|
125
|
+
const r = JSON.parse(line);
|
|
126
|
+
if (r.stage === 'stage:detailing') {
|
|
127
|
+
detailingByIssue[r.issueNumber] = round1((detailingByIssue[r.issueNumber] || 0) + r.durationDays);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
114
132
|
// ── Helper ──────────────────────────────────────────────────────
|
|
115
133
|
function daysSince(isoStr) {
|
|
116
134
|
return (Date.now() - new Date(isoStr).getTime()) / 864e5;
|
|
@@ -138,6 +156,22 @@ jobs:
|
|
|
138
156
|
// ── Signal collection ───────────────────────────────────────────
|
|
139
157
|
const signals = [];
|
|
140
158
|
|
|
159
|
+
// ── Review actor map (from AGENTIC_PULSE_REVIEWERS env var) ─────
|
|
160
|
+
const actorMap = {}; // login → role
|
|
161
|
+
const reviewersEnv = (process.env.AGENTIC_PULSE_REVIEWERS || '').trim();
|
|
162
|
+
if (reviewersEnv) {
|
|
163
|
+
for (const line of reviewersEnv.split('\n')) {
|
|
164
|
+
const eq = line.indexOf('=');
|
|
165
|
+
if (eq < 0) continue;
|
|
166
|
+
const role = line.slice(0, eq).trim();
|
|
167
|
+
const logins = line.slice(eq + 1).trim();
|
|
168
|
+
for (const login of logins.split(',').map(l => l.trim()).filter(Boolean)) {
|
|
169
|
+
actorMap[login] = role;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const taxonomyEnabled = Object.keys(actorMap).length > 0;
|
|
174
|
+
|
|
141
175
|
// 1. Orphan issues: open >14 days with no linked PR
|
|
142
176
|
const closeRe = /(?:closes?|fixes?|resolves?)\s+#(\d+)/gi;
|
|
143
177
|
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
|
|
@@ -223,29 +257,68 @@ jobs:
|
|
|
223
257
|
}
|
|
224
258
|
}
|
|
225
259
|
|
|
226
|
-
// 3. Rework rate
|
|
260
|
+
// 3. Rework rate with actor taxonomy (if AGENTIC_PULSE_REVIEWERS configured)
|
|
227
261
|
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
228
262
|
const weekMerged = recentPRs.filter(pr => pr.merged_at && new Date(pr.merged_at) > weekAgo);
|
|
229
263
|
|
|
230
264
|
if (weekMerged.length > 0) {
|
|
231
265
|
let firstShots = 0;
|
|
266
|
+
const reworkByRole = {};
|
|
267
|
+
const reworkDetails = []; // { pr_number, issue_number } for stage correlation
|
|
268
|
+
const issueRe = /(?:closes?|fixes?|resolves?)\s+#(\d+)/i;
|
|
269
|
+
|
|
232
270
|
for (const pr of weekMerged.slice(0, 10)) {
|
|
233
271
|
try {
|
|
234
272
|
const commits = await github.rest.pulls.listCommits({
|
|
235
273
|
owner, repo, pull_number: pr.number, per_page: 100
|
|
236
274
|
});
|
|
237
|
-
const times = commits.data
|
|
275
|
+
const times = commits.data
|
|
276
|
+
.map(c => new Date(c.commit.committer.date).getTime())
|
|
277
|
+
.sort((a, b) => a - b);
|
|
278
|
+
|
|
238
279
|
let sessions = 1;
|
|
239
280
|
for (let i = 1; i < times.length; i++) {
|
|
240
281
|
if (times[i] - times[i-1] > 10 * 60 * 1000) sessions++;
|
|
241
282
|
}
|
|
242
|
-
|
|
283
|
+
|
|
284
|
+
if (sessions === 1) { firstShots++; continue; }
|
|
285
|
+
|
|
286
|
+
if (!taxonomyEnabled) continue;
|
|
287
|
+
|
|
288
|
+
let reviewTriggered = false;
|
|
289
|
+
try {
|
|
290
|
+
const reviews = await github.rest.pulls.listReviews({
|
|
291
|
+
owner, repo, pull_number: pr.number, per_page: 100
|
|
292
|
+
});
|
|
293
|
+
const attributedRoles = new Set();
|
|
294
|
+
for (const review of reviews.data) {
|
|
295
|
+
if (!review.user) continue;
|
|
296
|
+
const role = actorMap[review.user.login];
|
|
297
|
+
if (!role || review.state === 'APPROVED' || attributedRoles.has(role)) continue;
|
|
298
|
+
const reviewTime = new Date(review.submitted_at).getTime();
|
|
299
|
+
if (times.some(t => t > reviewTime)) {
|
|
300
|
+
reworkByRole[role] = (reworkByRole[role] || 0) + 1;
|
|
301
|
+
reviewTriggered = true;
|
|
302
|
+
attributedRoles.add(role);
|
|
303
|
+
if (!reworkDetails.some(d => d.pr_number === pr.number)) {
|
|
304
|
+
const m = issueRe.exec(pr.body || '');
|
|
305
|
+
if (m) reworkDetails.push({ pr_number: pr.number, issue_number: parseInt(m[1]) });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} catch(e) { /* reviews not accessible — skip taxonomy for this PR */ }
|
|
310
|
+
|
|
311
|
+
if (!reviewTriggered) {
|
|
312
|
+
reworkByRole.self_correction = (reworkByRole.self_correction || 0) + 1;
|
|
313
|
+
}
|
|
314
|
+
|
|
243
315
|
} catch (e) { /* skip if commits not accessible */ }
|
|
244
316
|
}
|
|
317
|
+
|
|
245
318
|
const total = Math.min(weekMerged.length, 10);
|
|
246
319
|
const pct = Math.round(firstShots / total * 100);
|
|
320
|
+
const reworkCount = total - firstShots;
|
|
247
321
|
|
|
248
|
-
// Detect if repo uses an agent label (jules, sweep, codex, etc.)
|
|
249
322
|
const agentLabels = new Set(['jules', 'sweep', 'codex', 'copilot']);
|
|
250
323
|
const usesAgent = weekMerged.some(pr =>
|
|
251
324
|
(pr.labels || []).some(l => agentLabels.has(l.name.toLowerCase()))
|
|
@@ -253,27 +326,78 @@ jobs:
|
|
|
253
326
|
const subject = usesAgent ? 'Agent first-shot rate' : 'PRs sem rework';
|
|
254
327
|
const verb = usesAgent ? 'acertaram de primeira' : 'foram mergeados sem rework';
|
|
255
328
|
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
329
|
+
if (taxonomyEnabled && reworkCount > 0) {
|
|
330
|
+
const lines = [];
|
|
331
|
+
for (const [role, count] of Object.entries(reworkByRole).sort((a, b) => b[1] - a[1])) {
|
|
332
|
+
const s = count > 1 ? 's' : '';
|
|
333
|
+
if (role === 'code_reviewer') lines.push(` ↳ Code reviewer: **${count} PR${s}** → revisar DoD em stage:development`);
|
|
334
|
+
else if (role === 'qa_agent') lines.push(` ↳ QA Agent: **${count} PR${s}** → spec com lacunas funcionais em stage:detailing`);
|
|
335
|
+
else if (role === 'self_correction') lines.push(` ↳ Self-correction: **${count} PR${s}** (causa não determinada automaticamente)`);
|
|
336
|
+
else lines.push(` ↳ ${role}: **${count} PR${s}**`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const reviewerRework = reworkByRole.code_reviewer || 0;
|
|
340
|
+
const level = reviewerRework >= Math.ceil(reworkCount * 0.8) ? 'red'
|
|
341
|
+
: (reviewerRework >= Math.ceil(reworkCount * 0.5) || (reworkByRole.qa_agent || 0) > 0) ? 'yellow'
|
|
342
|
+
: 'neutral';
|
|
343
|
+
const emoji = level === 'red' ? '🔴' : level === 'yellow' ? '🟡' : '🔵';
|
|
344
|
+
|
|
264
345
|
signals.push({
|
|
265
|
-
level
|
|
266
|
-
emoji
|
|
267
|
-
title:
|
|
268
|
-
body:
|
|
346
|
+
level,
|
|
347
|
+
emoji,
|
|
348
|
+
title: `**Rework: ${100 - pct}%** — ${reworkCount} de ${total} PRs tiveram commits extras`,
|
|
349
|
+
body: lines.join('\n')
|
|
269
350
|
});
|
|
351
|
+
|
|
352
|
+
// ── Stage correlation ────────────────────────────────────────
|
|
353
|
+
if (reworkDetails.length > 0 && Object.keys(detailingByIssue).length > 0) {
|
|
354
|
+
const reworkIssueNums = new Set(reworkDetails.map(d => d.issue_number));
|
|
355
|
+
|
|
356
|
+
const reworkGroup = reworkDetails
|
|
357
|
+
.map(d => detailingByIssue[d.issue_number])
|
|
358
|
+
.filter(t => t !== undefined);
|
|
359
|
+
|
|
360
|
+
const cleanGroup = weekMerged.slice(0, 10)
|
|
361
|
+
.map(pr => { const m = issueRe.exec(pr.body || ''); return m ? parseInt(m[1]) : null; })
|
|
362
|
+
.filter(n => n !== null && !reworkIssueNums.has(n))
|
|
363
|
+
.map(n => detailingByIssue[n])
|
|
364
|
+
.filter(t => t !== undefined);
|
|
365
|
+
|
|
366
|
+
if (reworkGroup.length >= 3 && cleanGroup.length >= 3) {
|
|
367
|
+
const avgRework = round1(reworkGroup.reduce((a, b) => a + b, 0) / reworkGroup.length);
|
|
368
|
+
const avgClean = round1(cleanGroup.reduce((a, b) => a + b, 0) / cleanGroup.length);
|
|
369
|
+
if (avgRework < avgClean * 0.75) {
|
|
370
|
+
signals.push({
|
|
371
|
+
level: 'neutral',
|
|
372
|
+
emoji: '💡',
|
|
373
|
+
title: `**Stage correlation:** PRs com reviewer rework tiveram Detailing médio de ${avgRework}d vs ${avgClean}d (N=${reworkGroup.length} vs ${cleanGroup.length})`,
|
|
374
|
+
body: '→ Specs rápidas correlacionam com mais rework de review'
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
270
380
|
} else {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
381
|
+
// Taxonomy disabled or no rework — existing signal unchanged
|
|
382
|
+
if (pct >= 80) {
|
|
383
|
+
signals.push({
|
|
384
|
+
level: 'green', emoji: '🟢',
|
|
385
|
+
title: `**${subject}: ${pct}%**`,
|
|
386
|
+
body: `${firstShots} de ${total} PRs ${verb} esta semana. ✅`
|
|
387
|
+
});
|
|
388
|
+
} else if (pct < 50) {
|
|
389
|
+
signals.push({
|
|
390
|
+
level: 'yellow', emoji: '🟡',
|
|
391
|
+
title: `**${subject}: ${pct}% — rework alto**`,
|
|
392
|
+
body: `Apenas ${firstShots} de ${total} PRs sem commits extras.\n→ Specs incompletas ou mudanças de requisito durante implementação.`
|
|
393
|
+
});
|
|
394
|
+
} else {
|
|
395
|
+
signals.push({
|
|
396
|
+
level: 'neutral', emoji: '🔵',
|
|
397
|
+
title: `**${subject}: ${pct}%**`,
|
|
398
|
+
body: `${firstShots} de ${total} PRs sem rework commits.`
|
|
399
|
+
});
|
|
400
|
+
}
|
|
277
401
|
}
|
|
278
402
|
}
|
|
279
403
|
|
|
@@ -292,12 +416,10 @@ jobs:
|
|
|
292
416
|
|
|
293
417
|
// ── Stage Residence Time section (conditional) ──────────────────
|
|
294
418
|
let stageSection = '';
|
|
295
|
-
const jsonlPath = `.agentic-pdlc/metrics/raw/${weekKey}.jsonl`;
|
|
296
419
|
if (fs.existsSync(jsonlPath)) {
|
|
297
420
|
const records = fs.readFileSync(jsonlPath, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
|
|
298
421
|
if (records.length > 0) {
|
|
299
422
|
const STAGES = [
|
|
300
|
-
['stage:exploration', 'Exploration'],
|
|
301
423
|
['stage:brainstorming', 'Brainstorming'],
|
|
302
424
|
['stage:detailing', 'Detailing'],
|
|
303
425
|
['stage:approval', 'Approval'],
|
|
@@ -311,11 +433,14 @@ jobs:
|
|
|
311
433
|
}
|
|
312
434
|
const avg = arr => arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
313
435
|
|
|
314
|
-
let maxStage = null, maxDays =
|
|
436
|
+
let maxStage = null, maxDays = -1;
|
|
315
437
|
const rows = [];
|
|
316
438
|
for (const [stage, label] of STAGES) {
|
|
317
439
|
const days = byStage[stage];
|
|
318
|
-
if (!days)
|
|
440
|
+
if (!days) {
|
|
441
|
+
rows.push(`| **${label}** | — | — |`);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
319
444
|
const a = round1(avg(days));
|
|
320
445
|
rows.push(`| **${label}** | ${a}d | ${days.length} |`);
|
|
321
446
|
if (a > maxDays) { maxStage = label; maxDays = a; }
|
|
@@ -374,7 +499,7 @@ jobs:
|
|
|
374
499
|
console.log(`Built pulse issue for ${weekKey} — ${signals.length} signals, ${reds} red, ${yellows} yellow`);
|
|
375
500
|
|
|
376
501
|
- name: Create Weekly Pulse Issue
|
|
377
|
-
uses: actions/github-script@
|
|
502
|
+
uses: actions/github-script@v8
|
|
378
503
|
with:
|
|
379
504
|
script: |
|
|
380
505
|
const { owner, repo } = context.repo;
|
|
@@ -394,19 +519,27 @@ jobs:
|
|
|
394
519
|
console.log(`Created label ${LABEL}`);
|
|
395
520
|
}
|
|
396
521
|
|
|
397
|
-
// Close previous pulse issues
|
|
522
|
+
// Close previous pulse issues; upsert if same week already exists
|
|
398
523
|
const prev = await github.rest.issues.listForRepo({
|
|
399
524
|
owner, repo, labels: LABEL, state: 'open', per_page: 20
|
|
400
525
|
});
|
|
526
|
+
let existingIssue = null;
|
|
401
527
|
for (const issue of prev.data) {
|
|
402
|
-
if (issue.title
|
|
528
|
+
if (issue.title === title) {
|
|
529
|
+
existingIssue = issue;
|
|
530
|
+
console.log(`Found existing pulse for ${title}: #${issue.number} — will update body`);
|
|
531
|
+
} else {
|
|
403
532
|
await github.rest.issues.update({ owner, repo, issue_number: issue.number, state: 'closed' });
|
|
404
533
|
console.log(`Closed previous pulse: #${issue.number}`);
|
|
405
534
|
}
|
|
406
535
|
}
|
|
407
536
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
537
|
+
if (existingIssue) {
|
|
538
|
+
await github.rest.issues.update({ owner, repo, issue_number: existingIssue.number, body });
|
|
539
|
+
console.log(`✅ Updated pulse issue: #${existingIssue.number} — ${title}`);
|
|
540
|
+
} else {
|
|
541
|
+
const created = await github.rest.issues.create({
|
|
542
|
+
owner, repo, title, body, labels: [LABEL]
|
|
543
|
+
});
|
|
544
|
+
console.log(`✅ Created pulse issue: #${created.data.number} — ${title}`);
|
|
545
|
+
}
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -11,7 +11,7 @@ jobs:
|
|
|
11
11
|
name: Run tests and linters
|
|
12
12
|
runs-on: ubuntu-latest
|
|
13
13
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
14
|
+
- uses: actions/checkout@v5.0.1
|
|
15
15
|
|
|
16
16
|
- name: Setup environment
|
|
17
17
|
run: echo "Replace this with your language/toolchain setup (e.g., actions/setup-node)"
|
|
@@ -14,10 +14,10 @@ jobs:
|
|
|
14
14
|
|
|
15
15
|
steps:
|
|
16
16
|
- name: Checkout Code
|
|
17
|
-
uses: actions/checkout@
|
|
17
|
+
uses: actions/checkout@v5.0.1
|
|
18
18
|
|
|
19
19
|
- name: Setup Node.js
|
|
20
|
-
uses: actions/setup-node@
|
|
20
|
+
uses: actions/setup-node@v6.4.0
|
|
21
21
|
with:
|
|
22
22
|
node-version: '24'
|
|
23
23
|
registry-url: 'https://registry.npmjs.org'
|
|
@@ -8,7 +8,6 @@ on:
|
|
|
8
8
|
env:
|
|
9
9
|
PROJECT_ID: "PVT_kwHODpFFL84BXg7h"
|
|
10
10
|
STATUS_FIELD_ID: "PVTSSF_lAHODpFFL84BXg7hzhStRHI"
|
|
11
|
-
STATUS_EXPLORATION: "96ac537d"
|
|
12
11
|
STATUS_BRAINSTORMING: "8eb07c5b"
|
|
13
12
|
STATUS_DETAILING: "9f6ce70e"
|
|
14
13
|
STATUS_APPROVAL: "31bf4610"
|
|
@@ -28,14 +27,13 @@ jobs:
|
|
|
28
27
|
steps:
|
|
29
28
|
- name: Validate Board Configuration
|
|
30
29
|
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
31
|
-
uses: actions/github-script@
|
|
30
|
+
uses: actions/github-script@v8
|
|
32
31
|
with:
|
|
33
32
|
github-token: ${{ env.PROJECT_TOKEN }}
|
|
34
33
|
script: |
|
|
35
34
|
const projectId = process.env.PROJECT_ID;
|
|
36
35
|
const statusFieldId = process.env.STATUS_FIELD_ID;
|
|
37
36
|
const envVars = {
|
|
38
|
-
'STATUS_EXPLORATION': process.env.STATUS_EXPLORATION,
|
|
39
37
|
'STATUS_BRAINSTORMING': process.env.STATUS_BRAINSTORMING,
|
|
40
38
|
'STATUS_DETAILING': process.env.STATUS_DETAILING,
|
|
41
39
|
'STATUS_APPROVAL': process.env.STATUS_APPROVAL,
|
|
@@ -37,12 +37,12 @@ jobs:
|
|
|
37
37
|
|
|
38
38
|
for NUM in $ISSUE_NUMS; do
|
|
39
39
|
LABELS=$(gh issue view "$NUM" --repo "$REPO" --json labels --jq '[.labels[].name] | join(" ")' 2>/dev/null || echo "")
|
|
40
|
-
if echo "$LABELS" | grep -qw "stage:approval" || echo "$LABELS" | grep -qw "spec:approved" || echo "$LABELS" | grep -qw "stage:development"; then
|
|
40
|
+
if echo "$LABELS" | grep -qw "stage:approval" || echo "$LABELS" | grep -qw "spec:approved" || echo "$LABELS" | grep -qw "stage:development" || echo "$LABELS" | grep -qw "stage:testing" || echo "$LABELS" | grep -qw "human-approved"; then
|
|
41
41
|
echo "✅ Issue #$NUM approved"
|
|
42
42
|
else
|
|
43
43
|
STAGE=$(echo "$LABELS" | tr ' ' '\n' | grep "^stage:" | head -1 || echo "none")
|
|
44
44
|
echo "❌ Issue #$NUM missing approval (current: $STAGE)"
|
|
45
|
-
echo " Required: stage:approval OR spec:approved OR stage:development label on the issue."
|
|
45
|
+
echo " Required: stage:approval OR spec:approved OR stage:development OR stage:testing OR human-approved label on the issue."
|
|
46
46
|
echo " Emergency bypass: add 'hotfix' label to this PR."
|
|
47
47
|
exit 1
|
|
48
48
|
fi
|
|
@@ -6,13 +6,12 @@ on:
|
|
|
6
6
|
pull_request_review:
|
|
7
7
|
types: [submitted]
|
|
8
8
|
issues:
|
|
9
|
-
types: [labeled]
|
|
9
|
+
types: [labeled, closed]
|
|
10
10
|
|
|
11
11
|
env:
|
|
12
12
|
PROJECT_ID: "PVT_kwHODpFFL84BXg7h"
|
|
13
13
|
STATUS_FIELD_ID: "PVTSSF_lAHODpFFL84BXg7hzhStRHI"
|
|
14
14
|
STATUS_IDEA: "bb6e5a20"
|
|
15
|
-
STATUS_EXPLORATION: "96ac537d"
|
|
16
15
|
STATUS_BRAINSTORMING: "8eb07c5b"
|
|
17
16
|
STATUS_DETAILING: "9f6ce70e"
|
|
18
17
|
STATUS_APPROVAL: "31bf4610"
|
|
@@ -32,7 +31,7 @@ jobs:
|
|
|
32
31
|
steps:
|
|
33
32
|
- name: Detect Label and Move Issue
|
|
34
33
|
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
35
|
-
uses: actions/github-script@
|
|
34
|
+
uses: actions/github-script@v8
|
|
36
35
|
with:
|
|
37
36
|
github-token: ${{ env.PROJECT_TOKEN }}
|
|
38
37
|
script: |
|
|
@@ -40,10 +39,7 @@ jobs:
|
|
|
40
39
|
let targetStatusId = null;
|
|
41
40
|
let stageName = null;
|
|
42
41
|
|
|
43
|
-
if (labelName === 'stage:
|
|
44
|
-
targetStatusId = process.env.STATUS_EXPLORATION;
|
|
45
|
-
stageName = 'Exploration';
|
|
46
|
-
} else if (labelName === 'stage:brainstorming') {
|
|
42
|
+
if (labelName === 'stage:brainstorming') {
|
|
47
43
|
targetStatusId = process.env.STATUS_BRAINSTORMING;
|
|
48
44
|
stageName = 'Brainstorming';
|
|
49
45
|
} else if (labelName === 'stage:detailing') {
|
|
@@ -55,9 +51,6 @@ jobs:
|
|
|
55
51
|
} else if (labelName === 'stage:development') {
|
|
56
52
|
targetStatusId = process.env.STATUS_DEVELOPMENT;
|
|
57
53
|
stageName = 'Development';
|
|
58
|
-
} else if (labelName === 'stage:testing') {
|
|
59
|
-
targetStatusId = process.env.STATUS_TESTING;
|
|
60
|
-
stageName = 'Testing';
|
|
61
54
|
}
|
|
62
55
|
|
|
63
56
|
if (!targetStatusId) {
|
|
@@ -87,6 +80,42 @@ jobs:
|
|
|
87
80
|
await moveItem(node_id, targetStatusId);
|
|
88
81
|
console.log(`Issue #${number} moved to ${stageName}`);
|
|
89
82
|
|
|
83
|
+
# human-approved on issue → qa:approved on linked open PRs
|
|
84
|
+
handle-human-approved:
|
|
85
|
+
name: human-approved → qa:approved on linked PRs
|
|
86
|
+
if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'human-approved'
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
permissions:
|
|
89
|
+
issues: write
|
|
90
|
+
pull-requests: write
|
|
91
|
+
steps:
|
|
92
|
+
- name: Add qa:approved to linked open PRs
|
|
93
|
+
uses: actions/github-script@v8
|
|
94
|
+
with:
|
|
95
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
96
|
+
script: |
|
|
97
|
+
const { owner, repo } = context.repo;
|
|
98
|
+
const issueNumber = context.payload.issue.number;
|
|
99
|
+
const pattern = new RegExp(`(?:Closes?|Fixes?|Resolves?)\\s+#${issueNumber}\\b`, 'i');
|
|
100
|
+
|
|
101
|
+
const prs = await github.paginate(github.rest.pulls.list, {
|
|
102
|
+
owner, repo, state: 'open', per_page: 100
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const linked = prs.filter(pr => pattern.test(pr.body ?? ''));
|
|
106
|
+
|
|
107
|
+
if (linked.length === 0) {
|
|
108
|
+
console.log(`No open PRs linking issue #${issueNumber}. Exiting.`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const pr of linked) {
|
|
113
|
+
await github.rest.issues.addLabels({
|
|
114
|
+
owner, repo, issue_number: pr.number, labels: ['qa:approved']
|
|
115
|
+
}).catch(() => {});
|
|
116
|
+
console.log(`PR #${pr.number} → qa:approved`);
|
|
117
|
+
}
|
|
118
|
+
|
|
90
119
|
# OPTIONAL: Uncomment to enable architecture-violation → Idea
|
|
91
120
|
# move-violation-to-board:
|
|
92
121
|
# name: architecture-violation → 💡 Idea
|
|
@@ -95,7 +124,7 @@ jobs:
|
|
|
95
124
|
# steps:
|
|
96
125
|
# - name: Move issue to Idea
|
|
97
126
|
# if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
98
|
-
# uses: actions/github-script@
|
|
127
|
+
# uses: actions/github-script@v8
|
|
99
128
|
# with:
|
|
100
129
|
# github-token: ${{ env.PROJECT_TOKEN }}
|
|
101
130
|
# script: |
|
|
@@ -125,7 +154,7 @@ jobs:
|
|
|
125
154
|
steps:
|
|
126
155
|
- name: Move linked issue to Testing
|
|
127
156
|
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
128
|
-
uses: actions/github-script@
|
|
157
|
+
uses: actions/github-script@v8
|
|
129
158
|
with:
|
|
130
159
|
github-token: ${{ env.PROJECT_TOKEN }}
|
|
131
160
|
script: |
|
|
@@ -156,11 +185,17 @@ jobs:
|
|
|
156
185
|
});
|
|
157
186
|
};
|
|
158
187
|
|
|
188
|
+
const stageLabelsToRemove = ['stage:development', 'stage:brainstorming', 'stage:detailing', 'jules'];
|
|
189
|
+
|
|
159
190
|
if (linkedIssues.length > 0) {
|
|
160
191
|
for (const n of linkedIssues) {
|
|
161
192
|
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
|
|
162
193
|
await moveItem(issue.node_id);
|
|
163
|
-
|
|
194
|
+
for (const label of stageLabelsToRemove) {
|
|
195
|
+
await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: label }).catch(() => {});
|
|
196
|
+
}
|
|
197
|
+
await github.rest.issues.addLabels({ owner, repo, issue_number: n, labels: ['stage:testing'] }).catch(() => {});
|
|
198
|
+
console.log(`Issue #${n} → Testing (labels updated)`);
|
|
164
199
|
}
|
|
165
200
|
} else {
|
|
166
201
|
await moveItem(pr.node_id);
|
|
@@ -179,7 +214,7 @@ jobs:
|
|
|
179
214
|
steps:
|
|
180
215
|
- name: Move linked issue to Code Review / PR
|
|
181
216
|
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
182
|
-
uses: actions/github-script@
|
|
217
|
+
uses: actions/github-script@v8
|
|
183
218
|
with:
|
|
184
219
|
github-token: ${{ env.PROJECT_TOKEN }}
|
|
185
220
|
script: |
|
|
@@ -213,6 +248,9 @@ jobs:
|
|
|
213
248
|
for (const n of linkedIssues) {
|
|
214
249
|
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
|
|
215
250
|
await moveItem(issue.node_id);
|
|
251
|
+
if (issue.labels?.some(l => l.name === 'stage:testing')) {
|
|
252
|
+
await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: 'stage:testing' }).catch(() => {});
|
|
253
|
+
}
|
|
216
254
|
console.log(`Issue #${n} → Code Review / PR`);
|
|
217
255
|
}
|
|
218
256
|
} else {
|
|
@@ -230,7 +268,7 @@ jobs:
|
|
|
230
268
|
steps:
|
|
231
269
|
- name: Swap PR labels
|
|
232
270
|
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
233
|
-
uses: actions/github-script@
|
|
271
|
+
uses: actions/github-script@v8
|
|
234
272
|
with:
|
|
235
273
|
github-token: ${{ env.PROJECT_TOKEN }}
|
|
236
274
|
script: |
|
|
@@ -249,7 +287,7 @@ jobs:
|
|
|
249
287
|
steps:
|
|
250
288
|
- name: Move issue to Production
|
|
251
289
|
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
252
|
-
uses: actions/github-script@
|
|
290
|
+
uses: actions/github-script@v8
|
|
253
291
|
with:
|
|
254
292
|
github-token: ${{ env.PROJECT_TOKEN }}
|
|
255
293
|
script: |
|
|
@@ -277,8 +315,33 @@ jobs:
|
|
|
277
315
|
for (const n of linkedIssues) {
|
|
278
316
|
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
|
|
279
317
|
await moveItem(issue.node_id);
|
|
318
|
+
if (issue.labels?.some(l => l.name === 'stage:approval')) {
|
|
319
|
+
await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: 'stage:approval' }).catch(() => {});
|
|
320
|
+
}
|
|
280
321
|
console.log(`Issue #${n} → Production`);
|
|
281
322
|
}
|
|
282
323
|
} else {
|
|
283
324
|
await moveItem(pr.node_id);
|
|
284
325
|
}
|
|
326
|
+
|
|
327
|
+
cleanup-labels-on-close:
|
|
328
|
+
name: Issue closed → strip stage/agent labels
|
|
329
|
+
if: github.event_name == 'issues' && github.event.action == 'closed'
|
|
330
|
+
runs-on: ubuntu-latest
|
|
331
|
+
steps:
|
|
332
|
+
- name: Remove transient labels
|
|
333
|
+
uses: actions/github-script@v8
|
|
334
|
+
with:
|
|
335
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
336
|
+
script: |
|
|
337
|
+
const { owner, repo } = context.repo;
|
|
338
|
+
const issue_number = context.payload.issue.number;
|
|
339
|
+
const toRemove = [
|
|
340
|
+
'stage:brainstorming', 'stage:detailing',
|
|
341
|
+
'stage:approval', 'stage:development', 'stage:testing',
|
|
342
|
+
'qa:needs-work', 'pr:in-review', 'jules'
|
|
343
|
+
];
|
|
344
|
+
for (const label of toRemove) {
|
|
345
|
+
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: label }).catch(() => {});
|
|
346
|
+
}
|
|
347
|
+
console.log(`Issue #${issue_number} labels cleaned up`);
|