create-agentic-pdlc 2.1.6 → 2.2.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.
Files changed (46) hide show
  1. package/.agentic-pdlc/SETUP_PROMPT.md +73 -0
  2. package/.agentic-pdlc/hooks/pdlc-stage-gate.sh +39 -0
  3. package/.agentic-pdlc/metrics/.gitkeep +0 -0
  4. package/.agentic-pdlc/metrics/2026-W19.md +21 -0
  5. package/.agentic-pdlc/metrics/raw/2026-W19.jsonl +6 -0
  6. package/.agentic-pdlc/metrics/raw/2026-W20.jsonl +1 -0
  7. package/.agentic-pdlc/templates/.github/CODEOWNERS +5 -0
  8. package/.agentic-pdlc/templates/.github/copilot-instructions.md +12 -0
  9. package/.agentic-pdlc/templates/.github/workflows/add-to-board.yml +38 -0
  10. package/.agentic-pdlc/templates/.github/workflows/agent-trigger.yml +146 -0
  11. package/.agentic-pdlc/templates/.github/workflows/agentic-metrics.yml +412 -0
  12. package/.agentic-pdlc/templates/.github/workflows/auto-approve.yml +16 -0
  13. package/.agentic-pdlc/templates/.github/workflows/ci.yml +40 -0
  14. package/.agentic-pdlc/templates/.github/workflows/pdlc-health-check.yml +123 -0
  15. package/.agentic-pdlc/templates/.github/workflows/pdlc-stage-gate.yml +51 -0
  16. package/.agentic-pdlc/templates/.github/workflows/project-automation.yml +278 -0
  17. package/.agentic-pdlc/templates/.github/workflows/protect-workflows.yml +21 -0
  18. package/.agentic-pdlc/templates/.github/workflows/qa-agent.yml +128 -0
  19. package/.agentic-pdlc/templates/AGENTS.md +81 -0
  20. package/.agentic-pdlc/templates/docs/pdlc.md +15 -5
  21. package/.agentic-setup-prompt.md +73 -0
  22. package/.agentic-setup.md +73 -0
  23. package/.claude/settings.json +15 -0
  24. package/.cursorrules +9 -0
  25. package/.github/ISSUE_TEMPLATE/pulse-feedback.md +11 -0
  26. package/.github/workflows/add-to-board.yml +38 -0
  27. package/.github/workflows/agent-trigger.yml +30 -43
  28. package/.github/workflows/agentic-metrics.yml +412 -0
  29. package/.github/workflows/pdlc-health-check.yml +10 -10
  30. package/.github/workflows/pdlc-stage-gate.yml +51 -0
  31. package/.github/workflows/project-automation.yml +68 -18
  32. package/.github/workflows/qa-agent.yml +112 -11
  33. package/CLAUDE.md +9 -0
  34. package/SETUP.md +28 -0
  35. package/adapters/claude-code/skill.md +63 -15
  36. package/adapters/hooks/pdlc-stage-gate.sh +44 -0
  37. package/bin/cli.js +113 -11
  38. package/docs/pdlc.md +15 -5
  39. package/package.json +1 -1
  40. package/pr_comments.json +20 -0
  41. package/templates/.github/workflows/add-to-board.yml +38 -0
  42. package/templates/.github/workflows/agent-trigger.yml +34 -4
  43. package/templates/.github/workflows/pdlc-stage-gate.yml +51 -0
  44. package/templates/.github/workflows/project-automation.yml +78 -54
  45. package/templates/.github/workflows/qa-agent.yml +14 -13
  46. package/templates/docs/pdlc.md +15 -5
