create-agentic-pdlc 2.1.6 → 2.2.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 +73 -0
- package/.agentic-pdlc/hooks/pdlc-stage-gate.sh +39 -0
- package/.agentic-pdlc/metrics/.gitkeep +0 -0
- package/.agentic-pdlc/metrics/2026-W19.md +21 -0
- package/.agentic-pdlc/metrics/raw/2026-W19.jsonl +6 -0
- package/.agentic-pdlc/metrics/raw/2026-W20.jsonl +1 -0
- package/.agentic-pdlc/templates/.github/CODEOWNERS +5 -0
- package/.agentic-pdlc/templates/.github/copilot-instructions.md +12 -0
- package/.agentic-pdlc/templates/.github/workflows/add-to-board.yml +38 -0
- package/.agentic-pdlc/templates/.github/workflows/agent-trigger.yml +146 -0
- package/.agentic-pdlc/templates/.github/workflows/agentic-metrics.yml +412 -0
- package/.agentic-pdlc/templates/.github/workflows/auto-approve.yml +16 -0
- package/.agentic-pdlc/templates/.github/workflows/ci.yml +40 -0
- package/.agentic-pdlc/templates/.github/workflows/pdlc-health-check.yml +123 -0
- package/.agentic-pdlc/templates/.github/workflows/pdlc-stage-gate.yml +51 -0
- package/.agentic-pdlc/templates/.github/workflows/project-automation.yml +278 -0
- package/.agentic-pdlc/templates/.github/workflows/protect-workflows.yml +21 -0
- package/.agentic-pdlc/templates/.github/workflows/qa-agent.yml +128 -0
- package/.agentic-pdlc/templates/AGENTS.md +81 -0
- package/.agentic-pdlc/templates/docs/pdlc.md +15 -5
- package/.agentic-setup-prompt.md +73 -0
- package/.agentic-setup.md +73 -0
- package/.claude/settings.json +15 -0
- package/.cursorrules +9 -0
- package/.github/ISSUE_TEMPLATE/pulse-feedback.md +11 -0
- package/.github/workflows/add-to-board.yml +38 -0
- package/.github/workflows/agent-trigger.yml +30 -43
- package/.github/workflows/agentic-metrics.yml +412 -0
- package/.github/workflows/pdlc-health-check.yml +10 -10
- package/.github/workflows/pdlc-stage-gate.yml +51 -0
- package/.github/workflows/project-automation.yml +68 -18
- package/.github/workflows/qa-agent.yml +112 -11
- package/CLAUDE.md +9 -0
- package/SETUP.md +28 -0
- package/adapters/claude-code/skill.md +41 -3
- package/adapters/hooks/pdlc-stage-gate.sh +44 -0
- package/bin/cli.js +28 -5
- package/docs/pdlc.md +15 -5
- package/package.json +1 -1
- package/pr_comments.json +20 -0
- package/templates/.github/workflows/add-to-board.yml +38 -0
- package/templates/.github/workflows/agent-trigger.yml +34 -4
- package/templates/.github/workflows/pdlc-stage-gate.yml +51 -0
- package/templates/.github/workflows/project-automation.yml +78 -54
- package/templates/.github/workflows/qa-agent.yml +14 -13
- 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}`);
|
|
@@ -6,16 +6,16 @@ on:
|
|
|
6
6
|
- cron: '0 8 * * 1' # Every Monday at 8am
|
|
7
7
|
|
|
8
8
|
env:
|
|
9
|
-
PROJECT_ID: "
|
|
10
|
-
STATUS_FIELD_ID: "
|
|
11
|
-
STATUS_EXPLORATION: "
|
|
12
|
-
STATUS_BRAINSTORMING: "
|
|
13
|
-
STATUS_DETAILING: "
|
|
14
|
-
STATUS_APPROVAL: "
|
|
15
|
-
STATUS_DEVELOPMENT: "
|
|
16
|
-
STATUS_TESTING: "
|
|
17
|
-
STATUS_CODE_REVIEW_PR: "
|
|
18
|
-
STATUS_PRODUCTION: "
|
|
9
|
+
PROJECT_ID: "PVT_kwHODpFFL84BXg7h"
|
|
10
|
+
STATUS_FIELD_ID: "PVTSSF_lAHODpFFL84BXg7hzhStRHI"
|
|
11
|
+
STATUS_EXPLORATION: "96ac537d"
|
|
12
|
+
STATUS_BRAINSTORMING: "8eb07c5b"
|
|
13
|
+
STATUS_DETAILING: "9f6ce70e"
|
|
14
|
+
STATUS_APPROVAL: "31bf4610"
|
|
15
|
+
STATUS_DEVELOPMENT: "2c9e78e6"
|
|
16
|
+
STATUS_TESTING: "96b59ade"
|
|
17
|
+
STATUS_CODE_REVIEW_PR: "86ca9720"
|
|
18
|
+
STATUS_PRODUCTION: "1581e5bd"
|
|
19
19
|
|
|
20
20
|
jobs:
|
|
21
21
|
check-drift:
|
|
@@ -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."
|
|
@@ -2,24 +2,24 @@ name: PDLC Board Automation
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
pull_request:
|
|
5
|
-
types: [opened, reopened, closed]
|
|
5
|
+
types: [opened, reopened, closed, labeled]
|
|
6
6
|
pull_request_review:
|
|
7
7
|
types: [submitted]
|
|
8
8
|
issues:
|
|
9
9
|
types: [labeled]
|
|
10
10
|
|
|
11
11
|
env:
|
|
12
|
-
PROJECT_ID: "
|
|
13
|
-
STATUS_FIELD_ID: "
|
|
14
|
-
STATUS_IDEA: "
|
|
15
|
-
STATUS_EXPLORATION: "
|
|
16
|
-
STATUS_BRAINSTORMING: "
|
|
17
|
-
STATUS_DETAILING: "
|
|
18
|
-
STATUS_APPROVAL: "
|
|
19
|
-
STATUS_DEVELOPMENT: "
|
|
20
|
-
STATUS_TESTING: "
|
|
21
|
-
STATUS_CODE_REVIEW_PR: "
|
|
22
|
-
STATUS_PRODUCTION: "
|
|
12
|
+
PROJECT_ID: "PVT_kwHODpFFL84BXg7h"
|
|
13
|
+
STATUS_FIELD_ID: "PVTSSF_lAHODpFFL84BXg7hzhStRHI"
|
|
14
|
+
STATUS_IDEA: "bb6e5a20"
|
|
15
|
+
STATUS_EXPLORATION: "96ac537d"
|
|
16
|
+
STATUS_BRAINSTORMING: "8eb07c5b"
|
|
17
|
+
STATUS_DETAILING: "9f6ce70e"
|
|
18
|
+
STATUS_APPROVAL: "31bf4610"
|
|
19
|
+
STATUS_DEVELOPMENT: "2c9e78e6"
|
|
20
|
+
STATUS_TESTING: "96b59ade"
|
|
21
|
+
STATUS_CODE_REVIEW_PR: "86ca9720"
|
|
22
|
+
STATUS_PRODUCTION: "1581e5bd"
|
|
23
23
|
|
|
24
24
|
jobs:
|
|
25
25
|
# Issue Labeled → Move Upstream
|
|
@@ -115,16 +115,15 @@ jobs:
|
|
|
115
115
|
# });
|
|
116
116
|
# console.log(`Issue #${number} moved to Idea`);
|
|
117
117
|
|
|
118
|
-
# PR Opened → Move linked issue to
|
|
119
|
-
# 💡 VARIANT B (QA Agent): If using an AI QA agent, change `STATUS_CODE_REVIEW_PR` to `STATUS_TESTING` on line ~158
|
|
118
|
+
# PR Opened → Move linked issue to Testing (Variant B — QA Agent enabled)
|
|
120
119
|
move-card-on-pr-open:
|
|
121
|
-
name: Open PR →
|
|
120
|
+
name: Open PR → Testing
|
|
122
121
|
if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened')
|
|
123
122
|
runs-on: ubuntu-latest
|
|
124
123
|
env:
|
|
125
124
|
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
126
125
|
steps:
|
|
127
|
-
- name: Move linked issue to
|
|
126
|
+
- name: Move linked issue to Testing
|
|
128
127
|
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
129
128
|
uses: actions/github-script@v7
|
|
130
129
|
with:
|
|
@@ -137,6 +136,59 @@ jobs:
|
|
|
137
136
|
const body = pr.body ?? '';
|
|
138
137
|
|
|
139
138
|
// Extract issues linked via "Closes #N", "Fixes #N", "Resolves #N"
|
|
139
|
+
const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
|
|
140
|
+
.map(m => parseInt(m[1]));
|
|
141
|
+
|
|
142
|
+
const moveItem = async (nodeId) => {
|
|
143
|
+
const { addProjectV2ItemById: { item } } = await github.graphql(`
|
|
144
|
+
mutation($p: ID!, $c: ID!) {
|
|
145
|
+
addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
|
|
146
|
+
}`, { p: process.env.PROJECT_ID, c: nodeId });
|
|
147
|
+
|
|
148
|
+
await github.graphql(`
|
|
149
|
+
mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
|
|
150
|
+
updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
|
|
151
|
+
projectV2Item { id }
|
|
152
|
+
}
|
|
153
|
+
}`, {
|
|
154
|
+
p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
|
|
155
|
+
v: { singleSelectOptionId: process.env.STATUS_TESTING }
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (linkedIssues.length > 0) {
|
|
160
|
+
for (const n of linkedIssues) {
|
|
161
|
+
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
|
|
162
|
+
await moveItem(issue.node_id);
|
|
163
|
+
console.log(`Issue #${n} → Testing`);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
await moveItem(pr.node_id);
|
|
167
|
+
console.log(`PR #${prNumber} → Testing (no linked issue)`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: ['pr:in-review'] }).catch(() => {});
|
|
171
|
+
|
|
172
|
+
# QA Approved → Move linked issue to Code Review / PR
|
|
173
|
+
move-card-on-qa-pass:
|
|
174
|
+
name: qa:approved → Code Review / PR
|
|
175
|
+
if: github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'qa:approved'
|
|
176
|
+
runs-on: ubuntu-latest
|
|
177
|
+
env:
|
|
178
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
179
|
+
steps:
|
|
180
|
+
- name: Move linked issue to Code Review / PR
|
|
181
|
+
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
182
|
+
uses: actions/github-script@v7
|
|
183
|
+
with:
|
|
184
|
+
github-token: ${{ env.PROJECT_TOKEN }}
|
|
185
|
+
script: |
|
|
186
|
+
const prNumber = context.payload.pull_request.number;
|
|
187
|
+
const { owner, repo } = context.repo;
|
|
188
|
+
|
|
189
|
+
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
|
190
|
+
const body = pr.body ?? '';
|
|
191
|
+
|
|
140
192
|
const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
|
|
141
193
|
.map(m => parseInt(m[1]));
|
|
142
194
|
|
|
@@ -168,8 +220,6 @@ jobs:
|
|
|
168
220
|
console.log(`PR #${prNumber} → Code Review / PR (no linked issue)`);
|
|
169
221
|
}
|
|
170
222
|
|
|
171
|
-
await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: ['pr:in-review'] }).catch(() => {});
|
|
172
|
-
|
|
173
223
|
# Review Approved → Add Label
|
|
174
224
|
move-card-on-review-approved:
|
|
175
225
|
name: Approved PR → Add Label
|