@spec-wave/cli 0.4.0 → 0.5.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/bin/spec-wave.mjs CHANGED
@@ -136,6 +136,24 @@ program
136
136
  await decompose(options).catch(err => { console.error(err.message); process.exit(1); });
137
137
  });
138
138
 
139
+ program
140
+ .command('code-review')
141
+ .description('Move Feature para Code Review ao abrir um PR (usado pelo GitHub Action)')
142
+ .requiredOption('--pr-number <n>', 'Número do Pull Request')
143
+ .action(async (options) => {
144
+ const { codeReview } = await import('../src/commands/code-review.mjs');
145
+ await codeReview(options).catch(err => { console.error(err.message); process.exit(1); });
146
+ });
147
+
148
+ program
149
+ .command('qa')
150
+ .description('Move Feature para QA ao aprovar um PR (usado pelo GitHub Action)')
151
+ .requiredOption('--pr-number <n>', 'Número do Pull Request')
152
+ .action(async (options) => {
153
+ const { qa } = await import('../src/commands/qa.mjs');
154
+ await qa(options).catch(err => { console.error(err.message); process.exit(1); });
155
+ });
156
+
139
157
  program
140
158
  .command('implement')
141
159
  .description('Aciona o spec-kit implement para uma Story (todas as tasks) ou uma Task')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec-wave/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Setup spec-driven GitHub workflow with Projects v2, labels, issue templates, and AI-powered Actions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -145,6 +145,12 @@ export async function isRepoInitialized(token, owner, repo) {
145
145
  }
146
146
  }
147
147
 