@@ -0,0 +1,412 @@
1
+ name: Agentic Metrics — Weekly Pulse
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '0 8 * * 0' # Sunday 8am UTC
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: write
10
+ issues: write
11
+
12
+ jobs:
13
+ generate-pulse:
14
+ name: Generate Weekly Agentic Pulse
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Collect Stage Residence Time
20
+ uses: actions/github-script@v7
21
+ with:
22
+ script: |
23
+ const fs = require('fs');
24
+
25
+ const { owner, repo } = context.repo;
26
+ const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
27
+ const STAGE_LABELS = new Set([
28
+ 'stage:exploration', 'stage:brainstorming', 'stage:detailing',
29
+ 'stage:approval', 'stage:development', 'stage:testing'
30
+ ]);
31
+
32
+ const QUERY = `
33
+ query($owner: String!, $repo: String!, $since: DateTime!, $cursor: String) {
34
+ repository(owner: $owner, name: $repo) {
35
+ issues(first: 50, after: $cursor, filterBy: { since: $since }) {
36
+ pageInfo { hasNextPage endCursor }
37
+ nodes {
38
+ number
39
+ timelineItems(first: 100, itemTypes: [LABELED_EVENT, UNLABELED_EVENT]) {
40
+ nodes {
41
+ ... on LabeledEvent { __typename createdAt label { name } }
42
+ ... on UnlabeledEvent { __typename createdAt label { name } }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ `;
50
+
51
+ const records = [];
52
+ let cursor = null;
53
+ do {
54
+ const result = await github.graphql(QUERY, { owner, repo, since, cursor });
55
+ const { nodes, pageInfo } = result.repository.issues;
56
+
57
+ for (const issue of nodes) {
58
+ const events = issue.timelineItems.nodes
59
+ .filter(e => e.label && STAGE_LABELS.has(e.label.name))
60
+ .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
61
+
62
+ const labeledAt = {};
63
+ for (const ev of events) {
64
+ const stage = ev.label.name;
65
+ if (ev.__typename === 'LabeledEvent') {
66
+ labeledAt[stage] = new Date(ev.createdAt);
67
+ } else if (ev.__typename === 'UnlabeledEvent' && labeledAt[stage]) {
68
+ const durationDays = Math.round((new Date(ev.createdAt) - labeledAt[stage]) / 864e5 * 10) / 10;
69
+ records.push({ issueNumber: issue.number, stage, durationDays });
70
+ delete labeledAt[stage];
71
+ }
72
+ }
73
+ }
74
+
75
+ cursor = pageInfo.hasNextPage ? pageInfo.endCursor : null;
76
+ } while (cursor);
77
+
78
+ function isoWeek(d) {
79
+ const dt = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
80
+ dt.setUTCDate(dt.getUTCDate() + 4 - (dt.getUTCDay() || 7));
81
+ const y = new Date(Date.UTC(dt.getUTCFullYear(), 0, 1));
82
+ return { year: dt.getUTCFullYear(), week: Math.ceil((((dt - y) / 86400000) + 1) / 7) };
83
+ }
84
+
85
+ const { year, week } = isoWeek(new Date());
86
+ const weekKey = `${year}-W${String(week).padStart(2, '0')}`;
87
+
88
+ fs.mkdirSync('.agentic-pdlc/metrics/raw', { recursive: true });
89
+ fs.writeFileSync(
90
+ `.agentic-pdlc/metrics/raw/${weekKey}.jsonl`,
91
+ records.map(r => JSON.stringify(r)).join('\n')
92
+ );
93
+
94
+ core.exportVariable('WEEK_KEY', weekKey);
95
+ console.log(`Collected ${records.length} stage transitions → week ${weekKey}`);
96
+
97
+ - name: Commit JSONL raw data
98
+ run: |
99
+ git config user.name "github-actions[bot]"
100
+ git config user.email "github-actions[bot]@users.noreply.github.com"
101
+ git add .agentic-pdlc/metrics/raw/
102
+ git diff --staged --quiet && echo "No new stage data" && exit 0
103
+ git commit -m "metrics: raw stage data ${WEEK_KEY} [skip ci]"
104
+ git push
105
+
106
+ - name: Collect PR and Issue Insights
107
+ uses: actions/github-script@v7
108
+ with:
109
+ script: |
110
+ const fs = require('fs');
111
+ const { owner, repo } = context.repo;
112
+ const weekKey = process.env.WEEK_KEY;
113
+
114
+ // ── Helper ──────────────────────────────────────────────────────
115
+ function daysSince(isoStr) {
116
+ return (Date.now() - new Date(isoStr).getTime()) / 864e5;
117
+ }
118
+ function round1(n) { return Math.round(n * 10) / 10; }
119
+
120
+ // Week date range for issue title (ISO week, Mon–Sun)
121
+ function weekDateRange(key) {
122
+ const [yr, wStr] = key.split('-W');
123
+ const week = parseInt(wStr, 10);
124
+ // Jan 4 is always in week 1
125
+ const jan4 = new Date(Date.UTC(parseInt(yr, 10), 0, 4));
126
+ const monday = new Date(jan4);
127
+ monday.setUTCDate(jan4.getUTCDate() - ((jan4.getUTCDay() + 6) % 7) + (week - 1) * 7);
128
+ const sunday = new Date(monday);
129
+ sunday.setUTCDate(monday.getUTCDate() + 6);
130
+ const months = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'];
131
+ const pad = n => String(n).padStart(2, '0');
132
+ const startMonth = months[monday.getUTCMonth()];
133
+ const endMonth = months[sunday.getUTCMonth()];
134
+ const monthLabel = startMonth === endMonth ? startMonth : startMonth + '/' + endMonth;
135
+ return pad(monday.getUTCDate()) + '-' + pad(sunday.getUTCDate()) + '/' + monthLabel;
136
+ }
137
+
138
+ // ── Signal collection ───────────────────────────────────────────
139
+ const signals = [];
140
+
141
+ // 1. Orphan issues: open >14 days with no linked PR
142
+ const closeRe = /(?:closes?|fixes?|resolves?)\s+#(\d+)/gi;
143
+ const openIssues = await github.paginate(github.rest.issues.listForRepo, {
144
+ owner, repo, state: 'open', per_page: 100
145
+ });
146
+
147
+ const recentPRs = await github.paginate(github.rest.pulls.list, {
148
+ owner, repo, state: 'closed', per_page: 50,
149
+ sort: 'updated', direction: 'desc'
150
+ });
151
+
152
+ const linkedIssueNums = new Set();
153
+ for (const pr of recentPRs) {
154
+ const body = pr.body || '';
155
+ let m;
156
+ while ((m = closeRe.exec(body)) !== null) linkedIssueNums.add(parseInt(m[1]));
157
+ closeRe.lastIndex = 0;
158
+ }
159
+
160
+ const orphans = openIssues.filter(i =>
161
+ !i.pull_request &&
162
+ daysSince(i.created_at) > 14 &&
163
+ !linkedIssueNums.has(i.number)
164
+ );
165
+
166
+ if (orphans.length > 0) {
167
+ const names = orphans.slice(0, 5).map(i => `#${i.number} "${i.title}"`).join(', ');
168
+ const extra = orphans.length > 5 ? ` e mais ${orphans.length - 5}` : '';
169
+ signals.push({
170
+ level: 'red',
171
+ emoji: '🔴',
172
+ title: `**${orphans.length} issue${orphans.length > 1 ? 's abertas' : ' aberta'} há 14+ dias sem PR linkado**`,
173
+ body: `${names}${extra}\n→ Atribua, planeje ou feche se não for mais relevante.`
174
+ });
175
+ } else {
176
+ signals.push({
177
+ level: 'green',
178
+ emoji: '🟢',
179
+ title: '**Nenhuma issue esquecida**',
180
+ body: 'Todas as issues abertas têm menos de 14 dias ou têm PR linkado.'
181
+ });
182
+ }
183
+
184
+ // 2. PR merge time trend: last 10 merged PRs split 5+5
185
+ const mergedPRs = recentPRs
186
+ .filter(pr => pr.merged_at)
187
+ .slice(0, 10);
188
+
189
+ if (mergedPRs.length >= 4) {
190
+ const half = Math.floor(mergedPRs.length / 2);
191
+ const recent = mergedPRs.slice(0, half);
192
+ const older = mergedPRs.slice(half);
193
+ const avgDays = prs => {
194
+ const total = prs.reduce((s, pr) =>
195
+ s + (new Date(pr.merged_at) - new Date(pr.created_at)) / 864e5, 0);
196
+ return round1(total / prs.length);
197
+ };
198
+ const recentAvg = avgDays(recent);
199
+ const olderAvg = avgDays(older);
200
+ const ratio = recentAvg / olderAvg;
201
+
202
+ if (ratio >= 1.5) {
203
+ signals.push({
204
+ level: 'yellow',
205
+ emoji: '🟡',
206
+ title: `**Tempo de merge aumentou ${round1(ratio)}x esta semana**`,
207
+ body: `Últimos ${half} PRs: **${recentAvg} dias** · ${half} anteriores: **${olderAvg} dias**\n→ Algo desacelerou o review. Verifique PRs pendentes de aprovação.`
208
+ });
209
+ } else if (ratio <= 0.67) {
210
+ signals.push({
211
+ level: 'green',
212
+ emoji: '🟢',
213
+ title: `**Tempo de merge melhorou ${round1(1/ratio)}x esta semana**`,
214
+ body: `Últimos ${half} PRs: **${recentAvg} dias** · ${half} anteriores: **${olderAvg} dias** ✅`
215
+ });
216
+ } else {
217
+ signals.push({
218
+ level: 'neutral',
219
+ emoji: '🔵',
220
+ title: `**Tempo de merge estável: ${recentAvg} dias** (semana passada: ${olderAvg}d)`,
221
+ body: ''
222
+ });
223
+ }
224
+ }
225
+
226
+ // 3. Rework rate: commits per PR — single push session = first-shot
227
+ const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
228
+ const weekMerged = recentPRs.filter(pr => pr.merged_at && new Date(pr.merged_at) > weekAgo);
229
+
230
+ if (weekMerged.length > 0) {
231
+ let firstShots = 0;
232
+ for (const pr of weekMerged.slice(0, 10)) {
233
+ try {
234
+ const commits = await github.rest.pulls.listCommits({
235
+ owner, repo, pull_number: pr.number, per_page: 100
236
+ });
237
+ const times = commits.data.map(c => new Date(c.commit.committer.date).getTime()).sort();
238
+ let sessions = 1;
239
+ for (let i = 1; i < times.length; i++) {
240
+ if (times[i] - times[i-1] > 10 * 60 * 1000) sessions++;
241
+ }
242
+ if (sessions === 1) firstShots++;
243
+ } catch (e) { /* skip if commits not accessible */ }
244
+ }
245
+ const total = Math.min(weekMerged.length, 10);
246
+ const pct = Math.round(firstShots / total * 100);
247
+
248
+ // Detect if repo uses an agent label (jules, sweep, codex, etc.)
249
+ const agentLabels = new Set(['jules', 'sweep', 'codex', 'copilot']);
250
+ const usesAgent = weekMerged.some(pr =>
251
+ (pr.labels || []).some(l => agentLabels.has(l.name.toLowerCase()))
252
+ );
253
+ const subject = usesAgent ? 'Agent first-shot rate' : 'PRs sem rework';
254
+ const verb = usesAgent ? 'acertaram de primeira' : 'foram mergeados sem rework';
255
+
256
+ if (pct >= 80) {
257
+ signals.push({
258
+ level: 'green',
259
+ emoji: '🟢',
260
+ title: `**${subject}: ${pct}%**`,
261
+ body: `${firstShots} de ${total} PRs ${verb} esta semana. ✅`
262
+ });
263
+ } else if (pct < 50) {
264
+ signals.push({
265
+ level: 'yellow',
266
+ emoji: '🟡',
267
+ title: `**${subject}: ${pct}% — rework alto**`,
268
+ body: `Apenas ${firstShots} de ${total} PRs sem commits extras.\n→ Specs incompletas ou mudanças de requisito durante implementação.`
269
+ });
270
+ } else {
271
+ signals.push({
272
+ level: 'neutral',
273
+ emoji: '🔵',
274
+ title: `**${subject}: ${pct}%**`,
275
+ body: `${firstShots} de ${total} PRs sem rework commits.`
276
+ });
277
+ }
278
+ }
279
+
280
+ // 4. Unlinked PRs: merged without Closes/Fixes #N
281
+ const unlinkRe = /(?:closes?|fixes?|resolves?)\s+#\d+/i;
282
+ const unlinked = weekMerged.filter(pr => !unlinkRe.test(pr.body || ''));
283
+ if (unlinked.length > 0) {
284
+ const nums = unlinked.map(pr => `#${pr.number}`).join(', ');
285
+ signals.push({
286
+ level: 'yellow',
287
+ emoji: '🔵',
288
+ title: `**${unlinked.length} PR${unlinked.length > 1 ? 's mergeados' : ' mergeado'} sem link de issue**`,
289
+ body: `${nums} não ${unlinked.length > 1 ? 'têm' : 'tem'} \`Closes #N\` no body. Difícil rastrear o que resolveram.\n→ Edite o body do PR ou feche as issues relacionadas manualmente.`
290
+ });
291
+ }
292
+
293
+ // ── Stage Residence Time section (conditional) ──────────────────
294
+ let stageSection = '';
295
+ const jsonlPath = `.agentic-pdlc/metrics/raw/${weekKey}.jsonl`;
296
+ if (fs.existsSync(jsonlPath)) {
297
+ const records = fs.readFileSync(jsonlPath, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
298
+ if (records.length > 0) {
299
+ const STAGES = [
300
+ ['stage:exploration', 'Exploration'],
301
+ ['stage:brainstorming', 'Brainstorming'],
302
+ ['stage:detailing', 'Detailing'],
303
+ ['stage:approval', 'Approval'],
304
+ ['stage:development', 'Development'],
305
+ ['stage:testing', 'Testing'],
306
+ ];
307
+ const byStage = {};
308
+ for (const r of records) {
309
+ if (!byStage[r.stage]) byStage[r.stage] = [];
310
+ byStage[r.stage].push(r.durationDays);
311
+ }
312
+ const avg = arr => arr.reduce((a, b) => a + b, 0) / arr.length;
313
+
314
+ let maxStage = null, maxDays = 0;
315
+ const rows = [];
316
+ for (const [stage, label] of STAGES) {
317
+ const days = byStage[stage];
318
+ if (!days) continue;
319
+ const a = round1(avg(days));
320
+ rows.push(`| **${label}** | ${a}d | ${days.length} |`);
321
+ if (a > maxDays) { maxStage = label; maxDays = a; }
322
+ }
323
+
324
+ if (rows.length > 0) {
325
+ stageSection = `\n---\n\n### 🔄 Stage Residence Time *(board ativo)*\n\n| Stage | Média | Issues |\n|---|---|---|\n${rows.join('\n')}\n\n> \`${maxStage}\` é o maior gargalo (${maxDays}d avg). Considere quebrar specs ou aumentar cadência de revisão nessa fase.\n`;
326
+ }
327
+ }
328
+ }
329
+
330
+ // ── Narrative ───────────────────────────────────────────────────
331
+ const reds = signals.filter(s => s.level === 'red').length;
332
+ const yellows = signals.filter(s => s.level === 'yellow').length;
333
+
334
+ let narrative;
335
+ if (reds >= 2) narrative = '⚠️ Fluxo sob pressão esta semana. Múltiplos bloqueios identificados. Priorize limpar issues orphans e revisar PRs pendentes antes de iniciar novas features.';
336
+ else if (reds === 1 && yellows >= 1) narrative = 'Semana mista. Um ponto crítico e sinais de desaceleração. Verifique os itens marcados em 🔴 antes de avançar.';
337
+ else if (reds === 0 && yellows === 0) narrative = '🟢 Fluxo saudável. Nenhum bloqueio identificado esta semana. Bom momento para avançar em novas features.';
338
+ else narrative = 'Fluxo normal. Alguns ajustes de processo podem melhorar rastreabilidade e velocidade.';
339
+
340
+ // ── Build issue body ────────────────────────────────────────────
341
+ const signalLines = signals.map(s =>
342
+ `${s.emoji} ${s.title}${s.body ? '\n' + s.body : ''}`
343
+ ).join('\n\n');
344
+
345
+ const nextWeekNum = (() => {
346
+ const d = new Date(); d.setDate(d.getDate() + 7);
347
+ const dt = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
348
+ dt.setUTCDate(dt.getUTCDate() + 4 - (dt.getUTCDay() || 7));
349
+ const y = new Date(Date.UTC(dt.getUTCFullYear(), 0, 1));
350
+ const w = Math.ceil((((dt - y) / 86400000) + 1) / 7);
351
+ return `${dt.getUTCFullYear()}-W${String(w).padStart(2, '0')}`;
352
+ })();
353
+
354
+ const body = [
355
+ '> Auto-generated by [agentic-pdlc](https://github.com/rafaeltcosta86/agentic-pdlc). Appears every Sunday.',
356
+ '',
357
+ narrative,
358
+ '',
359
+ '---',
360
+ '',
361
+ '### Sinais desta semana',
362
+ '',
363
+ signalLines,
364
+ stageSection,
365
+ '---',
366
+ '*Próximo pulse: ' + nextWeekNum + '*',
367
+ '',
368
+ '💬 Este pulse foi útil? [Conta pra gente](https://github.com/rafaeltcosta86/agentic-pdlc/issues/new?template=pulse-feedback.md&title=' + encodeURIComponent('[Pulse] ' + weekKey) + ') — 1 clique, sem formulário.'
369
+ ].join('\n');
370
+
371
+ const dateRange = weekDateRange(weekKey);
372
+ core.exportVariable('PULSE_TITLE', '📊 Agentic Pulse — ' + weekKey + ' (' + dateRange + ')');
373
+ core.exportVariable('PULSE_BODY', body);
374
+ console.log(`Built pulse issue for ${weekKey} — ${signals.length} signals, ${reds} red, ${yellows} yellow`);
375
+
376
+ - name: Create Weekly Pulse Issue
377
+ uses: actions/github-script@v7
378
+ with:
379
+ script: |
380
+ const { owner, repo } = context.repo;
381
+ const title = process.env.PULSE_TITLE;
382
+ const body = process.env.PULSE_BODY;
383
+ const LABEL = 'metrics:weekly';
384
+
385
+ // Ensure label exists
386
+ try {
387
+ await github.rest.issues.getLabel({ owner, repo, name: LABEL });
388
+ } catch (e) {
389
+ await github.rest.issues.createLabel({
390
+ owner, repo, name: LABEL,
391
+ color: '0075ca',
392
+ description: 'Auto-generated weekly metrics pulse'
393
+ });
394
+ console.log(`Created label ${LABEL}`);
395
+ }
396
+
397
+ // Close previous pulse issues
398
+ const prev = await github.rest.issues.listForRepo({
399
+ owner, repo, labels: LABEL, state: 'open', per_page: 20
400
+ });
401
+ for (const issue of prev.data) {
402
+ if (issue.title !== title) {
403
+ await github.rest.issues.update({ owner, repo, issue_number: issue.number, state: 'closed' });
404
+ console.log(`Closed previous pulse: #${issue.number}`);
405
+ }
406
+ }
407
+
408
+ // Create new pulse issue
409
+ const created = await github.rest.issues.create({
410
+ owner, repo, title, body, labels: [LABEL]
411
+ });
412
+ console.log(`✅ Created pulse issue: #${created.data.number} — ${title}`);
@@ -0,0 +1,16 @@
1
+ name: Auto Approve PRs
2
+ on:
3
+ pull_request:
4
+ types: [opened, labeled, synchronize]
5
+
6
+ permissions:
7
+ pull-requests: write
8
+
9
+ jobs:
10
+ auto-approve:
11
+ runs-on: ubuntu-latest
12
+ if: contains(github.event.pull_request.labels.*.name, 'auto-approve')
13
+ steps:
14
+ - uses: hmarr/auto-approve-action@v4
15
+ with:
16
+ github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,40 @@
1
+ name: Sentinel / CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [ main ]
6
+ push:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ validate:
11
+ name: Run tests and linters
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Setup environment
17
+ run: echo "Replace this with your language/toolchain setup (e.g., actions/setup-node)"
18
+
19
+ - name: Install dependencies
20
+ run: echo "Replace this with your package manager install command"
21
+
22
+ - name: Run Linters
23
+ if: ${{ !contains('{{LINT_COMMAND}}', '{{') }}
24
+ run: |
25
+ {{LINT_COMMAND}}
26
+
27
+ - name: Typecheck
28
+ if: ${{ !contains('{{TYPECHECK_COMMAND}}', '{{') }}
29
+ run: |
30
+ {{TYPECHECK_COMMAND}}
31
+
32
+ - name: Build
33
+ if: ${{ !contains('{{BUILD_COMMAND}}', '{{') }}
34
+ run: |
35
+ {{BUILD_COMMAND}}
36
+
37
+ - name: Run Tests
38
+ if: ${{ !contains('{{TEST_COMMAND}}', '{{') }}
39
+ run: |
40
+ {{TEST_COMMAND}}
@@ -0,0 +1,123 @@
1
+ name: PDLC Health Check (Drift Detection)
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ schedule:
6
+ - cron: '0 8 * * 1' # Every Monday at 8am
7
+
8
+ env:
9
+ PROJECT_ID: "{{PROJECT_ID}}"
10
+ STATUS_FIELD_ID: "{{STATUS_FIELD_ID}}"
11
+ STATUS_EXPLORATION: "{{ID_EXPLORATION}}"
12
+ STATUS_BRAINSTORMING: "{{ID_BRAINSTORMING}}"
13
+ STATUS_DETAILING: "{{ID_DETAILING}}"
14
+ STATUS_APPROVAL: "{{ID_APPROVAL}}"
15
+ STATUS_DEVELOPMENT: "{{ID_DEVELOPMENT}}"
16
+ STATUS_TESTING: "{{ID_TESTING}}"
17
+ STATUS_CODE_REVIEW_PR: "{{ID_CODE_REVIEW_PR}}"
18
+ STATUS_PRODUCTION: "{{ID_PRODUCTION}}"
19
+
20
+ jobs:
21
+ check-drift:
22
+ name: Detect Project Board Drift
23
+ runs-on: ubuntu-latest
24
+ env:
25
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
26
+ permissions:
27
+ issues: write
28
+ steps:
29
+ - name: Validate Board Configuration
30
+ if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
31
+ uses: actions/github-script@v7
32
+ with:
33
+ github-token: ${{ env.PROJECT_TOKEN }}
34
+ script: |
35
+ const projectId = process.env.PROJECT_ID;
36
+ const statusFieldId = process.env.STATUS_FIELD_ID;
37
+ const envVars = {
38
+ 'STATUS_EXPLORATION': process.env.STATUS_EXPLORATION,
39
+ 'STATUS_BRAINSTORMING': process.env.STATUS_BRAINSTORMING,
40
+ 'STATUS_DETAILING': process.env.STATUS_DETAILING,
41
+ 'STATUS_APPROVAL': process.env.STATUS_APPROVAL,
42
+ 'STATUS_DEVELOPMENT': process.env.STATUS_DEVELOPMENT,
43
+ 'STATUS_TESTING': process.env.STATUS_TESTING,
44
+ 'STATUS_CODE_REVIEW_PR': process.env.STATUS_CODE_REVIEW_PR,
45
+ 'STATUS_PRODUCTION': process.env.STATUS_PRODUCTION
46
+ };
47
+
48
+ const query = `
49
+ query($projectId: ID!) {
50
+ node(id: $projectId) {
51
+ ... on ProjectV2 {
52
+ title
53
+ fields(first: 20) {
54
+ nodes {
55
+ ... on ProjectV2SingleSelectField {
56
+ id
57
+ name
58
+ options {
59
+ id
60
+ name
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ `;
69
+
70
+ let result;
71
+ try {
72
+ result = await github.graphql(query, { projectId });
73
+ } catch (error) {
74
+ console.log("❌ Error fetching project. Verify your PROJECT_ID.");
75
+ console.log(error);
76
+ return;
77
+ }
78
+
79
+ const project = result.node;
80
+ if (!project) {
81
+ console.log("❌ Project not found.");
82
+ return;
83
+ }
84
+
85
+ const statusField = project.fields.nodes.find(f => f.id === statusFieldId);
86
+ if (!statusField) {
87
+ console.log("❌ Status field not found.");
88
+ return;
89
+ }
90
+
91
+ const validOptions = statusField.options;
92
+ const validOptionIds = validOptions.map(o => o.id);
93
+
94
+ let hasDrift = false;
95
+ let missingVars = [];
96
+
97
+ for (const [varName, id] of Object.entries(envVars)) {
98
+ if (id && !id.startsWith('{{') && !validOptionIds.includes(id)) {
99
+ hasDrift = true;
100
+ missingVars.push(varName);
101
+ }
102
+ }
103
+
104
+ if (hasDrift) {
105
+ console.log("🚨 Drift detected! The following mapped columns no longer exist: " + missingVars.join(", "));
106
+
107
+ let table = "| Column Name | New ID |\n|---|---|\n";
108
+ validOptions.forEach(opt => {
109
+ table += `| ${opt.name} | \`${opt.id}\` |\n`;
110
+ });
111
+
112
+ const body = `🚨 **Agentic PDLC Drift Detected**\n\nThe following columns mapped in your \`.github/workflows/project-automation.yml\` no longer exist in your project board:\n\n**${missingVars.join(", ")}**\n\n### How to fix it:\nHere is the list of current columns in your board with their valid IDs. Please update the \`env\` block in your \`.github/workflows/project-automation.yml\` and \`.github/workflows/pdlc-health-check.yml\`.\n\n${table}`;
113
+
114
+ await github.rest.issues.create({
115
+ owner: context.repo.owner,
116
+ repo: context.repo.repo,
117
+ title: '🚨 Agentic PDLC Drift Detected in Project Board',
118
+ body: body,
119
+ labels: ['bug']
120
+ });
121
+ } else {
122
+ console.log("✅ No drift detected. Board configuration is healthy.");
123
+ }
@@ -0,0 +1,51 @@
1
+ name: PDLC Stage Gate
2
+ on:
3
+ pull_request:
4
+ types: [opened, synchronize, reopened, labeled, unlabeled]
5
+
6
+ permissions:
7
+ pull-requests: read
8
+ issues: read
9
+
10
+ jobs:
11
+ stage-gate:
12
+ name: PDLC Stage Gate
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Check stage:approval
16
+ env:
17
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18
+ run: |
19
+ set -e
20
+ REPO="${{ github.repository }}"
21
+ PR_NUMBER="${{ github.event.pull_request.number }}"
22
+
23
+ PR_LABELS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '[.labels[].name] | join(" ")')
24
+ if echo "$PR_LABELS" | grep -qw "hotfix"; then
25
+ echo "✅ Hotfix label — stage gate bypassed."
26
+ exit 0
27
+ fi
28
+
29
+ PR_BODY=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json body --jq '.body // ""')
30
+ ISSUE_NUMS=$(echo "$PR_BODY" | grep -oiE '(Closes?|Fixes?|Resolves?)\s+#([0-9]+)' | grep -oE '[0-9]+' || true)
31
+
32
+ if [ -z "$ISSUE_NUMS" ]; then
33
+ echo "❌ No linked issues in PR body."
34
+ echo " Add 'Closes #N' to PR body, or add 'hotfix' label to PR for emergencies."
35
+ exit 1
36
+ fi
37
+
38
+ for NUM in $ISSUE_NUMS; do
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
41
+ echo "✅ Issue #$NUM approved"
42
+ else
43
+ STAGE=$(echo "$LABELS" | tr ' ' '\n' | grep "^stage:" | head -1 || echo "none")
44
+ echo "❌ Issue #$NUM missing approval (current: $STAGE)"
45
+ echo " Required: stage:approval OR spec:approved OR stage:development label on the issue."
46
+ echo " Emergency bypass: add 'hotfix' label to this PR."
47
+ exit 1
48
+ fi
49
+ done
50
+
51
+ echo "✅ All linked issues approved."