@precisa-saude/cli 1.0.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/LICENSE +201 -0
- package/README.md +131 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +640 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +630 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/README.md +51 -0
- package/dist/templates/dotfiles/editorconfig +12 -0
- package/dist/templates/dotfiles/gitignore +10 -0
- package/dist/templates/dotfiles/npmrc +1 -0
- package/dist/templates/dotfiles/nvmrc +1 -0
- package/dist/templates/dotfiles/prettierignore +7 -0
- package/dist/templates/dotfiles/prettierrc +1 -0
- package/dist/templates/dotfiles/renovate.json +29 -0
- package/dist/templates/github/ISSUE_TEMPLATE/bug.md +33 -0
- package/dist/templates/github/ISSUE_TEMPLATE/config.yml +8 -0
- package/dist/templates/github/ISSUE_TEMPLATE/feature.md +18 -0
- package/dist/templates/github/PULL_REQUEST_TEMPLATE.md +18 -0
- package/dist/templates/github/workflows/ci.yml +44 -0
- package/dist/templates/github/workflows/release.yml +62 -0
- package/dist/templates/github/workflows/review.yml +435 -0
- package/dist/templates/governance/CITATION.cff +8 -0
- package/dist/templates/governance/CODE_OF_CONDUCT.md +43 -0
- package/dist/templates/governance/CONTRIBUTING.md +32 -0
- package/dist/templates/governance/CONVENTIONS.md +37 -0
- package/dist/templates/governance/SECURITY.md +22 -0
- package/dist/templates/governance/SUPPORT.md +14 -0
- package/dist/templates/husky/commit-msg +8 -0
- package/dist/templates/husky/pre-commit +1 -0
- package/dist/templates/husky/pre-push +21 -0
- package/dist/templates/templates.manifest.yml +125 -0
- package/package.json +62 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
name: Code Review
|
|
2
|
+
|
|
3
|
+
# Tries providers in order (Anthropic → OpenAI) and uses the first one that
|
|
4
|
+
# returns a 200. Any non-200 from a provider triggers fallback to the next.
|
|
5
|
+
# If all configured providers fail (or none are configured), the workflow
|
|
6
|
+
# exits without posting a review and without emitting a round marker, so
|
|
7
|
+
# the next PR activity retries fresh.
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
pull_request:
|
|
11
|
+
types: [opened, synchronize, reopened]
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: review-${{ github.event.pull_request.number }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
check-review-status:
|
|
19
|
+
name: Check review status
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
timeout-minutes: 2
|
|
22
|
+
permissions:
|
|
23
|
+
pull-requests: read
|
|
24
|
+
issues: read
|
|
25
|
+
outputs:
|
|
26
|
+
should_run: ${{ steps.check.outputs.should_run }}
|
|
27
|
+
review_round: ${{ steps.check.outputs.review_round }}
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
|
|
31
|
+
- name: Decide whether to run
|
|
32
|
+
id: check
|
|
33
|
+
uses: actions/github-script@v8
|
|
34
|
+
with:
|
|
35
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
36
|
+
script: |
|
|
37
|
+
const prNumber = context.issue.number;
|
|
38
|
+
|
|
39
|
+
// Skip docs/config-only PRs
|
|
40
|
+
const { data: files } = await github.rest.pulls.listFiles({
|
|
41
|
+
owner: context.repo.owner,
|
|
42
|
+
repo: context.repo.repo,
|
|
43
|
+
pull_number: prNumber,
|
|
44
|
+
per_page: 100,
|
|
45
|
+
});
|
|
46
|
+
const ignore = [/\.md$/, /^docs\//, /^\.github\//, /\.json$/, /\.ya?ml$/, /\.cff$/];
|
|
47
|
+
const hasCode = files.some(f => !ignore.some(p => p.test(f.filename)));
|
|
48
|
+
if (!hasCode) {
|
|
49
|
+
core.setOutput('should_run', 'false');
|
|
50
|
+
core.setOutput('review_round', 'skip');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { data: comments } = await github.rest.issues.listComments({
|
|
55
|
+
owner: context.repo.owner,
|
|
56
|
+
repo: context.repo.repo,
|
|
57
|
+
issue_number: prNumber,
|
|
58
|
+
per_page: 100,
|
|
59
|
+
});
|
|
60
|
+
const round1 = comments.some(c => c.body?.includes('<!-- AUTOMATED_REVIEW_ROUND_1 -->'));
|
|
61
|
+
const round2 = comments.some(c => c.body?.includes('<!-- AUTOMATED_REVIEW_ROUND_2_COMPLETE -->'));
|
|
62
|
+
|
|
63
|
+
if (round2) {
|
|
64
|
+
core.setOutput('should_run', 'false');
|
|
65
|
+
core.setOutput('review_round', 'complete');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
core.setOutput('should_run', 'true');
|
|
69
|
+
core.setOutput('review_round', round1 ? '2' : '1');
|
|
70
|
+
|
|
71
|
+
review:
|
|
72
|
+
name: Automated review
|
|
73
|
+
needs: check-review-status
|
|
74
|
+
if: needs.check-review-status.outputs.should_run == 'true'
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
timeout-minutes: 10
|
|
77
|
+
continue-on-error: true
|
|
78
|
+
permissions:
|
|
79
|
+
contents: read
|
|
80
|
+
pull-requests: write
|
|
81
|
+
env:
|
|
82
|
+
ANTHROPIC_MODEL: claude-haiku-4-5-20251001
|
|
83
|
+
OPENAI_MODEL: gpt-4o-mini
|
|
84
|
+
MAX_DIFF_SIZE: '150000'
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
with:
|
|
88
|
+
fetch-depth: 0
|
|
89
|
+
|
|
90
|
+
- name: Build review input
|
|
91
|
+
id: pr
|
|
92
|
+
env:
|
|
93
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
94
|
+
REVIEW_ROUND: ${{ needs.check-review-status.outputs.review_round }}
|
|
95
|
+
run: |
|
|
96
|
+
PR_NUMBER=${{ github.event.pull_request.number }}
|
|
97
|
+
HEAD_SHA=$(gh pr view "$PR_NUMBER" --json headRefOid -q .headRefOid)
|
|
98
|
+
echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT
|
|
99
|
+
echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT
|
|
100
|
+
|
|
101
|
+
if [ "$REVIEW_ROUND" = "2" ]; then
|
|
102
|
+
FIRST=$(gh pr view "$PR_NUMBER" --json commits -q '.commits[0].oid')
|
|
103
|
+
git diff "$FIRST"..HEAD > pr.diff
|
|
104
|
+
else
|
|
105
|
+
gh pr diff "$PR_NUMBER" > pr.diff
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
gh pr view "$PR_NUMBER" --json files -q '.files[].path' > changed_files.txt
|
|
109
|
+
|
|
110
|
+
DIFF_SIZE=$(wc -c < pr.diff)
|
|
111
|
+
if [ "$DIFF_SIZE" -gt "$MAX_DIFF_SIZE" ]; then
|
|
112
|
+
head -c "$MAX_DIFF_SIZE" pr.diff > pr.diff.tmp && mv pr.diff.tmp pr.diff
|
|
113
|
+
echo "diff_truncated=true" >> $GITHUB_OUTPUT
|
|
114
|
+
else
|
|
115
|
+
echo "diff_truncated=false" >> $GITHUB_OUTPUT
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
- name: Call review API (with provider failover)
|
|
119
|
+
id: call
|
|
120
|
+
env:
|
|
121
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
122
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
123
|
+
DIFF_TRUNCATED: ${{ steps.pr.outputs.diff_truncated }}
|
|
124
|
+
REVIEW_ROUND: ${{ needs.check-review-status.outputs.review_round }}
|
|
125
|
+
run: |
|
|
126
|
+
node << 'SCRIPT'
|
|
127
|
+
const fs = require('fs');
|
|
128
|
+
|
|
129
|
+
const diff = fs.readFileSync('pr.diff', 'utf8');
|
|
130
|
+
const files = fs.readFileSync('changed_files.txt', 'utf8').trim();
|
|
131
|
+
const round = process.env.REVIEW_ROUND;
|
|
132
|
+
const truncated = process.env.DIFF_TRUNCATED === 'true';
|
|
133
|
+
|
|
134
|
+
const system = [
|
|
135
|
+
'You are a senior software engineer performing a code review.',
|
|
136
|
+
'Analyze the PR diff carefully and provide actionable, specific feedback.',
|
|
137
|
+
'Focus on bugs, security issues, performance problems, and significant code-quality concerns.',
|
|
138
|
+
'Do not include nitpicks, style preferences, or suggestions to add comments.',
|
|
139
|
+
].join(' ');
|
|
140
|
+
|
|
141
|
+
const user = [
|
|
142
|
+
`Review Round: ${round}`,
|
|
143
|
+
'',
|
|
144
|
+
'## Files changed in this PR',
|
|
145
|
+
'These are the ONLY files modified. Do NOT reference any other files in feedback:',
|
|
146
|
+
files,
|
|
147
|
+
'',
|
|
148
|
+
truncated ? '> **Note**: Diff truncated due to size. Review only what is shown below.\n' : '',
|
|
149
|
+
'## Diff',
|
|
150
|
+
'',
|
|
151
|
+
'```diff',
|
|
152
|
+
diff,
|
|
153
|
+
'```',
|
|
154
|
+
'',
|
|
155
|
+
'Respond with ONLY a valid JSON object (no markdown fences). Use this exact structure:',
|
|
156
|
+
'{"summary": "...", "changes": ["..."], "feedback": [{"file": "path", "line": 42, "comment": "..."}]}',
|
|
157
|
+
'',
|
|
158
|
+
'Rules:',
|
|
159
|
+
'- The "file" field MUST be one of the files listed above.',
|
|
160
|
+
'- The "line" field MUST reference a line number from the NEW version (right side of diff, + lines). Cross-check against @@ hunk headers.',
|
|
161
|
+
'- Include feedback only for real issues; skip nits.',
|
|
162
|
+
'- Do NOT hallucinate code not visible in the diff.',
|
|
163
|
+
'- If no issues, return an empty feedback array.',
|
|
164
|
+
].join('\n');
|
|
165
|
+
|
|
166
|
+
// Per-1M-token USD pricing. Update when providers change rates.
|
|
167
|
+
// Missing entries silently skip cost display (tokens still shown).
|
|
168
|
+
const PRICING = {
|
|
169
|
+
'claude-haiku-4-5-20251001': { in: 1.00, out: 5.00 },
|
|
170
|
+
'claude-sonnet-4-6': { in: 3.00, out: 15.00 },
|
|
171
|
+
'claude-opus-4-7': { in: 15.00, out: 75.00 },
|
|
172
|
+
'gpt-4o-mini': { in: 0.15, out: 0.60 },
|
|
173
|
+
'gpt-4o': { in: 2.50, out: 10.00 },
|
|
174
|
+
'gpt-5-mini': { in: 0.25, out: 2.00 },
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
function computeCost(model, promptTokens, completionTokens) {
|
|
178
|
+
const rate = PRICING[model];
|
|
179
|
+
if (!rate) return null;
|
|
180
|
+
return (promptTokens / 1_000_000) * rate.in + (completionTokens / 1_000_000) * rate.out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const providers = {
|
|
184
|
+
anthropic: {
|
|
185
|
+
label: 'Anthropic',
|
|
186
|
+
envKey: 'ANTHROPIC_API_KEY',
|
|
187
|
+
modelEnv: 'ANTHROPIC_MODEL',
|
|
188
|
+
defaultModel: 'claude-haiku-4-5-20251001',
|
|
189
|
+
build: (model, key) => ({
|
|
190
|
+
url: 'https://api.anthropic.com/v1/messages',
|
|
191
|
+
headers: {
|
|
192
|
+
'content-type': 'application/json',
|
|
193
|
+
'x-api-key': key,
|
|
194
|
+
'anthropic-version': '2023-06-01',
|
|
195
|
+
},
|
|
196
|
+
body: JSON.stringify({
|
|
197
|
+
model,
|
|
198
|
+
max_tokens: 8192,
|
|
199
|
+
system,
|
|
200
|
+
messages: [{ role: 'user', content: user }],
|
|
201
|
+
}),
|
|
202
|
+
}),
|
|
203
|
+
extractText: (r) => r?.content?.[0]?.text ?? '',
|
|
204
|
+
extractUsage: (r) => ({
|
|
205
|
+
prompt_tokens: r?.usage?.input_tokens ?? 0,
|
|
206
|
+
completion_tokens: r?.usage?.output_tokens ?? 0,
|
|
207
|
+
}),
|
|
208
|
+
extractError: (r) => r?.error?.message ?? r?.message ?? 'see workflow logs',
|
|
209
|
+
},
|
|
210
|
+
openai: {
|
|
211
|
+
label: 'OpenAI',
|
|
212
|
+
envKey: 'OPENAI_API_KEY',
|
|
213
|
+
modelEnv: 'OPENAI_MODEL',
|
|
214
|
+
defaultModel: 'gpt-4o-mini',
|
|
215
|
+
build: (model, key) => ({
|
|
216
|
+
url: 'https://api.openai.com/v1/chat/completions',
|
|
217
|
+
headers: {
|
|
218
|
+
'content-type': 'application/json',
|
|
219
|
+
Authorization: `Bearer ${key}`,
|
|
220
|
+
},
|
|
221
|
+
body: JSON.stringify({
|
|
222
|
+
model,
|
|
223
|
+
max_tokens: 8192,
|
|
224
|
+
messages: [
|
|
225
|
+
{ role: 'system', content: system },
|
|
226
|
+
{ role: 'user', content: user },
|
|
227
|
+
],
|
|
228
|
+
response_format: { type: 'json_object' },
|
|
229
|
+
}),
|
|
230
|
+
}),
|
|
231
|
+
extractText: (r) => r?.choices?.[0]?.message?.content ?? '',
|
|
232
|
+
extractUsage: (r) => ({
|
|
233
|
+
prompt_tokens: r?.usage?.prompt_tokens ?? 0,
|
|
234
|
+
completion_tokens: r?.usage?.completion_tokens ?? 0,
|
|
235
|
+
}),
|
|
236
|
+
extractError: (r) => r?.error?.message ?? 'see workflow logs',
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Provider order: try Anthropic, fall back to OpenAI.
|
|
241
|
+
const order = ['anthropic', 'openai'];
|
|
242
|
+
|
|
243
|
+
const setOutput = (k, v) => {
|
|
244
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${k}<<EOF\n${String(v)}\nEOF\n`);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
async function tryProvider(name) {
|
|
248
|
+
const p = providers[name];
|
|
249
|
+
const key = process.env[p.envKey];
|
|
250
|
+
if (!key) return { ok: false, reason: 'no key configured', skip: true };
|
|
251
|
+
|
|
252
|
+
const model = process.env[p.modelEnv] || p.defaultModel;
|
|
253
|
+
const req = p.build(model, key);
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const resp = await fetch(req.url, {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: req.headers,
|
|
259
|
+
body: req.body,
|
|
260
|
+
});
|
|
261
|
+
const text = await resp.text();
|
|
262
|
+
let parsed;
|
|
263
|
+
try {
|
|
264
|
+
parsed = JSON.parse(text);
|
|
265
|
+
} catch {
|
|
266
|
+
return { ok: false, reason: `HTTP ${resp.status}: non-JSON response`, provider: name, model };
|
|
267
|
+
}
|
|
268
|
+
if (!resp.ok) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
reason: `HTTP ${resp.status}: ${p.extractError(parsed)}`,
|
|
272
|
+
provider: name,
|
|
273
|
+
model,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const reviewText = p.extractText(parsed);
|
|
277
|
+
const usage = p.extractUsage(parsed);
|
|
278
|
+
return { ok: true, provider: name, model, reviewText, usage };
|
|
279
|
+
} catch (e) {
|
|
280
|
+
return { ok: false, reason: `network error: ${e.message}`, provider: name, model };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
(async () => {
|
|
285
|
+
const attempts = [];
|
|
286
|
+
let winner = null;
|
|
287
|
+
|
|
288
|
+
for (const name of order) {
|
|
289
|
+
const attempt = await tryProvider(name);
|
|
290
|
+
attempts.push(attempt);
|
|
291
|
+
if (attempt.ok) {
|
|
292
|
+
winner = attempt;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
if (attempt.skip) continue;
|
|
296
|
+
console.log(`::warning::${providers[name].label} failed: ${attempt.reason}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!winner) {
|
|
300
|
+
const configured = attempts.filter((a) => !a.skip);
|
|
301
|
+
if (configured.length === 0) {
|
|
302
|
+
console.log('::warning::No review providers configured. Skipping.');
|
|
303
|
+
setOutput('status', 'skipped');
|
|
304
|
+
setOutput('error_reason', 'no providers configured');
|
|
305
|
+
} else {
|
|
306
|
+
const reasons = configured
|
|
307
|
+
.map((a) => `${providers[a.provider].label}: ${a.reason}`)
|
|
308
|
+
.join('; ');
|
|
309
|
+
console.log(`::warning::All providers failed. ${reasons}`);
|
|
310
|
+
setOutput('status', 'error');
|
|
311
|
+
setOutput('error_reason', reasons);
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
fs.writeFileSync('review.json', winner.reviewText);
|
|
317
|
+
setOutput('status', 'ok');
|
|
318
|
+
setOutput('provider_label', providers[winner.provider].label);
|
|
319
|
+
setOutput('provider_model', winner.model);
|
|
320
|
+
|
|
321
|
+
const promptTokens = winner.usage.prompt_tokens;
|
|
322
|
+
const completionTokens = winner.usage.completion_tokens;
|
|
323
|
+
const cost = computeCost(winner.model, promptTokens, completionTokens);
|
|
324
|
+
setOutput('prompt_tokens', String(promptTokens));
|
|
325
|
+
setOutput('completion_tokens', String(completionTokens));
|
|
326
|
+
setOutput('cost_usd', cost == null ? '' : cost.toFixed(4));
|
|
327
|
+
|
|
328
|
+
// Emit fallback details to the workflow log (not the PR comment).
|
|
329
|
+
const failed = attempts.filter((a) => !a.ok && !a.skip);
|
|
330
|
+
const fellBack = failed.length > 0;
|
|
331
|
+
setOutput('fell_back', fellBack ? 'true' : 'false');
|
|
332
|
+
if (fellBack) {
|
|
333
|
+
const notes = failed
|
|
334
|
+
.map((a) => `${providers[a.provider].label} failed: ${a.reason}`)
|
|
335
|
+
.join('; ');
|
|
336
|
+
console.log(`::notice::Fell back to ${providers[winner.provider].label}. ${notes}`);
|
|
337
|
+
}
|
|
338
|
+
})();
|
|
339
|
+
SCRIPT
|
|
340
|
+
|
|
341
|
+
- name: Post review
|
|
342
|
+
uses: actions/github-script@v8
|
|
343
|
+
with:
|
|
344
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
345
|
+
script: |
|
|
346
|
+
const fs = require('fs');
|
|
347
|
+
const prNumber = ${{ steps.pr.outputs.number }};
|
|
348
|
+
const headSha = '${{ steps.pr.outputs.head_sha }}';
|
|
349
|
+
const round = '${{ needs.check-review-status.outputs.review_round }}';
|
|
350
|
+
const status = `${{ steps.call.outputs.status }}`;
|
|
351
|
+
const errorReason = `${{ steps.call.outputs.error_reason }}`;
|
|
352
|
+
const providerLabel = `${{ steps.call.outputs.provider_label }}`;
|
|
353
|
+
const providerModel = `${{ steps.call.outputs.provider_model }}`;
|
|
354
|
+
const promptTokens = Number(`${{ steps.call.outputs.prompt_tokens }}` || '0');
|
|
355
|
+
const completionTokens = Number(`${{ steps.call.outputs.completion_tokens }}` || '0');
|
|
356
|
+
const costUsd = `${{ steps.call.outputs.cost_usd }}`;
|
|
357
|
+
const fellBack = `${{ steps.call.outputs.fell_back }}` === 'true';
|
|
358
|
+
|
|
359
|
+
const fmt = (n) => n.toLocaleString('en-US');
|
|
360
|
+
const costPart = costUsd ? ` | $${costUsd}` : '';
|
|
361
|
+
const usagePart = promptTokens + completionTokens > 0
|
|
362
|
+
? ` | ${fmt(promptTokens)} in / ${fmt(completionTokens)} out${costPart}`
|
|
363
|
+
: '';
|
|
364
|
+
const fallbackBadge = fellBack ? ' (fallback)' : '';
|
|
365
|
+
|
|
366
|
+
// Failure paths — no round marker, so the next push retries fresh.
|
|
367
|
+
if (status !== 'ok') {
|
|
368
|
+
const note = status === 'skipped'
|
|
369
|
+
? `Review skipped — ${errorReason}. No round marker emitted; the next PR activity will retry as Round 1.`
|
|
370
|
+
: `Review failed — ${errorReason}. No round marker emitted; the next PR activity will retry as Round 1.`;
|
|
371
|
+
await github.rest.issues.createComment({
|
|
372
|
+
owner: context.repo.owner,
|
|
373
|
+
repo: context.repo.repo,
|
|
374
|
+
issue_number: prNumber,
|
|
375
|
+
body: `### Automated Review — not posted\n\n${note}`,
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const raw = fs.readFileSync('review.json', 'utf8').trim();
|
|
381
|
+
let review;
|
|
382
|
+
try {
|
|
383
|
+
let s = raw;
|
|
384
|
+
if (s.startsWith('```')) s = s.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
|
|
385
|
+
review = JSON.parse(s);
|
|
386
|
+
} catch (e) {
|
|
387
|
+
await github.rest.issues.createComment({
|
|
388
|
+
owner: context.repo.owner,
|
|
389
|
+
repo: context.repo.repo,
|
|
390
|
+
issue_number: prNumber,
|
|
391
|
+
body: `## Automated Review (Round ${round})\n\n${raw}\n\n---\n*Reviewed by ${providerLabel} ${providerModel}${fallbackBadge}${usagePart}*`,
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const summary = review.summary || 'No summary provided';
|
|
397
|
+
const changes = (review.changes || []).map(c => `- ${c}`).join('\n') || '_none_';
|
|
398
|
+
const feedback = review.feedback || [];
|
|
399
|
+
|
|
400
|
+
const isRound1 = round === '1';
|
|
401
|
+
const marker = isRound1 ? '<!-- AUTOMATED_REVIEW_ROUND_1 -->' : '<!-- AUTOMATED_REVIEW_ROUND_2_COMPLETE -->';
|
|
402
|
+
const label = isRound1 ? 'Round 1' : 'Round 2 (Final)';
|
|
403
|
+
|
|
404
|
+
let body = `## Automated Review — ${label}\n\n${marker}\n\n`;
|
|
405
|
+
body += `### Summary\n${summary}\n\n`;
|
|
406
|
+
body += `### Changes\n${changes}\n\n---\n`;
|
|
407
|
+
body += feedback.length === 0
|
|
408
|
+
? `:white_check_mark: No issues found\n\n`
|
|
409
|
+
: `:mag: Found ${feedback.length} suggestion${feedback.length === 1 ? '' : 's'} (see inline comments)\n\n`;
|
|
410
|
+
body += `*Reviewed by ${providerLabel} ${providerModel}${fallbackBadge}${usagePart}`;
|
|
411
|
+
body += isRound1 ? ` — Round 1 of 2*` : ` — Round 2 (Final); no further reviews will be performed*`;
|
|
412
|
+
|
|
413
|
+
await github.rest.issues.createComment({
|
|
414
|
+
owner: context.repo.owner,
|
|
415
|
+
repo: context.repo.repo,
|
|
416
|
+
issue_number: prNumber,
|
|
417
|
+
body,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
for (const item of feedback) {
|
|
421
|
+
try {
|
|
422
|
+
await github.rest.pulls.createReviewComment({
|
|
423
|
+
owner: context.repo.owner,
|
|
424
|
+
repo: context.repo.repo,
|
|
425
|
+
pull_number: prNumber,
|
|
426
|
+
body: item.comment,
|
|
427
|
+
path: item.file,
|
|
428
|
+
line: item.line,
|
|
429
|
+
commit_id: headSha,
|
|
430
|
+
side: 'RIGHT',
|
|
431
|
+
});
|
|
432
|
+
} catch (e) {
|
|
433
|
+
console.log(`Failed to post comment on ${item.file}:${item.line} — ${e.message}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Código de Conduta — Contributor Covenant
|
|
2
|
+
|
|
3
|
+
## Nosso compromisso
|
|
4
|
+
|
|
5
|
+
Nós, como membros, contribuidores e líderes, nos comprometemos a tornar a participação em nossa comunidade uma experiência livre de assédio para todos, independentemente de idade, tamanho corporal, deficiência visível ou invisível, etnia, características sexuais, identidade e expressão de gênero, nível de experiência, educação, status socioeconômico, nacionalidade, aparência pessoal, raça, religião ou identidade e orientação sexual.
|
|
6
|
+
|
|
7
|
+
Comprometemo-nos a agir e interagir de maneiras que contribuam para uma comunidade aberta, acolhedora, diversa, inclusiva e saudável.
|
|
8
|
+
|
|
9
|
+
## Nossos padrões
|
|
10
|
+
|
|
11
|
+
Exemplos de comportamento que contribuem para um ambiente positivo:
|
|
12
|
+
|
|
13
|
+
- Demonstrar empatia e gentileza com outras pessoas
|
|
14
|
+
- Respeitar opiniões, pontos de vista e experiências diferentes
|
|
15
|
+
- Dar e aceitar graciosamente feedback construtivo
|
|
16
|
+
- Assumir responsabilidade e pedir desculpas aos afetados por nossos erros, aprendendo com a experiência
|
|
17
|
+
- Focar no que é melhor não apenas para nós individualmente, mas para a comunidade como um todo
|
|
18
|
+
|
|
19
|
+
Exemplos de comportamento inaceitável:
|
|
20
|
+
|
|
21
|
+
- Uso de linguagem ou imagens sexualizadas e atenção ou avanços sexuais de qualquer tipo
|
|
22
|
+
- Trolling, comentários insultuosos ou depreciativos, e ataques pessoais ou políticos
|
|
23
|
+
- Assédio público ou privado
|
|
24
|
+
- Publicar informações privadas de outros, como endereço físico ou de e-mail, sem permissão explícita
|
|
25
|
+
- Outra conduta que poderia razoavelmente ser considerada inapropriada em um ambiente profissional
|
|
26
|
+
|
|
27
|
+
## Responsabilidades de aplicação
|
|
28
|
+
|
|
29
|
+
Os líderes da comunidade são responsáveis por esclarecer e fazer cumprir nossos padrões de comportamento aceitável e tomarão ação corretiva apropriada e justa em resposta a qualquer comportamento que considerem inapropriado, ameaçador, ofensivo ou prejudicial.
|
|
30
|
+
|
|
31
|
+
## Escopo
|
|
32
|
+
|
|
33
|
+
Este Código de Conduta aplica-se em todos os espaços da comunidade e também quando um indivíduo está representando oficialmente a comunidade em espaços públicos.
|
|
34
|
+
|
|
35
|
+
## Aplicação
|
|
36
|
+
|
|
37
|
+
Instâncias de comportamento abusivo, de assédio ou de outra forma inaceitável podem ser reportadas aos líderes da comunidade responsáveis pela aplicação em **{{CONDUCT_EMAIL}}**.
|
|
38
|
+
|
|
39
|
+
Todas as reclamações serão revisadas e investigadas de forma rápida e justa.
|
|
40
|
+
|
|
41
|
+
## Atribuição
|
|
42
|
+
|
|
43
|
+
Este Código de Conduta é adaptado do [Contributor Covenant](https://www.contributor-covenant.org), versão 2.1.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for considering a contribution to `{{REPO_NAME}}`.
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install
|
|
9
|
+
pnpm -r build
|
|
10
|
+
pnpm -r typecheck
|
|
11
|
+
pnpm -r test
|
|
12
|
+
pnpm -r lint
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Node {{NODE_VERSION}}+ and pnpm {{PNPM_VERSION}}+ are required (see `.nvmrc` and the `packageManager` field).
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
1. Fork (or branch) from `main`
|
|
20
|
+
2. Open a PR — direct pushes to `main` are not allowed
|
|
21
|
+
3. CI must be green
|
|
22
|
+
4. A reviewer approves and merges via GitHub
|
|
23
|
+
|
|
24
|
+
Commits follow [Conventional Commits](https://www.conventionalcommits.org). Allowed scopes for this repo are configured in `.commitlintrc.cjs`.
|
|
25
|
+
|
|
26
|
+
## Dependency changes
|
|
27
|
+
|
|
28
|
+
Ask before adding runtime or peer dependencies. In the PR description explain: what the package does, why it's needed, and whether an existing dependency could serve the purpose.
|
|
29
|
+
|
|
30
|
+
## Signing
|
|
31
|
+
|
|
32
|
+
All commits should be GPG-signed. The `commit-msg` hook blocks AI-attribution footers (`Co-Authored-By: Claude` and similar).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Conventions
|
|
2
|
+
|
|
3
|
+
## Commit format
|
|
4
|
+
|
|
5
|
+
[Conventional Commits](https://www.conventionalcommits.org):
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
type(scope): short description
|
|
9
|
+
|
|
10
|
+
optional longer body
|
|
11
|
+
|
|
12
|
+
BREAKING CHANGE: notes (optional)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `build`, `ci`, `revert`
|
|
16
|
+
|
|
17
|
+
**Scopes**: declared in `.commitlintrc.cjs`.
|
|
18
|
+
|
|
19
|
+
**AI attribution**: Do not include `Co-Authored-By: Claude`, `Generated with Claude`, or similar lines. The `commit-msg` hook enforces this.
|
|
20
|
+
|
|
21
|
+
## Code style
|
|
22
|
+
|
|
23
|
+
- TypeScript strict mode, `noUncheckedIndexedAccess`
|
|
24
|
+
- ESM modules
|
|
25
|
+
- Prettier-enforced formatting (100 char width, single quotes, semicolons, trailing commas — via `@precisa-saude/prettier-config`)
|
|
26
|
+
- ESLint rules via `@precisa-saude/eslint-config`
|
|
27
|
+
- Imports sorted by `eslint-plugin-simple-import-sort`
|
|
28
|
+
- Interfaces, objects, and JSX props sorted by `eslint-plugin-perfectionist` where applicable
|
|
29
|
+
|
|
30
|
+
## Tests
|
|
31
|
+
|
|
32
|
+
Vitest with an 80% coverage floor (configured per-repo in `vitest.config.ts`).
|
|
33
|
+
|
|
34
|
+
## Dependency rules
|
|
35
|
+
|
|
36
|
+
- Core/SDK packages avoid runtime dependencies when feasible
|
|
37
|
+
- External dependencies require explicit approval in the PR
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
If you discover a security vulnerability in this project, please report it responsibly.
|
|
6
|
+
|
|
7
|
+
**Do not open a public GitHub issue.**
|
|
8
|
+
|
|
9
|
+
Email: **{{SECURITY_EMAIL}}**
|
|
10
|
+
|
|
11
|
+
Include:
|
|
12
|
+
|
|
13
|
+
- Description of the vulnerability
|
|
14
|
+
- Steps to reproduce
|
|
15
|
+
- Potential impact
|
|
16
|
+
- Suggested fix (if any)
|
|
17
|
+
|
|
18
|
+
We will acknowledge receipt within 48 hours and provide a timeline for resolution.
|
|
19
|
+
|
|
20
|
+
## Disclosure
|
|
21
|
+
|
|
22
|
+
Disclosure is coordinated privately. Fixes are released in the affected package(s) and noted in the CHANGELOG.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Support
|
|
2
|
+
|
|
3
|
+
## Getting help
|
|
4
|
+
|
|
5
|
+
- **Report bugs**: [GitHub Issues](https://github.com/{{REPO_SLUG}}/issues)
|
|
6
|
+
- **Request features**: [GitHub Issues](https://github.com/{{REPO_SLUG}}/issues)
|
|
7
|
+
- **Questions and discussions**: [GitHub Discussions](https://github.com/{{REPO_SLUG}}/discussions)
|
|
8
|
+
|
|
9
|
+
## Before opening an issue
|
|
10
|
+
|
|
11
|
+
1. Search existing issues to avoid duplicates
|
|
12
|
+
2. Include a minimal reproducible example
|
|
13
|
+
3. Specify the package and version
|
|
14
|
+
4. Include Node/pnpm versions and the OS you're on
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pnpm exec commitlint --edit "$1"
|
|
2
|
+
|
|
3
|
+
# Block AI attribution lines in commit messages
|
|
4
|
+
if grep -q -i -E "co-authored-by:.*claude|generated with.*claude|co-authored-by:.*anthropic|co-authored-by:.*gpt|co-authored-by:.*copilot" "$1"; then
|
|
5
|
+
echo "❌ Commit message contains AI attribution. Remove these lines:"
|
|
6
|
+
grep -i -E "co-authored-by:.*claude|generated with.*claude|co-authored-by:.*anthropic|co-authored-by:.*gpt|co-authored-by:.*copilot" "$1"
|
|
7
|
+
exit 1
|
|
8
|
+
fi
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pnpm exec lint-staged
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Full-checks hook. Runs the same gates as CI so a failing push is caught locally.
|
|
2
|
+
# Per-repo coverage thresholds live in vitest config, not here.
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
echo "→ verifying lockfile is in sync"
|
|
7
|
+
pnpm install --frozen-lockfile --prefer-offline
|
|
8
|
+
|
|
9
|
+
echo "→ prettier"
|
|
10
|
+
pnpm format:check
|
|
11
|
+
|
|
12
|
+
echo "→ eslint"
|
|
13
|
+
pnpm -r lint
|
|
14
|
+
|
|
15
|
+
echo "→ typescript"
|
|
16
|
+
pnpm -r typecheck
|
|
17
|
+
|
|
18
|
+
echo "→ tests"
|
|
19
|
+
pnpm -r test
|
|
20
|
+
|
|
21
|
+
echo "✓ pre-push checks passed"
|