@spec-wave/cli 0.13.0 → 0.15.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/README.md +39 -6
- package/bin/spec-wave.mjs +17 -1
- package/package.json +1 -1
- package/src/api/github-rest.mjs +198 -2
- package/src/commands/code-review.mjs +5 -8
- package/src/commands/decompose.mjs +412 -130
- package/src/commands/dev-agent.mjs +3 -2
- package/src/commands/doctor.mjs +239 -9
- package/src/commands/generate-plan.mjs +105 -30
- package/src/commands/generate-spec.mjs +17 -5
- package/src/commands/implement.mjs +39 -15
- package/src/commands/info.mjs +4 -3
- package/src/commands/issue.mjs +4 -4
- package/src/commands/move.mjs +162 -0
- package/src/commands/order.mjs +1 -12
- package/src/commands/qa.mjs +5 -8
- package/src/commands/refresh.mjs +4 -3
- package/src/commands/story.mjs +1 -12
- package/src/commands/task.mjs +1 -11
- package/src/commands/update.mjs +372 -71
- package/src/commands/validate.mjs +37 -22
- package/src/config.mjs +40 -1
- package/src/lib/board.mjs +88 -26
- package/src/lib/claude.mjs +315 -70
- package/src/lib/critique.mjs +391 -91
- package/src/lib/decomposition-doc.mjs +451 -0
- package/src/lib/implement-board.mjs +14 -1
- package/src/lib/pr-branch.mjs +267 -0
- package/src/lib/project-root.mjs +93 -0
- package/src/lib/templates.mjs +53 -0
- package/src/setup/files.mjs +3 -10
- package/src/templates/skill/SKILL.md +158 -30
- package/src/templates/workflows/code-review.yml +1 -1
- package/src/templates/workflows/decompose.yml +20 -6
- package/src/templates/workflows/generate-plan.yml +1 -1
- package/src/templates/workflows/generate-spec.yml +1 -1
- package/src/templates/workflows/qa.yml +1 -1
- package/src/templates/workflows/validate.yml +1 -1
package/README.md
CHANGED
|
@@ -68,7 +68,8 @@ Ferramenta Node.js que configura e opera o fluxo via linha de comando.
|
|
|
68
68
|
| `generate-spec` | Gera `spec.md` (usado pelo GitHub Action) |
|
|
69
69
|
| `generate-plan` | Gera `plan.md` (usado pelo GitHub Action) |
|
|
70
70
|
| `validate` | Valida spec.md e plan.md (usado pelo GitHub Action) |
|
|
71
|
-
| `decompose` |
|
|
71
|
+
| `decompose` | Gera o rascunho da decomposição em `decomposition.md`; com `--apply`, cria as Stories/Tasks a partir do rascunho revisado (usado pelo GitHub Action) |
|
|
72
|
+
| `move <n> <etapa>` | Move qualquer item do board (Feature, Story, Task, Bug, RFC) para uma Etapa — a Etapa nunca retrocede |
|
|
72
73
|
| `code-review` | Move Feature para Code Review ao abrir PR (usado pelo GitHub Action) |
|
|
73
74
|
| `qa` | Move Feature para QA ao aprovar PR (usado pelo GitHub Action) |
|
|
74
75
|
| `implement` | Aciona o spec-kit localmente para implementar uma Feature (Stories pendentes em ordem de dependência), Story ou Task |
|
|
@@ -81,7 +82,7 @@ Ferramenta Node.js que configura e opera o fluxo via linha de comando.
|
|
|
81
82
|
| `generate-spec.yml` | label `spec-wave:spec` | Gera `docs/features/<slug>/spec.md` via IA |
|
|
82
83
|
| `generate-plan.yml` | label `spec-wave:plan` | Gera `docs/features/<slug>/plan.md` via IA |
|
|
83
84
|
| `validate.yml` | label `spec-wave:ready` | Valida seções obrigatórias; adiciona `spec-wave:plan-approved` |
|
|
84
|
-
| `decompose.yml` |
|
|
85
|
+
| `decompose.yml` | labels `spec-wave:decompose` / `spec-wave:decompose-apply` | 1º grava e critica o rascunho `decomposition.md`; 2º cria Stories e Tasks como sub-issues |
|
|
85
86
|
| `code-review.yml` | PR aberto/reaberto | Move Feature para `👀 Code Review` |
|
|
86
87
|
| `qa.yml` | PR aprovado | Move Feature para `🧪 QA` |
|
|
87
88
|
|
|
@@ -285,20 +286,52 @@ O Action `validate.yml` verifica se todas as seções obrigatórias estão prese
|
|
|
285
286
|
|
|
286
287
|
---
|
|
287
288
|
|
|
288
|
-
### 6. Decompor em Stories e Tasks
|
|
289
|
+
### 6. Decompor em Stories e Tasks (duas etapas)
|
|
290
|
+
|
|
291
|
+
A decomposição não cria issues de uma vez. Primeiro nasce um **rascunho revisável**;
|
|
292
|
+
as issues só são criadas depois que você aprova.
|
|
293
|
+
|
|
294
|
+
**6a. Gerar o rascunho:**
|
|
289
295
|
|
|
290
296
|
```bash
|
|
291
297
|
gh issue edit 12 --add-label "spec-wave:decompose"
|
|
292
298
|
```
|
|
293
299
|
|
|
294
|
-
O Action `
|
|
300
|
+
O Action grava `docs/features/<slug>/decomposition.md` no repositório e submete o
|
|
301
|
+
arquivo à crítica adversarial. **Nenhuma issue é criada.**
|
|
302
|
+
|
|
303
|
+
```markdown
|
|
304
|
+
# Decomposição — [FEATURE] Pagamento com PIX
|
|
305
|
+
<!-- spec-wave:decomposition v1 issue=12 kind=stories -->
|
|
306
|
+
|
|
307
|
+
## Story 1 — selecionar PIX como forma de pagamento
|
|
308
|
+
|
|
309
|
+
**User story:** Como cliente, quero selecionar PIX, para pagar mais rápido
|
|
310
|
+
**Depende de:** —
|
|
311
|
+
|
|
312
|
+
### Task 1.1 — criar endpoint POST /orders/:id/payment/pix
|
|
313
|
+
### Task 1.2 — integrar API do banco via webhook
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Se a crítica encontrar contradições **graves**, ela comenta na issue citando
|
|
317
|
+
`Story N` / `Task N.M`, aplica `spec-wave:critique-failed` e o Action **falha**.
|
|
318
|
+
Você corrige o `decomposition.md` — o artefato que os achados referenciam — e
|
|
319
|
+
reaplica `spec-wave:decompose`: o arquivo é criticado como está, sem ser regerado.
|
|
320
|
+
|
|
321
|
+
**6b. Aplicar o rascunho aprovado:**
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
gh issue edit 12 --add-label "spec-wave:decompose-apply"
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Agora as sub-issues da Feature #12 são criadas a partir do arquivo:
|
|
295
328
|
|
|
296
329
|
```
|
|
297
|
-
#13 [STORY]
|
|
330
|
+
#13 [STORY] selecionar PIX como forma de pagamento
|
|
298
331
|
#14 [TASK] Criar endpoint POST /orders/:id/payment/pix
|
|
299
332
|
#15 [TASK] Integrar API do banco via webhook
|
|
300
333
|
#16 [TASK] Exibir QR Code na tela de checkout
|
|
301
|
-
#17 [STORY]
|
|
334
|
+
#17 [STORY] receber confirmação do pagamento
|
|
302
335
|
#18 [TASK] Webhook de confirmação do banco
|
|
303
336
|
#19 [TASK] Notificação por e-mail ao confirmar
|
|
304
337
|
```
|
package/bin/spec-wave.mjs
CHANGED
|
@@ -93,6 +93,9 @@ program
|
|
|
93
93
|
.option('--skip-skill', 'Não verifica/atualiza a skill instalada')
|
|
94
94
|
.option('--skip-config', 'Não verifica/atualiza o .spec-wave.json local')
|
|
95
95
|
.option('--skip-repo', 'Não verifica/atualiza workflows e labels do repo')
|
|
96
|
+
.option('--branch [nome]', 'Envia os arquivos do repo como Pull Request numa branch, em um único commit (sem valor: spec-wave/update-v<versão>)')
|
|
97
|
+
.option('--config-in-pr', 'Força incluir o .spec-wave.json no Pull Request')
|
|
98
|
+
.option('--no-config-in-pr', 'Força manter o .spec-wave.json fora do Pull Request')
|
|
96
99
|
.option('--dry-run', 'Mostra o que seria atualizado sem alterar nada')
|
|
97
100
|
.option('--yes', 'Aplica sem pedir confirmação')
|
|
98
101
|
.action(async (options) => {
|
|
@@ -157,8 +160,9 @@ program
|
|
|
157
160
|
|
|
158
161
|
program
|
|
159
162
|
.command('decompose')
|
|
160
|
-
.description('
|
|
163
|
+
.description('Gera o rascunho da decomposição em decomposition.md; com --apply, cria as Stories/Tasks a partir do rascunho revisado (usado pelo GitHub Action)')
|
|
161
164
|
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
165
|
+
.option('--apply', 'Aplica o decomposition.md já revisado: cria as issues (sem esta flag, apenas gera/critica o rascunho)')
|
|
162
166
|
.action(async (options) => {
|
|
163
167
|
const { decompose } = await import('../src/commands/decompose.mjs');
|
|
164
168
|
await decompose(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
@@ -212,6 +216,18 @@ program
|
|
|
212
216
|
await task({ action, issue: n }).catch(err => { console.error(err.message); process.exit(1); });
|
|
213
217
|
});
|
|
214
218
|
|
|
219
|
+
program
|
|
220
|
+
.command('move')
|
|
221
|
+
.description('Move qualquer item do board (Feature, Story, Task, Bug, RFC) para uma Etapa — a Etapa nunca retrocede')
|
|
222
|
+
.argument('<n>', 'Número da issue, ex.: 8 ou #8')
|
|
223
|
+
.argument('<etapa>', 'Etapa de destino, com ou sem emoji, ex.: "code review", "Homologação", "🎉 Done"')
|
|
224
|
+
.option('--status <valor>', 'Valor do campo Status no destino: Todo, In Progress ou Done (default: Todo)')
|
|
225
|
+
.action(async (n, etapa, options) => {
|
|
226
|
+
const { move } = await import('../src/commands/move.mjs');
|
|
227
|
+
await move({ issue: n, stage: etapa, ...options })
|
|
228
|
+
.catch(err => { console.error(err.message); process.exit(1); });
|
|
229
|
+
});
|
|
230
|
+
|
|
215
231
|
program
|
|
216
232
|
.command('story')
|
|
217
233
|
.description('Gerencia uma Story no board: review (move para Code Review)')
|
package/package.json
CHANGED
package/src/api/github-rest.mjs
CHANGED
|
@@ -4,6 +4,15 @@ function makeOctokit(token) {
|
|
|
4
4
|
return new Octokit({ auth: token });
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
// Para as consultas em que 404 é um RESULTADO ESPERADO ("a branch ainda não
|
|
8
|
+
// existe", "não há o que comparar") e não um erro: o logger padrão do Octokit
|
|
9
|
+
// imprime a linha `GET ... - 404` no stderr, que no meio de um spinner parece
|
|
10
|
+
// falha. Mesmo padrão do doctor.mjs, onde 404/403 também são respostas válidas.
|
|
11
|
+
const silentLog = { debug() {}, info() {}, warn() {}, error() {} };
|
|
12
|
+
function makeQuietOctokit(token) {
|
|
13
|
+
return new Octokit({ auth: token, log: silentLog });
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
export async function getOwnerNodeId(token, owner) {
|
|
8
17
|
const octokit = makeOctokit(token);
|
|
9
18
|
try {
|
|
@@ -87,6 +96,189 @@ export async function upsertFile(token, owner, repo, path, content, message) {
|
|
|
87
96
|
});
|
|
88
97
|
}
|
|
89
98
|
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Git Data API — commit ATÔMICO + Pull Request
|
|
101
|
+
//
|
|
102
|
+
// O upsertFile acima faz UM COMMIT POR ARQUIVO direto na branch default: oito
|
|
103
|
+
// arquivos = oito commits, e uma falha no quinto deixa o repositório num estado
|
|
104
|
+
// intermediário que ninguém pediu. Aqui a árvore inteira é montada primeiro e só
|
|
105
|
+
// então nascem o commit e a ref — antes da createRef/updateRef nada é
|
|
106
|
+
// ALCANÇÁVEL (os objetos existem no banco, mas nenhuma ref aponta para eles),
|
|
107
|
+
// então uma falha no meio não muda o repositório observável.
|
|
108
|
+
//
|
|
109
|
+
// Três pegadinhas da API, todas documentadas porque custam tempo de depuração:
|
|
110
|
+
// • createTree SEM `base_tree` produz uma árvore com SÓ os arquivos enviados —
|
|
111
|
+
// o commit resultante APAGA todo o resto do repositório, e a API responde 201.
|
|
112
|
+
// • getRef/getCommit/updateRef querem o ref SEM o prefixo `refs/`
|
|
113
|
+
// ('heads/main'); createRef quer COM ('refs/heads/x'). Trocar dá 404/422 sem
|
|
114
|
+
// explicação.
|
|
115
|
+
// • pulls.list exige `head` qualificado com o owner ('owner:branch'): sem o
|
|
116
|
+
// prefixo a API IGNORA o filtro e devolve todos os PRs abertos.
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Lê a ponta de uma branch: sha do commit e sha da ÁRVORE desse commit.
|
|
121
|
+
*
|
|
122
|
+
* A árvore é o que interessa para o `base_tree` e para detectar "nada mudou":
|
|
123
|
+
* git é endereçado por conteúdo, então árvore igual = conteúdo igual, sem
|
|
124
|
+
* precisar pedir diff.
|
|
125
|
+
*
|
|
126
|
+
* @returns {Promise<{commitSha: string, treeSha: string}|null>} null quando a
|
|
127
|
+
* branch não existe (404) — o chamador decide criar vs. atualizar a ref.
|
|
128
|
+
*/
|
|
129
|
+
export async function getBranchHead(token, owner, repo, branch) {
|
|
130
|
+
const octokit = makeQuietOctokit(token); // 404 = branch inexistente, não erro
|
|
131
|
+
let commitSha;
|
|
132
|
+
try {
|
|
133
|
+
const ref = await octokit.rest.git.getRef({ owner, repo, ref: `heads/${branch}` });
|
|
134
|
+
commitSha = ref.data.object.sha;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err.status === 404) return null;
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
const commit = await octokit.rest.git.getCommit({ owner, repo, commit_sha: commitSha });
|
|
140
|
+
return { commitSha, treeSha: commit.data.tree.sha };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Compara `base...branch`.
|
|
145
|
+
*
|
|
146
|
+
* 'behind'/'identical' significa que a branch não tem nenhum commit que a base
|
|
147
|
+
* já não tenha — sinal típico de branch de um PR já mergeado com squash. Serve
|
|
148
|
+
* só para AVISAR antes de empilhar um commit numa branch obsoleta; nunca para
|
|
149
|
+
* decidir um force-push.
|
|
150
|
+
*
|
|
151
|
+
* @returns {Promise<'identical'|'ahead'|'behind'|'diverged'|null>} null se a
|
|
152
|
+
* branch não existe ou a comparação falha (é só um aviso — não pode
|
|
153
|
+
* derrubar o fluxo).
|
|
154
|
+
*/
|
|
155
|
+
export async function compareBranches(token, owner, repo, base, branch) {
|
|
156
|
+
const octokit = makeQuietOctokit(token); // 404 = branch inexistente, não erro
|
|
157
|
+
try {
|
|
158
|
+
const res = await octokit.rest.repos.compareCommitsWithBasehead({
|
|
159
|
+
owner, repo, basehead: `${base}...${branch}`,
|
|
160
|
+
});
|
|
161
|
+
return res.data.status;
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Grava N arquivos em UM único commit numa branch (criando-a a partir de `base`
|
|
169
|
+
* quando ainda não existe).
|
|
170
|
+
*
|
|
171
|
+
* Idempotente de propósito: rodar `update --branch fix/x` duas vezes não gera um
|
|
172
|
+
* segundo commit. Quando a branch já existe o pai é a PONTA DELA — não a base —
|
|
173
|
+
* então a atualização da ref é sempre fast-forward e `force` fica em false (uma
|
|
174
|
+
* corrida com outro push falha em vez de sobrescrever trabalho alheio).
|
|
175
|
+
*
|
|
176
|
+
* Se a árvore montada tiver o MESMO sha da árvore do pai, nada mudou de fato e o
|
|
177
|
+
* commit é pulado: commit vazio num PR só confunde quem revisa.
|
|
178
|
+
*
|
|
179
|
+
* @param {object} opts
|
|
180
|
+
* @param {string} opts.branch branch de trabalho (head do PR)
|
|
181
|
+
* @param {string} opts.base branch de partida quando `branch` ainda não existe
|
|
182
|
+
* @param {Array<{path: string, content: string}>} opts.files conteúdo FINAL de cada arquivo
|
|
183
|
+
* @param {string} opts.message mensagem do commit único
|
|
184
|
+
* @returns {Promise<{branch, base, commitSha, createdBranch, unchanged, baseSha}>}
|
|
185
|
+
*/
|
|
186
|
+
export async function commitFilesToBranch(token, owner, repo, { branch, base, files, message }) {
|
|
187
|
+
const octokit = makeOctokit(token);
|
|
188
|
+
if (!files?.length) throw new Error('commitFilesToBranch: nenhum arquivo a enviar.');
|
|
189
|
+
|
|
190
|
+
const baseHead = await getBranchHead(token, owner, repo, base);
|
|
191
|
+
if (!baseHead) throw new Error(`Branch base "${base}" não encontrada em ${owner}/${repo}.`);
|
|
192
|
+
|
|
193
|
+
const head = await getBranchHead(token, owner, repo, branch);
|
|
194
|
+
const parent = head ?? baseHead;
|
|
195
|
+
|
|
196
|
+
// `content` inline: a própria API cria o blob, poupando um createBlob por
|
|
197
|
+
// arquivo. Vale só para texto UTF-8 — todos os nossos templates são.
|
|
198
|
+
// mode: 100644 = arquivo comum (100755 executável, 040000 subárvore).
|
|
199
|
+
const tree = await octokit.rest.git.createTree({
|
|
200
|
+
owner,
|
|
201
|
+
repo,
|
|
202
|
+
base_tree: parent.treeSha, // SEM isto o commit apagaria o repositório inteiro
|
|
203
|
+
tree: files.map(f => ({ path: f.path, mode: '100644', type: 'blob', content: f.content })),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (tree.data.sha === parent.treeSha) {
|
|
207
|
+
return {
|
|
208
|
+
branch, base, commitSha: parent.commitSha,
|
|
209
|
+
createdBranch: false, unchanged: true, baseSha: baseHead.commitSha,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const commit = await octokit.rest.git.createCommit({
|
|
214
|
+
owner, repo, message, tree: tree.data.sha, parents: [parent.commitSha],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (head) {
|
|
218
|
+
await octokit.rest.git.updateRef({
|
|
219
|
+
owner, repo, ref: `heads/${branch}`, sha: commit.data.sha, force: false,
|
|
220
|
+
});
|
|
221
|
+
} else {
|
|
222
|
+
await octokit.rest.git.createRef({
|
|
223
|
+
owner, repo, ref: `refs/heads/${branch}`, sha: commit.data.sha,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
branch, base, commitSha: commit.data.sha,
|
|
229
|
+
createdBranch: !head, unchanged: false, baseSha: baseHead.commitSha,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* PR ABERTO cuja head é `branch` neste mesmo repositório.
|
|
235
|
+
*
|
|
236
|
+
* @returns {Promise<{number, url, title, base}|null>}
|
|
237
|
+
*/
|
|
238
|
+
export async function findOpenPR(token, owner, repo, branch) {
|
|
239
|
+
const octokit = makeOctokit(token);
|
|
240
|
+
const res = await octokit.rest.pulls.list({
|
|
241
|
+
owner, repo, state: 'open', head: `${owner}:${branch}`, per_page: 1,
|
|
242
|
+
});
|
|
243
|
+
const pr = res.data[0];
|
|
244
|
+
return pr
|
|
245
|
+
? { number: pr.number, url: pr.html_url, title: pr.title, base: pr.base.ref }
|
|
246
|
+
: null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Abre o PR da branch — ou devolve o que já está aberto para ela.
|
|
251
|
+
*
|
|
252
|
+
* Rodar o update duas vezes não pode virar dois PRs. O findOpenPR cobre o caso
|
|
253
|
+
* normal; o catch do 422 cobre a corrida e o "No commits between base and head"
|
|
254
|
+
* (branch sem nada novo em relação à base → não existe PR a abrir → null).
|
|
255
|
+
* Qualquer outro 422 propaga com a mensagem real da API, senão uma base inválida
|
|
256
|
+
* viraria um silencioso "nada a revisar".
|
|
257
|
+
*
|
|
258
|
+
* @returns {Promise<{number, url, created}|null>}
|
|
259
|
+
*/
|
|
260
|
+
export async function ensurePullRequest(
|
|
261
|
+
token, owner, repo, { branch, base, title, body, draft = false }
|
|
262
|
+
) {
|
|
263
|
+
const octokit = makeOctokit(token);
|
|
264
|
+
const existing = await findOpenPR(token, owner, repo, branch);
|
|
265
|
+
if (existing) return { ...existing, created: false };
|
|
266
|
+
try {
|
|
267
|
+
const res = await octokit.rest.pulls.create({
|
|
268
|
+
owner, repo, title, body, head: branch, base, draft,
|
|
269
|
+
});
|
|
270
|
+
return { number: res.data.number, url: res.data.html_url, created: true };
|
|
271
|
+
} catch (err) {
|
|
272
|
+
if (err.status !== 422) throw err;
|
|
273
|
+
const again = await findOpenPR(token, owner, repo, branch);
|
|
274
|
+
if (again) return { ...again, created: false };
|
|
275
|
+
const detail = err.response?.data?.errors?.map(e => e.message).filter(Boolean).join('; ')
|
|
276
|
+
|| err.message;
|
|
277
|
+
if (/No commits between/i.test(detail)) return null;
|
|
278
|
+
throw new Error(detail);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
90
282
|
// `id` é o database id da issue — exigido pela API de dependências
|
|
91
283
|
// (blocked_by), que não aceita number nem node id.
|
|
92
284
|
export async function createIssue(token, owner, repo, title, body, labels) {
|
|
@@ -235,10 +427,14 @@ export async function getPR(token, owner, repo, prNumber) {
|
|
|
235
427
|
return res.data;
|
|
236
428
|
}
|
|
237
429
|
|
|
238
|
-
|
|
430
|
+
// `ref` é opcional (compatível com os chamadores de 4 argumentos): o modo PR do
|
|
431
|
+
// `update` precisa ler o .spec-wave.json na BASE, não no que a API escolher.
|
|
432
|
+
export async function getFileContent(token, owner, repo, path, ref) {
|
|
239
433
|
const octokit = makeOctokit(token);
|
|
240
434
|
try {
|
|
241
|
-
const res = await octokit.rest.repos.getContent({
|
|
435
|
+
const res = await octokit.rest.repos.getContent({
|
|
436
|
+
owner, repo, path, ...(ref ? { ref } : {}),
|
|
437
|
+
});
|
|
242
438
|
return Buffer.from(res.data.content, 'base64').toString('utf-8');
|
|
243
439
|
} catch (err) {
|
|
244
440
|
if (err.status === 404) return null;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
1
|
import { resolveToken } from '../api/auth.mjs';
|
|
4
2
|
import { getIssue, getPR, commentOnIssue } from '../api/github-rest.mjs';
|
|
5
3
|
import { addProjectItem, getIssueParent, listSubIssues, getItemSingleSelectValue } from '../api/github-graphql.mjs';
|
|
6
4
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
7
5
|
import { loadProjectConfig, resolveField, advanceToStage } from '../lib/board.mjs';
|
|
8
|
-
import {
|
|
6
|
+
import { loadConfig } from '../lib/project-root.mjs';
|
|
7
|
+
import { STATUS_OPTIONS, STAGE_ORDER, STAGE_DONE, PROGRESS_TODO, PROGRESS_DONE, isManualStageType } from '../config.mjs';
|
|
9
8
|
|
|
10
9
|
// Ao abrir o PR: Stories/Feature → Etapa "👀 Code Review" (Status "Todo");
|
|
11
10
|
// Tasks → Etapa "🎉 Done" (Status "Done"), pois a implementação da task terminou.
|
|
@@ -118,11 +117,9 @@ export async function codeReview({ prNumber }) {
|
|
|
118
117
|
const token = await resolveToken();
|
|
119
118
|
const projectToken = process.env.PROJECT_TOKEN || token;
|
|
120
119
|
const [envOwner, envRepo] = (process.env.GITHUB_REPOSITORY || '').split('/');
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const owner = envOwner || cfg.owner;
|
|
125
|
-
const repo = envRepo || cfg.repo;
|
|
120
|
+
const { config: cfg } = loadConfig();
|
|
121
|
+
const owner = envOwner || cfg?.owner;
|
|
122
|
+
const repo = envRepo || cfg?.repo;
|
|
126
123
|
|
|
127
124
|
if (!owner || !repo) {
|
|
128
125
|
throw new Error(
|