148
+ export async function getPR(token, owner, repo, prNumber) {
149
+ const octokit = makeOctokit(token);
150
+ const res = await octokit.rest.pulls.get({ owner, repo, pull_number: prNumber });
151
+ return res.data;
152
+ }
153
+
148
154
  export async function getFileContent(token, owner, repo, path) {
149
155
  const octokit = makeOctokit(token);
150
156
  try {
@@ -0,0 +1,138 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolveToken } from '../api/auth.mjs';
4
+ import { getIssue, getPR, commentOnIssue } from '../api/github-rest.mjs';
5
+ import { addProjectItem, setItemSingleSelect, getSingleSelectField, getIssueParent } from '../api/github-graphql.mjs';
6
+ import { detectIssueType } from '../lib/issue-type.mjs';
7
+ import { CONFIG_FILE, STATUS_OPTIONS } from '../config.mjs';
8
+
9
+ // Campo "Etapa" (custom) → "👀 Code Review". Campo "Status" (nativo) → "Todo".
10
+ const CODE_REVIEW_STAGE = STATUS_OPTIONS.find(s => s.name.includes('Code Review'))?.name;
11
+ const TODO_STATUS = 'Todo';
12
+
13
+ // Extrai números de issues referenciadas no corpo do PR (Closes #N, Fixes #N, #N solto).
14
+ function extractIssueNumbers(body) {
15
+ if (!body) return [];
16
+ const nums = new Set();
17
+ const re = /(?:closes?|fixes?|resolves?)\s+#(\d+)|(?<![/\w#])#(\d+)/gi;
18
+ for (const m of body.matchAll(re)) {
19
+ const n = parseInt(m[1] || m[2], 10);
20
+ if (n) nums.add(n);
21
+ }
22
+ return [...nums];
23
+ }
24
+
25
+ // Resolve o campo SINGLE_SELECT pelo nome: usa .spec-wave.json, legado ou API.
26
+ async function resolveField(token, project, name) {
27
+ if (project.fields?.[name]) return project.fields[name];
28
+ if (name === 'Etapa' && project.etapaFieldId) {
29
+ return { id: project.etapaFieldId, options: project.stageOptions || {} };
30
+ }
31
+ return await getSingleSelectField(token, project.id, name);
32
+ }
33
+
34
+ // A partir de qualquer issue (Feature/Story/Task), sobe a hierarquia e retorna a Feature.
35
+ async function resolveFeatureIssue(token, owner, repo, issueNumber) {
36
+ let issue;
37
+ try {
38
+ issue = await getIssue(token, owner, repo, issueNumber);
39
+ } catch {
40
+ return null;
41
+ }
42
+ const type = detectIssueType(issue);
43
+ if (type === 'Feature') return issue;
44
+ if (type !== 'Story' && type !== 'Task') return null;
45
+ let currentNodeId = issue.node_id;
46
+ for (let depth = 0; depth < 5; depth++) {
47
+ const parent = await getIssueParent(token, currentNodeId);
48
+ if (!parent) return null;
49
+ if (detectIssueType({ title: parent.title }) === 'Feature') {
50
+ return await getIssue(token, owner, repo, parent.number).catch(() => null);
51
+ }
52
+ currentNodeId = parent.nodeId;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ // Move um item do board para Etapa "👀 Code Review" e Status "Todo".
58
+ async function setCodeReview(token, project, etapaField, statusField, nodeId) {
59
+ const itemId = await addProjectItem(token, project.id, nodeId);
60
+ if (etapaField?.id && CODE_REVIEW_STAGE) {
61
+ const optionId = etapaField.options?.[CODE_REVIEW_STAGE];
62
+ if (optionId) await setItemSingleSelect(token, project.id, itemId, etapaField.id, optionId);
63
+ }
64
+ if (statusField?.id) {
65
+ const optionId = statusField.options?.[TODO_STATUS];
66
+ if (optionId) await setItemSingleSelect(token, project.id, itemId, statusField.id, optionId);
67
+ }
68
+ }
69
+
70
+ export async function codeReview({ prNumber }) {
71
+ const token = await resolveToken();
72
+ const [owner, repo] = (process.env.GITHUB_REPOSITORY || '').split('/');
73
+
74
+ if (!owner || !repo) {
75
+ throw new Error(
76
+ 'GITHUB_REPOSITORY env var não definida.\n' +
77
+ 'Este comando roda no GitHub Actions. Para testar localmente:\n' +
78
+ ` GITHUB_REPOSITORY=owner/repo spec-wave code-review --pr-number ${prNumber}`
79
+ );
80
+ }
81
+
82
+ const pr = await getPR(token, owner, repo, parseInt(prNumber, 10));
83
+ const issueNums = extractIssueNumbers(pr.body || '');
84
+
85
+ if (issueNums.length === 0) {
86
+ console.log('PR sem referências a issues — nenhuma Feature atualizada.');
87
+ return;
88
+ }
89
+
90
+ // Carrega projeto do .spec-wave.json
91
+ const configPath = path.join(process.cwd(), CONFIG_FILE);
92
+ if (!existsSync(configPath)) {
93
+ console.warn(`${CONFIG_FILE} não encontrado — board não atualizado.`);
94
+ return;
95
+ }
96
+ let project;
97
+ try {
98
+ project = JSON.parse(readFileSync(configPath, 'utf-8')).project || {};
99
+ } catch (err) {
100
+ console.warn(`${CONFIG_FILE} corrompido (${err.message}) — board não atualizado.`);
101
+ return;
102
+ }
103
+ if (!project.id) {
104
+ console.warn(`Project não configurado em ${CONFIG_FILE} — board não atualizado.`);
105
+ return;
106
+ }
107
+
108
+ // Resolve campos uma vez, reutiliza em todas as Features.
109
+ const etapaField = await resolveField(token, project, 'Etapa').catch(() => null);
110
+ const statusField = await resolveField(token, project, 'Status').catch(() => null);
111
+
112
+ const seen = new Set();
113
+ const updated = [];
114
+
115
+ for (const num of issueNums) {
116
+ const feature = await resolveFeatureIssue(token, owner, repo, num);
117
+ if (!feature || seen.has(feature.number)) continue;
118
+ seen.add(feature.number);
119
+ try {
120
+ await setCodeReview(token, project, etapaField, statusField, feature.node_id);
121
+ updated.push(`#${feature.number} ${feature.title}`);
122
+ console.log(`Feature #${feature.number} → "${CODE_REVIEW_STAGE}" / Status "${TODO_STATUS}".`);
123
+ } catch (err) {
124
+ console.warn(`Falha ao atualizar Feature #${feature.number}: ${err.message}`);
125
+ }
126
+ }
127
+
128
+ if (updated.length > 0) {
129
+ await commentOnIssue(
130
+ token, owner, repo, parseInt(prNumber, 10),
131
+ `🔍 **Code Review iniciado**\n\n` +
132
+ `Feature(s) movida(s) para **${CODE_REVIEW_STAGE}**:\n\n` +
133
+ updated.map(f => `- ${f}`).join('\n')
134
+ ).catch(() => {});
135
+ }
136
+
137
+ console.log(`code-review: ${updated.length} feature(s) atualizada(s).`);
138
+ }
@@ -1,9 +1,39 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { resolveToken } from '../api/auth.mjs';
3
4
  import { getIssue, createIssue, removeLabel, commentOnIssue } from '../api/github-rest.mjs';
4
- import { addSubIssue } from '../api/github-graphql.mjs';
5
+ import { addSubIssue, addProjectItem, setItemSingleSelect, getSingleSelectField } from '../api/github-graphql.mjs';
5
6
  import { generateDocument } from '../lib/claude.mjs';
6
7
  import { slugify } from '../lib/slugify.mjs';
8
+ import { CONFIG_FILE } from '../config.mjs';
9
+
10
+ const READY_STAGE = 'Todo';
11
+
12
+ // Carrega o projeto do .spec-wave.json. Retorna null se ausente ou sem project.id.
13
+ function loadProject() {
14
+ const configPath = path.join(process.cwd(), CONFIG_FILE);
15
+ if (!existsSync(configPath)) return null;
16
+ try {
17
+ return JSON.parse(readFileSync(configPath, 'utf-8')).project || null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ // Resolve o campo Status do Project: usa .spec-wave.json ou consulta API.
24
+ async function resolveStatusField(token, project) {
25
+ if (project.fields?.Status) return project.fields.Status;
26
+ return await getSingleSelectField(token, project.id, 'Status');
27
+ }
28
+
29
+ // Adiciona issue ao board e move para a etapa informada. Best-effort.
30
+ async function moveToStage(token, project, statusField, nodeId, stageName) {
31
+ if (!project?.id || !statusField) return;
32
+ const optionId = statusField.options?.[stageName];
33
+ if (!statusField.id || !optionId) return;
34
+ const itemId = await addProjectItem(token, project.id, nodeId);
35
+ await setItemSingleSelect(token, project.id, itemId, statusField.id, optionId);
36
+ }
7
37
 
8
38
  const SYSTEM_PROMPT = `Você é um Tech Lead experiente em decomposição de trabalho ágil.
9
39
  A partir da Feature fornecida (com spec.md e plan.md), gere uma lista de Stories e Tasks.
@@ -47,6 +77,17 @@ export async function decompose({ issueNumber }) {
47
77
 
48
78
  const issue = await getIssue(token, owner, repo, parseInt(issueNumber, 10));
49
79
  const slug = slugify(issue.title);
80
+
81
+ // Carrega projeto e resolve campo Status uma vez (reutilizado em todos os itens).
82
+ const project = loadProject();
83
+ let statusField = null;
84
+ if (project?.id && READY_STAGE) {
85
+ try {
86
+ statusField = await resolveStatusField(token, project);
87
+ } catch (err) {
88
+ console.warn(`Não foi possível resolver campo Status do board: ${err.message}`);
89
+ }
90
+ }
50
91
  const featureDir = `docs/features/${slug}`;
51
92
 
52
93
  const planContent = existsSync(`${featureDir}/plan.md`)
@@ -101,6 +142,13 @@ export async function decompose({ issueNumber }) {
101
142
  console.warn(` Story #${createdStory.number} criada, mas falhou ao vincular à Feature: ${err.message}`);
102
143
  }
103
144
 
145
+ // Move story para Ready no board.
146
+ try {
147
+ await moveToStage(token, project, statusField, createdStory.nodeId, READY_STAGE);
148
+ } catch (err) {
149
+ console.warn(` Falha ao mover story #${createdStory.number} para "${READY_STAGE}": ${err.message}`);
150
+ }
151
+
104
152
  for (const task of story.tasks || []) {
105
153
  console.log(` Criando task: ${task.title}`);
106
154
  const taskTitle = `[TASK] ${task.title}`;
@@ -113,9 +161,24 @@ export async function decompose({ issueNumber }) {
113
161
  } catch (err) {
114
162
  console.warn(` Task #${createdTask.number} criada, mas falhou ao vincular à Story: ${err.message}`);
115
163
  }
164
+
165
+ // Move task para Ready no board.
166
+ try {
167
+ await moveToStage(token, project, statusField, createdTask.nodeId, READY_STAGE);
168
+ } catch (err) {
169
+ console.warn(` Falha ao mover task #${createdTask.number} para "${READY_STAGE}": ${err.message}`);
170
+ }
116
171
  }
117
172
  }
118
173
 
174
+ // Move a própria Feature para Ready no board.
175
+ try {
176
+ await moveToStage(token, project, statusField, featureNodeId, READY_STAGE);
177
+ if (project?.id && statusField) console.log(`Feature movida para "${READY_STAGE}" no board.`);
178
+ } catch (err) {
179
+ console.warn(`Falha ao mover Feature para "${READY_STAGE}": ${err.message}`);
180
+ }
181
+
119
182
  // Remove trigger label
120
183
  await removeLabel(token, owner, repo, parseInt(issueNumber, 10), 'spec-wave:decompose');
121
184
 
@@ -0,0 +1,131 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolveToken } from '../api/auth.mjs';
4
+ import { getIssue, getPR, commentOnIssue } from '../api/github-rest.mjs';
5
+ import { addProjectItem, setItemSingleSelect, getSingleSelectField, getIssueParent } from '../api/github-graphql.mjs';
6
+ import { detectIssueType } from '../lib/issue-type.mjs';
7
+ import { CONFIG_FILE, STATUS_OPTIONS } from '../config.mjs';
8
+
9
+ const QA_STAGE = STATUS_OPTIONS.find(s => s.name.includes('QA'))?.name;
10
+ const TODO_STATUS = 'Todo';
11
+
12
+ function extractIssueNumbers(body) {
13
+ if (!body) return [];
14
+ const nums = new Set();
15
+ const re = /(?:closes?|fixes?|resolves?)\s+#(\d+)|(?<![/\w#])#(\d+)/gi;
16
+ for (const m of body.matchAll(re)) {
17
+ const n = parseInt(m[1] || m[2], 10);
18
+ if (n) nums.add(n);
19
+ }
20
+ return [...nums];
21
+ }
22
+
23
+ async function resolveField(token, project, name) {
24
+ if (project.fields?.[name]) return project.fields[name];
25
+ if (name === 'Etapa' && project.etapaFieldId) {
26
+ return { id: project.etapaFieldId, options: project.stageOptions || {} };
27
+ }
28
+ return await getSingleSelectField(token, project.id, name);
29
+ }
30
+
31
+ async function resolveFeatureIssue(token, owner, repo, issueNumber) {
32
+ let issue;
33
+ try {
34
+ issue = await getIssue(token, owner, repo, issueNumber);
35
+ } catch {
36
+ return null;
37
+ }
38
+ const type = detectIssueType(issue);
39
+ if (type === 'Feature') return issue;
40
+ if (type !== 'Story' && type !== 'Task') return null;
41
+ let currentNodeId = issue.node_id;
42
+ for (let depth = 0; depth < 5; depth++) {
43
+ const parent = await getIssueParent(token, currentNodeId);
44
+ if (!parent) return null;
45
+ if (detectIssueType({ title: parent.title }) === 'Feature') {
46
+ return await getIssue(token, owner, repo, parent.number).catch(() => null);
47
+ }
48
+ currentNodeId = parent.nodeId;
49
+ }
50
+ return null;
51
+ }
52
+
53
+ async function setQA(token, project, etapaField, statusField, nodeId) {
54
+ const itemId = await addProjectItem(token, project.id, nodeId);
55
+ if (etapaField?.id && QA_STAGE) {
56
+ const optionId = etapaField.options?.[QA_STAGE];
57
+ if (optionId) await setItemSingleSelect(token, project.id, itemId, etapaField.id, optionId);
58
+ }
59
+ if (statusField?.id) {
60
+ const optionId = statusField.options?.[TODO_STATUS];
61
+ if (optionId) await setItemSingleSelect(token, project.id, itemId, statusField.id, optionId);
62
+ }
63
+ }
64
+
65
+ export async function qa({ prNumber }) {
66
+ const token = await resolveToken();
67
+ const [owner, repo] = (process.env.GITHUB_REPOSITORY || '').split('/');
68
+
69
+ if (!owner || !repo) {
70
+ throw new Error(
71
+ 'GITHUB_REPOSITORY env var não definida.\n' +
72
+ 'Este comando roda no GitHub Actions. Para testar localmente:\n' +
73
+ ` GITHUB_REPOSITORY=owner/repo spec-wave qa --pr-number ${prNumber}`
74
+ );
75
+ }
76
+
77
+ const pr = await getPR(token, owner, repo, parseInt(prNumber, 10));
78
+ const issueNums = extractIssueNumbers(pr.body || '');
79
+
80
+ if (issueNums.length === 0) {
81
+ console.log('PR sem referências a issues — nenhuma Feature atualizada.');
82
+ return;
83
+ }
84
+
85
+ const configPath = path.join(process.cwd(), CONFIG_FILE);
86
+ if (!existsSync(configPath)) {
87
+ console.warn(`${CONFIG_FILE} não encontrado — board não atualizado.`);
88
+ return;
89
+ }
90
+ let project;
91
+ try {
92
+ project = JSON.parse(readFileSync(configPath, 'utf-8')).project || {};
93
+ } catch (err) {
94
+ console.warn(`${CONFIG_FILE} corrompido (${err.message}) — board não atualizado.`);
95
+ return;
96
+ }
97
+ if (!project.id) {
98
+ console.warn(`Project não configurado em ${CONFIG_FILE} — board não atualizado.`);
99
+ return;
100
+ }
101
+
102
+ const etapaField = await resolveField(token, project, 'Etapa').catch(() => null);
103
+ const statusField = await resolveField(token, project, 'Status').catch(() => null);
104
+
105
+ const seen = new Set();
106
+ const updated = [];
107
+
108
+ for (const num of issueNums) {
109
+ const feature = await resolveFeatureIssue(token, owner, repo, num);
110
+ if (!feature || seen.has(feature.number)) continue;
111
+ seen.add(feature.number);
112
+ try {
113
+ await setQA(token, project, etapaField, statusField, feature.node_id);
114
+ updated.push(`#${feature.number} ${feature.title}`);
115
+ console.log(`Feature #${feature.number} → "${QA_STAGE}" / Status "${TODO_STATUS}".`);
116
+ } catch (err) {
117
+ console.warn(`Falha ao atualizar Feature #${feature.number}: ${err.message}`);
118
+ }
119
+ }
120
+
121
+ if (updated.length > 0) {
122
+ await commentOnIssue(
123
+ token, owner, repo, parseInt(prNumber, 10),
124
+ `🧪 **PR aprovado — QA iniciado**\n\n` +
125
+ `Feature(s) movida(s) para **${QA_STAGE}**:\n\n` +
126
+ updated.map(f => `- ${f}`).join('\n')
127
+ ).catch(() => {});
128
+ }
129
+
130
+ console.log(`qa: ${updated.length} feature(s) atualizada(s).`);
131
+ }
@@ -1,8 +1,54 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { resolveToken } from '../api/auth.mjs';
3
4
  import { getIssue, removeLabel, addLabel, commentOnIssue } from '../api/github-rest.mjs';
5
+ import { addProjectItem, setItemSingleSelect, getSingleSelectField } from '../api/github-graphql.mjs';
4
6
  import { slugify } from '../lib/slugify.mjs';
5
- import { REQUIRED_PLAN_SECTIONS, REQUIRED_SPEC_SECTIONS } from '../config.mjs';
7
+ import { CONFIG_FILE, REQUIRED_PLAN_SECTIONS, REQUIRED_SPEC_SECTIONS } from '../config.mjs';
8
+
9
+ // Opção nativa do campo Status do GitHub Projects (Todo / In Progress / Done).
10
+ const DONE_STAGE = 'Done';
11
+
12
+ // Resolve um campo SINGLE_SELECT pelo nome: usa o .spec-wave.json, cai para o
13
+ // formato legado (etapaFieldId/stageOptions) e, por fim, consulta o Project.
14
+ async function resolveField(token, project, name) {
15
+ if (project.fields && project.fields[name]) return project.fields[name];
16
+ if (name === 'Etapa' && project.etapaFieldId) {
17
+ return { id: project.etapaFieldId, options: project.stageOptions || {} };
18
+ }
19
+ return await getSingleSelectField(token, project.id, name);
20
+ }
21
+
22
+ // Move o item da issue para a Etapa "🎉 Done" no board. Best-effort: loga e
23
+ // segue se o .spec-wave.json não tiver o Project ou o campo não for encontrado.
24
+ async function moveToDone(token, issue) {
25
+ const configPath = path.join(process.cwd(), CONFIG_FILE);
26
+ if (!existsSync(configPath)) {
27
+ console.warn(`${CONFIG_FILE} não encontrado — status do board não atualizado.`);
28
+ return;
29
+ }
30
+ let project;
31
+ try {
32
+ project = (JSON.parse(readFileSync(configPath, 'utf-8')).project) || {};
33
+ } catch (err) {
34
+ console.warn(`${CONFIG_FILE} corrompido (${err.message}) — status do board não atualizado.`);
35
+ return;
36
+ }
37
+ if (!project.id) {
38
+ console.warn(`Project não configurado no ${CONFIG_FILE} — status do board não atualizado.`);
39
+ return;
40
+ }
41
+ // addProjectItem é idempotente: retorna o item existente se a issue já está no board.
42
+ const itemId = await addProjectItem(token, project.id, issue.node_id);
43
+ const field = await resolveField(token, project, 'Status');
44
+ const optionId = field?.options?.[DONE_STAGE];
45
+ if (field?.id && optionId) {
46
+ await setItemSingleSelect(token, project.id, itemId, field.id, optionId);
47
+ console.log(`Status do board atualizado para "${DONE_STAGE}".`);
48
+ } else {
49
+ console.warn(`Etapa "${DONE_STAGE}" não encontrada no Project — status do board não atualizado.`);
50
+ }
51
+ }
6
52
 
7
53
  export async function validate({ issueNumber }) {
8
54
  const token = await resolveToken();
@@ -64,12 +110,22 @@ export async function validate({ issueNumber }) {
64
110
  process.exit(1);
65
111
  }
66
112
 
113
+ // Validação passou: move a issue para "🎉 Done" no board (best-effort).
114
+ let doneOk = false;
115
+ try {
116
+ await moveToDone(token, issue);
117
+ doneOk = !!DONE_STAGE;
118
+ } catch (err) {
119
+ console.warn(`Falha ao atualizar status do board: ${err.message}`);
120
+ }
121
+
67
122
  await commentOnIssue(
68
123
  token, owner, repo, parseInt(issueNumber, 10),
69
124
  `✅ **Validação concluída com sucesso!**\n\n` +
70
125
  `- [\`${specPath}\`](${specPath}) ✓\n` +
71
126
  `- [\`${planPath}\`](${planPath}) ✓\n\n` +
72
- `A Feature está pronta para decomposição. Mova o card para **📋 Backlog Técnico** ou use:\n` +
127
+ (doneOk ? `Status movido para **${DONE_STAGE}**. ` : '') +
128
+ `A Feature está pronta para decomposição. Use:\n` +
73
129
  `\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:decompose"\n\`\`\``
74
130
  );
75
131
 
package/src/config.mjs CHANGED
@@ -142,6 +142,8 @@ export const WORKFLOW_FILES = [
142
142
  'generate-spec.yml',
143
143
  'validate.yml',
144
144
  'decompose.yml',
145
+ 'code-review.yml',
146
+ 'qa.yml',
145
147
  ];
146
148
 
147
149
  export const ISSUE_TEMPLATE_FILES = [
@@ -0,0 +1,26 @@
1
+ name: Code Review
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, reopened]
6
+
7
+ jobs:
8
+ code-review:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ issues: write
12
+ pull-requests: write
13
+ contents: read
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '20'
21
+
22
+ - name: Move Feature to Code Review
23
+ run: npx @spec-wave/cli code-review --pr-number ${{ github.event.pull_request.number }}
24
+ env:
25
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26
+ GITHUB_REPOSITORY: ${{ github.repository }}
@@ -0,0 +1,27 @@
1
+ name: QA
2
+
3
+ on:
4
+ pull_request_review:
5
+ types: [submitted]
6
+
7
+ jobs:
8
+ qa:
9
+ if: github.event.review.state == 'approved'
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ issues: write
13
+ pull-requests: write
14
+ contents: read
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-node@v4
20
+ with:
21
+ node-version: '20'
22
+
23
+ - name: Move Feature to QA
24
+ run: npx @spec-wave/cli qa --pr-number ${{ github.event.pull_request.number }}
25
+ env:
26
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27
+ GITHUB_REPOSITORY: ${{ github.repository }}