create-agentic-pdlc 3.0.0 → 3.1.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.
- package/.coderabbit.yaml +7 -1
- package/.github/workflows/add-to-board.yml +55 -7
- package/.github/workflows/agent-trigger.yml +57 -25
- package/.github/workflows/board-reconciliation.yml +176 -0
- package/.github/workflows/pdlc-health-check.yml +81 -81
- package/.github/workflows/project-automation.yml +256 -209
- package/adapters/claude-code/skill.md +5 -5
- package/bin/cli.js +112 -19
- package/docs/superpowers/plans/2026-06-05-archive-card-on-issue-close.md +105 -0
- package/docs/superpowers/plans/2026-06-05-project-id-actions-variable.md +336 -0
- package/docs/superpowers/specs/2026-06-05-project-id-actions-variable-design.md +114 -0
- package/package.json +1 -1
- package/scripts/derive-column.js +20 -0
- package/templates/.github/workflows/add-to-board.yml +2 -2
- package/templates/.github/workflows/agent-trigger.yml +2 -2
- package/templates/.github/workflows/pdlc-health-check.yml +2 -2
- package/templates/.github/workflows/project-automation.yml +47 -8
- package/tests/cli.test.js +86 -0
package/bin/cli.js
CHANGED
|
@@ -60,6 +60,7 @@ const i18n = {
|
|
|
60
60
|
cursor_rules_written: t('✅ Default cursor rules written to .cursorrules', '✅ Regras padrão do cursor salvas em .cursorrules', '✅ Reglas por defecto de cursor guardadas en .cursorrules'),
|
|
61
61
|
setup_done: t('🎉 All set! Continue the setup with your agent:', '🎉 Aqui tá pronto! Continue o setup com o seu agente:', '🎉 ¡Listo! Continúa el setup con tu agente:'),
|
|
62
62
|
setup_done_hint: t('>>> Tell it to read and execute the .agentic-setup.md file!', '>>> Diga a ele para ler e executar o arquivo .agentic-setup.md!', '>>> Dile que lea y ejecute el archivo .agentic-setup.md!'),
|
|
63
|
+
upgrade_hint: t('💡 To add the full board + multi-agent automation later: npx create-agentic-pdlc --upgrade-to-agentic', '💡 Para adicionar o board completo + automação multi-agente mais tarde: npx create-agentic-pdlc --upgrade-to-agentic', '💡 Para agregar el tablero completo + automatización multi-agente más tarde: npx create-agentic-pdlc --upgrade-to-agentic'),
|
|
63
64
|
update_title: t('agentic-pdlc — Agent Configuration Status', 'agentic-pdlc — Status de Configuração dos Agentes', 'agentic-pdlc — Estado de Configuración de Agentes'),
|
|
64
65
|
update_no_context: t('❌ No .agentic-pdlc/cli-context.json found. Run npx create-agentic-pdlc first.', '❌ Arquivo .agentic-pdlc/cli-context.json não encontrado. Rode npx create-agentic-pdlc primeiro.', '❌ Archivo .agentic-pdlc/cli-context.json no encontrado. Ejecuta npx create-agentic-pdlc primero.'),
|
|
65
66
|
update_all_ok: t('All agents configured!', 'Todos os agentes configurados!', '¡Todos los agentes configurados!'),
|
|
@@ -74,12 +75,36 @@ const i18n = {
|
|
|
74
75
|
update_sentinel_ask: t(" Activate? CodeRabbit applies 'architecture-violation' label when violations are detected. (Y/n): ", " Ativar? CodeRabbit aplica a label 'architecture-violation' quando detecta violações. (S/n): ", " ¿Activar? CodeRabbit aplica la etiqueta 'architecture-violation' cuando detecta violaciones. (S/n): "),
|
|
75
76
|
configuring_protection: t('[3/3] Configuring branch protection...', '[3/3] Configurando proteção de branch...', '[3/3] Configurando protección de rama...'),
|
|
76
77
|
protection_ok: t('✅ Branch protection set — required checks: PDLC Stage Gate, QA Gate.', '✅ Proteção de branch configurada — checks obrigatórios: PDLC Stage Gate, QA Gate.', '✅ Protección de rama configurada — checks requeridos: PDLC Stage Gate, QA Gate.'),
|
|
77
|
-
protection_warn: t(
|
|
78
|
+
protection_warn: t(
|
|
79
|
+
'⚠️ Branch protection requires admin access — could not be set automatically.\n\n What it does: prevents PRs from merging unless these CI checks pass:\n • "PDLC Stage Gate" — blocks merge if the linked issue lacks spec:approved\n • "QA Gate" — blocks merge if automated QA checks failed\n\n Without it: the workflow still runs, but PRs can be merged without approval.\n\n To enable: Settings → Branches → main → Required status checks\n Add: "PDLC Stage Gate" and "QA Gate"',
|
|
80
|
+
'⚠️ Proteção de branch requer acesso de admin — não pôde ser configurada automaticamente.\n\n O que faz: impede que PRs sejam mergeados sem que esses checks de CI passem:\n • "PDLC Stage Gate" — bloqueia merge se a issue não tiver spec:approved\n • "QA Gate" — bloqueia merge se os checks automáticos de QA falharem\n\n Sem ela: o workflow continua funcionando, mas PRs podem ser mergeados sem aprovação.\n\n Para ativar: Settings → Branches → main → Required status checks\n Adicionar: "PDLC Stage Gate" e "QA Gate"',
|
|
81
|
+
'⚠️ La protección de rama requiere acceso de administrador — no se pudo configurar automáticamente.\n\n Qué hace: impide que los PRs se fusionen sin que pasen estos checks de CI:\n • "PDLC Stage Gate" — bloquea el merge si la issue no tiene spec:approved\n • "QA Gate" — bloquea el merge si los checks automáticos de QA fallaron\n\n Sin ella: el flujo sigue funcionando, pero los PRs se pueden fusionar sin aprobación.\n\n Para activar: Settings → Branches → main → Required status checks\n Agregar: "PDLC Stage Gate" y "QA Gate"'
|
|
82
|
+
),
|
|
78
83
|
issue_templates_copied: t(
|
|
79
84
|
'✅ Issue templates copied to .github/ISSUE_TEMPLATE/',
|
|
80
85
|
'✅ Issue templates copiados para .github/ISSUE_TEMPLATE/',
|
|
81
86
|
'✅ Issue templates copiados a .github/ISSUE_TEMPLATE/'
|
|
82
87
|
),
|
|
88
|
+
vars_project_id_ok: t(
|
|
89
|
+
'✅ vars.PROJECT_ID set as Actions Variable.',
|
|
90
|
+
'✅ vars.PROJECT_ID configurado como Variável do Actions.',
|
|
91
|
+
'✅ vars.PROJECT_ID configurado como Variable de Actions.'
|
|
92
|
+
),
|
|
93
|
+
vars_project_id_warn: t(
|
|
94
|
+
'⚠️ Could not set vars.PROJECT_ID — token may lack variables:write scope.\n Set manually: repo Settings → Secrets and variables → Variables → PROJECT_ID = ',
|
|
95
|
+
'⚠️ Não foi possível configurar vars.PROJECT_ID — o token pode não ter permissão variables:write.\n Configure manualmente: repo Settings → Secrets and variables → Variables → PROJECT_ID = ',
|
|
96
|
+
'⚠️ No se pudo configurar vars.PROJECT_ID — el token puede no tener permiso variables:write.\n Configura manualmente: repo Settings → Secrets and variables → Variables → PROJECT_ID = '
|
|
97
|
+
),
|
|
98
|
+
gh_not_installed: t(
|
|
99
|
+
'\n⚠️ GitHub CLI (gh) is not installed. Skipping label creation.',
|
|
100
|
+
'\n⚠️ GitHub CLI (gh) não está instalado. Pulando criação de labels.',
|
|
101
|
+
'\n⚠️ GitHub CLI (gh) no está instalado. Omitiendo creación de etiquetas.'
|
|
102
|
+
),
|
|
103
|
+
repo_context_missing: t(
|
|
104
|
+
'\n⚠️ Repository owner or name missing in cli-context.json. Automatic label creation will be skipped.',
|
|
105
|
+
'\n⚠️ Dono ou nome do repositório ausente em cli-context.json. Criação automática de labels será pulada.',
|
|
106
|
+
'\n⚠️ Propietario o nombre del repositorio faltante en cli-context.json. Se omitirá la creación automática de etiquetas.'
|
|
107
|
+
),
|
|
83
108
|
};
|
|
84
109
|
|
|
85
110
|
const cyan = '\x1b[36m';
|
|
@@ -89,19 +114,23 @@ const yellow = '\x1b[33m';
|
|
|
89
114
|
const red = '\x1b[31m';
|
|
90
115
|
|
|
91
116
|
const PDLC_LABELS = [
|
|
92
|
-
{ name: 'stage:exploration', color: '9b59b6', description: 'Issue is being evaluated' },
|
|
93
117
|
{ name: 'stage:brainstorming', color: 'e84393', description: 'Proposed approaches awaiting PM gate' },
|
|
94
118
|
{ name: 'stage:detailing', color: '3498db', description: 'Technical spec is being written' },
|
|
95
119
|
{ name: 'stage:development', color: 'e67e22', description: 'Agent is implementing the spec' },
|
|
96
|
-
{ name: 'stage:testing', color: '8e44ad', description: 'Agent is testing the implementation' },
|
|
97
120
|
{ name: 'spec:approved', color: '0e8a16', description: 'Spec approved — agent can implement' },
|
|
98
121
|
{ name: 'pr:in-review', color: 'e4e669', description: 'PR awaiting code review' },
|
|
99
122
|
{ name: 'pr:approved', color: '0e8a16', description: 'PR approved, ready for merge' },
|
|
100
123
|
{ name: 'architecture-violation', color: 'd93f0b', description: 'Invariant violation detected by CI' },
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
{ name: 'jules',
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const JULES_LABELS = [
|
|
127
|
+
{ name: 'jules', color: '5319e7', description: 'Jules AI Agent' },
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
const QA_LABELS = [
|
|
131
|
+
{ name: 'qa:approved', color: '0e8a16', description: 'QA Agent approved the implementation' },
|
|
132
|
+
{ name: 'qa:needs-work', color: 'd93f0b', description: 'QA Agent found issues' },
|
|
133
|
+
{ name: 'infra:qa-broken',color: 'F97316', description: 'QA Agent failed to run — manual review required' },
|
|
105
134
|
];
|
|
106
135
|
|
|
107
136
|
function buildBoardUrl(repoOwner, projectNumber, isOrg) {
|
|
@@ -282,6 +311,42 @@ function scaffoldLiteTemplates(sourceDir, targetDir) {
|
|
|
282
311
|
}
|
|
283
312
|
}
|
|
284
313
|
|
|
314
|
+
function createLabelsForRepo(labels, repo) {
|
|
315
|
+
for (const label of labels) {
|
|
316
|
+
try {
|
|
317
|
+
execFileSync('gh', ['label', 'create', label.name, '--color', label.color, '--description', label.description, '--repo', repo, '--force'], { stdio: 'ignore' });
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (err.code === 'ENOENT') {
|
|
320
|
+
console.warn(i18n.gh_not_installed);
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function setActionsVariable(repo, name, value, execFn = execFileSync) {
|
|
328
|
+
try {
|
|
329
|
+
execFn('gh', [
|
|
330
|
+
'api', `repos/${repo}/actions/variables/${name}`,
|
|
331
|
+
'--method', 'PATCH',
|
|
332
|
+
'-f', `name=${name}`,
|
|
333
|
+
'-f', `value=${value}`
|
|
334
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
335
|
+
} catch (err) {
|
|
336
|
+
const msg = (err.stderr?.toString() || '') + (err.message || '');
|
|
337
|
+
if (msg.includes('404') || msg.includes('Not Found')) {
|
|
338
|
+
execFn('gh', [
|
|
339
|
+
'api', `repos/${repo}/actions/variables`,
|
|
340
|
+
'--method', 'POST',
|
|
341
|
+
'-f', `name=${name}`,
|
|
342
|
+
'-f', `value=${value}`
|
|
343
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
344
|
+
} else {
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
285
350
|
function scaffoldFullTemplates(sourceDir, targetDir, projectId, statusFieldId, optionMap, repoOwner, repoName) {
|
|
286
351
|
const destTemplates = path.join(targetDir, '.agentic-pdlc', 'templates');
|
|
287
352
|
fs.mkdirSync(destTemplates, { recursive: true });
|
|
@@ -342,7 +407,6 @@ function scaffoldFullTemplates(sourceDir, targetDir, projectId, statusFieldId, o
|
|
|
342
407
|
const paPath = path.join(destTemplates, '.github', 'workflows', 'project-automation.yml');
|
|
343
408
|
if (fs.existsSync(paPath) && Object.keys(optionMap).length > 0) {
|
|
344
409
|
let wfContent = fs.readFileSync(paPath, 'utf8');
|
|
345
|
-
if (projectId) wfContent = wfContent.replace(/\{\{PROJECT_ID\}\}/g, () => projectId);
|
|
346
410
|
if (statusFieldId) wfContent = wfContent.replace(/\{\{STATUS_FIELD_ID\}\}/g, () => statusFieldId);
|
|
347
411
|
wfContent = wfContent.replace(/\{\{ID_IDEA\}\}/g, () => optionMap['💡 Idea - No move to Exploration directly'] || 'MISSING_ID');
|
|
348
412
|
wfContent = wfContent.replace(/\{\{ID_EXPLORATION\}\}/g, () => optionMap['🔍 Exploration'] || 'MISSING_ID');
|
|
@@ -536,21 +600,31 @@ async function runFullSetup() {
|
|
|
536
600
|
}
|
|
537
601
|
}
|
|
538
602
|
|
|
539
|
-
// Auto-provision
|
|
603
|
+
// Auto-provision PROJECT_TOKEN for personal repos
|
|
540
604
|
let patAutoSet = false;
|
|
541
605
|
if (projectId && !isOrg) {
|
|
542
606
|
try {
|
|
543
607
|
const tokenOut = execFileSync('gh', ['auth', 'token'], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim();
|
|
544
608
|
if (tokenOut) {
|
|
545
|
-
execFileSync('gh', ['secret', 'set', '
|
|
609
|
+
execFileSync('gh', ['secret', 'set', 'PROJECT_TOKEN', '--body', tokenOut, '--repo', repo], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
546
610
|
patAutoSet = true;
|
|
547
|
-
console.log(`\n${green}✅
|
|
611
|
+
console.log(`\n${green}✅ PROJECT_TOKEN secret set automatically (uses your gh OAuth token).${reset}`);
|
|
548
612
|
}
|
|
549
613
|
} catch (err) {
|
|
550
|
-
console.log(`\n${yellow}⚠️ Could not auto-set
|
|
614
|
+
console.log(`\n${yellow}⚠️ Could not auto-set PROJECT_TOKEN. Agent will guide manual setup.${reset}`);
|
|
551
615
|
}
|
|
552
616
|
} else if (projectId && isOrg) {
|
|
553
|
-
console.log(`\n${yellow}ℹ️ Org repo detected —
|
|
617
|
+
console.log(`\n${yellow}ℹ️ Org repo detected — PROJECT_TOKEN will require manual setup for security.${reset}`);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Set PROJECT_ID as GitHub Actions Variable
|
|
621
|
+
if (projectId) {
|
|
622
|
+
try {
|
|
623
|
+
setActionsVariable(repo, 'PROJECT_ID', projectId);
|
|
624
|
+
console.log(`${green}${i18n.vars_project_id_ok}${reset}`);
|
|
625
|
+
} catch (_) {
|
|
626
|
+
console.log(`${yellow}${i18n.vars_project_id_warn}${projectId}${reset}`);
|
|
627
|
+
}
|
|
554
628
|
}
|
|
555
629
|
|
|
556
630
|
await setBranchProtection(repo, ['PDLC Stage Gate', 'QA Gate']);
|
|
@@ -710,6 +784,10 @@ async function runUpdate() {
|
|
|
710
784
|
return;
|
|
711
785
|
}
|
|
712
786
|
|
|
787
|
+
const repo = ctx.repoOwner && ctx.repoName ? `${ctx.repoOwner}/${ctx.repoName}` : null;
|
|
788
|
+
if (!repo) {
|
|
789
|
+
console.warn(`${yellow}${i18n.repo_context_missing}${reset}`);
|
|
790
|
+
}
|
|
713
791
|
const paPath = path.join(targetDir, '.github', 'workflows', 'project-automation.yml');
|
|
714
792
|
const atPath = path.join(targetDir, '.github', 'workflows', 'agent-trigger.yml');
|
|
715
793
|
const results = [];
|
|
@@ -718,11 +796,14 @@ async function runUpdate() {
|
|
|
718
796
|
console.log(`\n${cyan}${i18n.update_jules_header}${reset}`);
|
|
719
797
|
const choice = (await askQuestion(i18n.update_jules_ask)).trim().toLowerCase();
|
|
720
798
|
if (choice === 'a' || choice === '') {
|
|
799
|
+
if (repo) createLabelsForRepo(JULES_LABELS, repo);
|
|
721
800
|
configureJules(atPath, '@google-labs-jules', 'jules');
|
|
722
801
|
results.push(t('✅ Jules configured (@google-labs-jules)', '✅ Jules configurado (@google-labs-jules)', '✅ Jules configurado (@google-labs-jules)'));
|
|
723
802
|
} else if (choice === 'b') {
|
|
724
803
|
const handle = (await askQuestion(i18n.update_jules_ask_handle)).trim();
|
|
725
|
-
|
|
804
|
+
const labelName = handle.replace('@', '').toLowerCase();
|
|
805
|
+
if (repo) createLabelsForRepo([{ name: labelName, color: '5319e7', description: `${handle} AI Agent` }], repo);
|
|
806
|
+
configureJules(atPath, handle, labelName);
|
|
726
807
|
results.push(t(`✅ Agent configured (${handle})`, `✅ Agente configurado (${handle})`, `✅ Agente configurado (${handle})`));
|
|
727
808
|
} else {
|
|
728
809
|
results.push(t('⏭ Jules — skipped', '⏭ Jules — pulado', '⏭ Jules — omitido'));
|
|
@@ -733,6 +814,7 @@ async function runUpdate() {
|
|
|
733
814
|
console.log(`\n${cyan}${i18n.update_qa_header}${reset}`);
|
|
734
815
|
const answer = (await askQuestion(i18n.update_qa_ask)).trim().toLowerCase();
|
|
735
816
|
if (!['n', 'no', 'não', 'nao'].includes(answer)) {
|
|
817
|
+
if (repo) createLabelsForRepo(QA_LABELS, repo);
|
|
736
818
|
activateQaAgent(paPath);
|
|
737
819
|
results.push(t(
|
|
738
820
|
'✅ QA Agent configured — Variant B activated (uses GITHUB_TOKEN, no extra secrets needed)',
|
|
@@ -790,7 +872,7 @@ function resolveMode(args) {
|
|
|
790
872
|
}
|
|
791
873
|
|
|
792
874
|
// Export for testing
|
|
793
|
-
if (typeof module !== 'undefined') module.exports = { resolveMode };
|
|
875
|
+
if (typeof module !== 'undefined') module.exports = { resolveMode, setActionsVariable, scaffoldLiteTemplates, scaffoldFullTemplates };
|
|
794
876
|
|
|
795
877
|
// ─── runLiteSetup ─────────────────────────────────────────────────────────────
|
|
796
878
|
|
|
@@ -849,6 +931,7 @@ async function runLiteSetup() {
|
|
|
849
931
|
});
|
|
850
932
|
|
|
851
933
|
copyAdapterFiles(agent, sourceDir, targetDir);
|
|
934
|
+
console.log(`${cyan}${i18n.upgrade_hint}${reset}`);
|
|
852
935
|
|
|
853
936
|
rl.close();
|
|
854
937
|
}
|
|
@@ -988,20 +1071,30 @@ async function runUpgradeToAgentic() {
|
|
|
988
1071
|
}
|
|
989
1072
|
}
|
|
990
1073
|
|
|
991
|
-
// Auto-provision
|
|
1074
|
+
// Auto-provision PROJECT_TOKEN for personal repos
|
|
992
1075
|
let patAutoSet = false;
|
|
993
1076
|
if (projectId && !isOrg) {
|
|
994
1077
|
try {
|
|
995
1078
|
const tokenOut = execFileSync('gh', ['auth', 'token'],
|
|
996
1079
|
{ stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim();
|
|
997
1080
|
if (tokenOut) {
|
|
998
|
-
execFileSync('gh', ['secret', 'set', '
|
|
1081
|
+
execFileSync('gh', ['secret', 'set', 'PROJECT_TOKEN', '--body', tokenOut, '--repo', repo],
|
|
999
1082
|
{ stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1000
1083
|
patAutoSet = true;
|
|
1001
|
-
console.log(`\n${green}✅
|
|
1084
|
+
console.log(`\n${green}✅ PROJECT_TOKEN secret set automatically (uses your gh OAuth token).${reset}`);
|
|
1002
1085
|
}
|
|
1003
1086
|
} catch (_) {
|
|
1004
|
-
console.log(`\n${yellow}⚠️ Could not auto-set
|
|
1087
|
+
console.log(`\n${yellow}⚠️ Could not auto-set PROJECT_TOKEN. Agent will guide manual setup.${reset}`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Set PROJECT_ID as GitHub Actions Variable
|
|
1092
|
+
if (projectId) {
|
|
1093
|
+
try {
|
|
1094
|
+
setActionsVariable(repo, 'PROJECT_ID', projectId);
|
|
1095
|
+
console.log(`${green}${i18n.vars_project_id_ok}${reset}`);
|
|
1096
|
+
} catch (_) {
|
|
1097
|
+
console.log(`${yellow}${i18n.vars_project_id_warn}${projectId}${reset}`);
|
|
1005
1098
|
}
|
|
1006
1099
|
}
|
|
1007
1100
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Archive Board Card on Issue Close Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Archive the GitHub Project board card when an issue is closed, preventing stale cards from accumulating in active columns.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Single new job appended to `project-automation.yml` template. Triggers on the already-present `issues: closed` event. Uses `addProjectV2ItemById` (idempotent) then `archiveProjectV2Item` — handles both issues closed without a PR and issues closed via PR merge safely.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** GitHub Actions, `actions/github-script@v8`, GitHub GraphQL API (`addProjectV2ItemById`, `archiveProjectV2Item`).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Map
|
|
14
|
+
|
|
15
|
+
| Action | Path | Responsibility |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Modify | `templates/.github/workflows/project-automation.yml` | Add `move-card-on-issue-close` job at end of file |
|
|
18
|
+
|
|
19
|
+
No unit tests — YAML workflow template; correctness verified by grep and manual inspection.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
### Task 1: Add `move-card-on-issue-close` job
|
|
24
|
+
|
|
25
|
+
**Files:**
|
|
26
|
+
- Modify: `templates/.github/workflows/project-automation.yml` (append after `cleanup-labels-on-close`)
|
|
27
|
+
|
|
28
|
+
- [ ] **Step 1: Verify the insertion point**
|
|
29
|
+
|
|
30
|
+
Run:
|
|
31
|
+
```bash
|
|
32
|
+
tail -5 templates/.github/workflows/project-automation.yml
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Expected: last line is `console.log(\`Issue #\${issue_number} labels cleaned up\`);` followed by closing braces. The file ends after `cleanup-labels-on-close`.
|
|
36
|
+
|
|
37
|
+
- [ ] **Step 2: Append the new job**
|
|
38
|
+
|
|
39
|
+
At the end of `templates/.github/workflows/project-automation.yml`, add:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
|
|
43
|
+
move-card-on-issue-close:
|
|
44
|
+
name: Closed issue → Archive from board
|
|
45
|
+
if: github.event_name == 'issues' && github.event.action == 'closed'
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
env:
|
|
48
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
49
|
+
steps:
|
|
50
|
+
- name: Archive board card
|
|
51
|
+
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
|
|
52
|
+
uses: actions/github-script@v8
|
|
53
|
+
with:
|
|
54
|
+
github-token: ${{ env.PROJECT_TOKEN }}
|
|
55
|
+
script: |
|
|
56
|
+
const nodeId = context.payload.issue.node_id;
|
|
57
|
+
let itemId;
|
|
58
|
+
try {
|
|
59
|
+
const added = await github.graphql(`
|
|
60
|
+
mutation($p: ID!, $c: ID!) {
|
|
61
|
+
addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
|
|
62
|
+
}`, { p: process.env.PROJECT_ID, c: nodeId });
|
|
63
|
+
itemId = added.addProjectV2ItemById.item.id;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.log(`Could not add issue to project: ${e.message}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await github.graphql(`
|
|
69
|
+
mutation($p: ID!, $i: ID!) {
|
|
70
|
+
archiveProjectV2Item(input: {projectId: $p, itemId: $i}) { item { id } }
|
|
71
|
+
}`, { p: process.env.PROJECT_ID, i: itemId });
|
|
72
|
+
console.log(`Issue #${context.payload.issue.number} archived from board`);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- [ ] **Step 3: Verify job was added and file is syntactically correct**
|
|
76
|
+
|
|
77
|
+
Run:
|
|
78
|
+
```bash
|
|
79
|
+
grep -n "move-card-on-issue-close\|archiveProjectV2Item" templates/.github/workflows/project-automation.yml
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Expected: two matches — the job name line and the mutation name.
|
|
83
|
+
|
|
84
|
+
Run:
|
|
85
|
+
```bash
|
|
86
|
+
python3 -c "import yaml, sys; yaml.safe_load(open('templates/.github/workflows/project-automation.yml'))" 2>&1
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Expected: no output (valid YAML).
|
|
90
|
+
|
|
91
|
+
- [ ] **Step 4: Verify guard condition matches existing pattern**
|
|
92
|
+
|
|
93
|
+
Run:
|
|
94
|
+
```bash
|
|
95
|
+
grep "PROJECT_TOKEN != ''" templates/.github/workflows/project-automation.yml
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Expected: multiple lines including the new job — confirms guard is consistent with other jobs.
|
|
99
|
+
|
|
100
|
+
- [ ] **Step 5: Commit**
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
git add templates/.github/workflows/project-automation.yml
|
|
104
|
+
git commit -m "feat(templates): archive board card when issue is closed"
|
|
105
|
+
```
|