@precisa-saude/cli 1.0.4 → 1.1.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.
Files changed (44) hide show
  1. package/dist/bin.js +41 -12
  2. package/dist/bin.js.map +1 -1
  3. package/dist/index.d.ts +8 -1
  4. package/dist/index.js +28 -9
  5. package/dist/index.js.map +1 -1
  6. package/dist/templates/.claude/agents/debugger.md +130 -0
  7. package/dist/templates/.claude/agents/docs-sync.md +97 -0
  8. package/dist/templates/.claude/agents/pr-review-responder.md +117 -0
  9. package/dist/templates/.claude/agents/pre-commit-check.md +85 -0
  10. package/dist/templates/.claude/agents/refactor-scout.md +139 -0
  11. package/dist/templates/AGENTS.md +45 -0
  12. package/dist/templates/CLAUDE.md +9 -0
  13. package/dist/templates/commitlintrc.cjs +10 -0
  14. package/dist/templates/eslint.config.js +19 -0
  15. package/dist/templates/github/ISSUE_TEMPLATE/bug_report.md +33 -0
  16. package/dist/templates/github/ISSUE_TEMPLATE/config.yml +4 -4
  17. package/dist/templates/github/ISSUE_TEMPLATE/feature_request.md +18 -0
  18. package/dist/templates/github/PULL_REQUEST_TEMPLATE.md +10 -10
  19. package/dist/templates/github/workflows/_checks.yml +102 -0
  20. package/dist/templates/github/workflows/_deploy-site.yml +168 -0
  21. package/dist/templates/github/workflows/_publish.yml +77 -0
  22. package/dist/templates/github/workflows/_release.yml +156 -0
  23. package/dist/templates/github/workflows/ci.yml +63 -27
  24. package/dist/templates/github/workflows/doctor.yml +71 -0
  25. package/dist/templates/github/workflows/publish-tag.yml +71 -0
  26. package/dist/templates/github/workflows/release.yml +22 -8
  27. package/dist/templates/github/workflows/review.yml +75 -23
  28. package/dist/templates/governance/CITATION.cff +1 -1
  29. package/dist/templates/governance/CONTRIBUTING.md +14 -14
  30. package/dist/templates/governance/CONVENTIONS.md +21 -19
  31. package/dist/templates/governance/SECURITY.md +13 -13
  32. package/dist/templates/governance/SUPPORT.md +10 -10
  33. package/dist/templates/husky/commit-msg +4 -2
  34. package/dist/templates/husky/pre-commit +10 -0
  35. package/dist/templates/husky/pre-push +24 -10
  36. package/dist/templates/package.json +71 -0
  37. package/dist/templates/pnpm-workspace.yaml +3 -0
  38. package/dist/templates/scripts/worktree.sh +5 -0
  39. package/dist/templates/templates.manifest.yml +127 -7
  40. package/dist/templates/tsconfig.json +4 -0
  41. package/dist/templates/turbo.json +32 -0
  42. package/package.json +2 -2
  43. package/dist/templates/github/ISSUE_TEMPLATE/bug.md +0 -33
  44. package/dist/templates/github/ISSUE_TEMPLATE/feature.md +0 -18
@@ -0,0 +1,156 @@
1
+ name: Release
2
+
3
+ # Reusable workflow — semantic-release with packages_changed guard.
4
+ # Produces outputs so downstream jobs (publish, deploy-site) can gate
5
+ # on whether a release happened and which commit carries the bump.
6
+ #
7
+ # Guards release on `packages/**` or root `package.json` changing in
8
+ # the pushed range. Site-only or docs-only pushes skip the release
9
+ # cycle. Set `require_package_changes: false` to release regardless.
10
+
11
+ on:
12
+ workflow_call:
13
+ inputs:
14
+ require_package_changes:
15
+ description: 'Only run semantic-release if packages/** or package.json changed'
16
+ type: boolean
17
+ default: true
18
+ outputs:
19
+ released:
20
+ description: 'Whether a new version was released'
21
+ value: ${{ jobs.release.outputs.released }}
22
+ version:
23
+ description: 'The released version number (without leading v)'
24
+ value: ${{ jobs.release.outputs.version }}
25
+ release_sha:
26
+ description: 'The commit SHA that was released (chore(release) commit, or github.sha on no-op)'
27
+ value: ${{ jobs.release.outputs.release_sha }}
28
+
29
+ jobs:
30
+ release:
31
+ name: semantic-release
32
+ runs-on: ubuntu-latest
33
+ permissions:
34
+ contents: write
35
+ issues: write
36
+ pull-requests: write
37
+ outputs:
38
+ released: ${{ steps.semantic.outputs.released }}
39
+ version: ${{ steps.semantic.outputs.version }}
40
+ # Fallback to github.sha on no-op — consumers without an `if: released == 'true'`
41
+ # guard won't checkout an empty ref.
42
+ release_sha: ${{ steps.semantic.outputs.release_sha || github.sha }}
43
+ steps:
44
+ - name: Check for package changes across pushed commits
45
+ id: changes
46
+ if: inputs.require_package_changes
47
+ uses: actions/github-script@v7
48
+ with:
49
+ # Compares `before..after` from the push payload. Paginates explicitly
50
+ # because `compareCommits` truncates at 300 files per page — a large
51
+ # push could otherwise silently miss detection.
52
+ script: |
53
+ const before = context.payload.before;
54
+ const after = context.sha;
55
+ const isInitial = !before || /^0+$/.test(before);
56
+ let filenames = [];
57
+ if (isInitial) {
58
+ const { data: commit } = await github.rest.repos.getCommit({
59
+ owner: context.repo.owner, repo: context.repo.repo, ref: after,
60
+ });
61
+ filenames = (commit.files ?? []).map(f => f.filename);
62
+ } else {
63
+ const pages = await github.paginate(
64
+ github.rest.repos.compareCommitsWithBasehead,
65
+ {
66
+ owner: context.repo.owner,
67
+ repo: context.repo.repo,
68
+ basehead: `${before}...${after}`,
69
+ per_page: 100,
70
+ },
71
+ );
72
+ filenames = pages.flatMap(p => (p.files ?? []).map(f => f.filename));
73
+ }
74
+ const packageChanged = filenames.some(
75
+ f => f.startsWith('packages/') || f === 'package.json'
76
+ );
77
+ core.setOutput('packages_changed', packageChanged ? 'true' : 'false');
78
+ core.info(`inspected ${filenames.length} files, packages_changed=${packageChanged}`);
79
+
80
+ - name: Generate GitHub App token
81
+ id: app-token
82
+ if: "!inputs.require_package_changes || steps.changes.outputs.packages_changed == 'true'"
83
+ uses: actions/create-github-app-token@v1
84
+ with:
85
+ app-id: ${{ secrets.RELEASE_APP_ID }}
86
+ private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
87
+
88
+ - uses: actions/checkout@v6
89
+ if: "!inputs.require_package_changes || steps.changes.outputs.packages_changed == 'true'"
90
+ with:
91
+ fetch-depth: 0
92
+ token: ${{ steps.app-token.outputs.token }}
93
+
94
+ - uses: pnpm/action-setup@v4
95
+ if: "!inputs.require_package_changes || steps.changes.outputs.packages_changed == 'true'"
96
+
97
+ - uses: actions/setup-node@v6
98
+ if: "!inputs.require_package_changes || steps.changes.outputs.packages_changed == 'true'"
99
+ with:
100
+ node-version: 22
101
+ cache: 'pnpm'
102
+ registry-url: 'https://registry.npmjs.org'
103
+
104
+ - name: Install dependencies
105
+ if: "!inputs.require_package_changes || steps.changes.outputs.packages_changed == 'true'"
106
+ run: pnpm install --frozen-lockfile
107
+
108
+ - name: Build
109
+ if: "!inputs.require_package_changes || steps.changes.outputs.packages_changed == 'true'"
110
+ run: pnpm turbo run build
111
+
112
+ - name: Run semantic-release
113
+ id: semantic
114
+ if: "!inputs.require_package_changes || steps.changes.outputs.packages_changed == 'true'"
115
+ env:
116
+ GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
117
+ HUSKY: 0
118
+ # Three cases:
119
+ # (a) success with release: stdout contains 'Published release' →
120
+ # we take the version from `git describe --tags` (more robust
121
+ # to format changes in semantic-release's stdout).
122
+ # (b) no-op: exit 0 AND stdout mentions 'no relevant changes' →
123
+ # released=false. Requiring BOTH conditions avoids masking
124
+ # silent failures where the tool exits 0 without publishing
125
+ # or explaining why.
126
+ # (c) real error: any other combination → non-zero exit.
127
+ run: |
128
+ set +e
129
+ OUTPUT=$(npx semantic-release 2>&1)
130
+ EXIT=$?
131
+ set -e
132
+ echo "$OUTPUT"
133
+
134
+ if echo "$OUTPUT" | grep -q "Published release"; then
135
+ RAW_TAG=$(git describe --tags --abbrev=0)
136
+ VERSION="${RAW_TAG#v}"
137
+ if [ -z "$VERSION" ]; then
138
+ echo "::error::semantic-release published but git describe found no tag"
139
+ exit 1
140
+ fi
141
+ RELEASE_SHA=$(git rev-parse HEAD)
142
+ {
143
+ echo "released=true"
144
+ echo "version=$VERSION"
145
+ echo "release_sha=$RELEASE_SHA"
146
+ } >> "$GITHUB_OUTPUT"
147
+ elif [ "$EXIT" -eq 0 ] && echo "$OUTPUT" | grep -qE "no relevant changes|There are no relevant changes"; then
148
+ echo "released=false" >> "$GITHUB_OUTPUT"
149
+ else
150
+ echo "::error::semantic-release exited $EXIT with no recognized release or no-op marker"
151
+ exit "${EXIT:-1}"
152
+ fi
153
+
154
+ - name: Skip release (no package changes)
155
+ if: inputs.require_package_changes && steps.changes.outputs.packages_changed != 'true'
156
+ run: echo "No changes under packages/** — release skipped"
@@ -1,4 +1,20 @@
1
- name: CI
1
+ name: CI/CD
2
+
3
+ # Canonical CI/CD orchestrator — mirrors platform's split-workflow shape.
4
+ # Each concern lives in its own reusable workflow under `.github/workflows/`:
5
+ #
6
+ # _checks.yml — parallel CI checks (lint, typecheck, format, build, test)
7
+ # _release.yml — semantic-release with packages_changed guard
8
+ # _publish.yml — npm publish pinned to release_sha
9
+ # _deploy-site.yml — Cloudflare Pages deploy + Slack notify (repos with sites)
10
+ # review.yml — automated Anthropic → OpenAI code review
11
+ # publish-tag.yml — manual recovery (workflow_dispatch)
12
+ #
13
+ # This orchestrator wires the pieces together. Each repo adapts:
14
+ # - Which reusable workflows to call (comment out what doesn't apply)
15
+ # - The publish `packages` list
16
+ # - The site `project_name` and `site_filter`
17
+ # - Extra repo-specific workflows (e.g. fhir's publish-ig.yml)
2
18
 
3
19
  on:
4
20
  push:
@@ -7,38 +23,58 @@ on:
7
23
  branches: [main]
8
24
  workflow_dispatch:
9
25
 
26
+ permissions:
27
+ contents: read
28
+ issues: write
29
+ pull-requests: write
30
+ deployments: write
31
+ id-token: write
32
+
10
33
  concurrency:
11
34
  group: ${{ github.workflow }}-${{ github.ref }}
12
35
  cancel-in-progress: true
13
36
 
14
37
  jobs:
15
- build-and-test:
16
- name: build + typecheck + lint + test
17
- runs-on: ubuntu-latest
18
- steps:
19
- - uses: actions/checkout@v4
20
-
21
- - uses: pnpm/action-setup@v4
22
-
23
- - uses: actions/setup-node@v4
24
- with:
25
- node-version: '{{NODE_VERSION}}'
26
- cache: pnpm
27
-
28
- - name: Install dependencies
29
- run: pnpm install --frozen-lockfile
30
-
31
- - name: Build
32
- run: pnpm -r build
38
+ # ── CI checks (every PR and push) ──────────────────────────────
39
+ checks:
40
+ uses: ./.github/workflows/_checks.yml
41
+ secrets: inherit
33
42
 
34
- - name: Typecheck
35
- run: pnpm -r typecheck
43
+ # ── Automated code review (PR only, after checks) ──────────────
44
+ review:
45
+ needs: checks
46
+ if: github.event_name == 'pull_request'
47
+ uses: ./.github/workflows/review.yml
48
+ with:
49
+ pr_number: ${{ github.event.pull_request.number }}
50
+ secrets: inherit
36
51
 
37
- - name: Lint
38
- run: pnpm -r lint
52
+ # ── Release (main only, guarded on package changes) ────────────
53
+ release:
54
+ needs: checks
55
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
56
+ uses: ./.github/workflows/_release.yml
57
+ secrets: inherit
39
58
 
40
- - name: Format check
41
- run: pnpm format:check
59
+ # ── Publish to npm (when release created a new version) ────────
60
+ publish:
61
+ needs: release
62
+ if: needs.release.outputs.released == 'true'
63
+ uses: ./.github/workflows/_publish.yml
64
+ with:
65
+ release_sha: ${{ needs.release.outputs.release_sha }}
66
+ # Newline-separated list of package directories. One per line.
67
+ packages: |
68
+ packages/core
69
+ packages/calculators
70
+ secrets: inherit
42
71
 
43
- - name: Test
44
- run: pnpm -r test
72
+ # ── Deploy site (repos with a site — remove if not applicable) ──
73
+ deploy-site:
74
+ needs: checks
75
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
76
+ uses: ./.github/workflows/_deploy-site.yml
77
+ with:
78
+ project_name: '{{PROJECT_NAME}}'
79
+ site_filter: '@{{SITE_FILTER}}'
80
+ secrets: inherit
@@ -0,0 +1,71 @@
1
+ name: Template drift audit
2
+
3
+ # Monthly audit — runs `precisa doctor` to detect drift between this
4
+ # repo's templated files (workflows, dotfiles, configs) and the current
5
+ # `@precisa-saude/cli` templates. When drift shows up, open a PR running
6
+ # `pnpm exec precisa sync` to absorb the changes.
7
+ #
8
+ # Runs on the 1st of each month at 06:00 America/Sao_Paulo (09:00 UTC).
9
+ # `workflow_dispatch` is wired so you can trigger it ad-hoc.
10
+ #
11
+ # This workflow catches the class of incident that motivated it:
12
+ # silent drift between canonical workflow templates and the real files
13
+ # in each repo. See fhir-brasil#26 (PAT_TOKEN → GitHub App) for the
14
+ # concrete failure mode — drift went undetected until a conditional
15
+ # job finally fired in production.
16
+
17
+ on:
18
+ schedule:
19
+ - cron: '0 9 1 * *'
20
+ workflow_dispatch:
21
+
22
+ permissions:
23
+ contents: read
24
+ issues: write
25
+
26
+ jobs:
27
+ doctor:
28
+ name: precisa doctor
29
+ runs-on: ubuntu-latest
30
+ timeout-minutes: 10
31
+ steps:
32
+ - uses: actions/checkout@v6
33
+ - uses: pnpm/action-setup@v4
34
+ - uses: actions/setup-node@v6
35
+ with:
36
+ node-version: 22
37
+ cache: 'pnpm'
38
+ - run: pnpm install --frozen-lockfile
39
+ - name: Audit templates
40
+ id: doctor
41
+ run: pnpm exec precisa doctor
42
+ continue-on-error: true
43
+ - name: Open issue on drift
44
+ if: steps.doctor.outcome == 'failure'
45
+ uses: actions/github-script@v7
46
+ with:
47
+ script: |
48
+ const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
49
+ const body = [
50
+ '`precisa doctor` detected drift between this repo and the canonical templates.',
51
+ '',
52
+ `Full log: ${runUrl}`,
53
+ '',
54
+ 'To fix:',
55
+ '',
56
+ '```bash',
57
+ 'pnpm exec precisa sync --dry-run # review the diff',
58
+ 'pnpm exec precisa sync # apply',
59
+ '```',
60
+ '',
61
+ 'Drift typically shows up when `@precisa-saude/cli` publishes',
62
+ 'new templates (workflow changes, dotfile updates) and this',
63
+ 'repo has not run `precisa sync` since.',
64
+ ].join('\n');
65
+ await github.rest.issues.create({
66
+ owner: context.repo.owner,
67
+ repo: context.repo.repo,
68
+ title: 'precisa doctor: template drift detected',
69
+ body,
70
+ labels: ['dependencies', 'tooling-drift'],
71
+ });
@@ -0,0 +1,71 @@
1
+ name: Publish tag to npm
2
+
3
+ # Recuperação manual: publica os pacotes de uma tag git específica no npm.
4
+ # Usado quando semantic-release criou a tag + commit chore(release) mas o
5
+ # step de publish foi interrompido (cancel de concorrência, npm fora do ar,
6
+ # rotação de token mid-release, etc.). Idempotente — só publica se a versão
7
+ # não estiver no registry.
8
+ #
9
+ # Uso: gh workflow run publish-tag.yml -f tag=v1.1.0
10
+
11
+ on:
12
+ workflow_dispatch:
13
+ inputs:
14
+ tag:
15
+ description: 'Tag Git para publicar (ex.: v1.1.0)'
16
+ required: true
17
+ type: string
18
+
19
+ permissions:
20
+ contents: read
21
+ # `id-token: write` is for Sigstore attestations (`--provenance`),
22
+ # not for npm auth — see _publish.yml for the full rationale.
23
+ id-token: write
24
+
25
+ jobs:
26
+ publish:
27
+ name: Publish tag
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v6
31
+ with:
32
+ ref: ${{ inputs.tag }}
33
+
34
+ - uses: pnpm/action-setup@v4
35
+
36
+ - uses: actions/setup-node@v6
37
+ with:
38
+ node-version: 22
39
+ cache: 'pnpm'
40
+ registry-url: 'https://registry.npmjs.org'
41
+
42
+ - name: Install dependencies
43
+ run: pnpm install --frozen-lockfile
44
+
45
+ - name: Build
46
+ run: pnpm turbo run build
47
+
48
+ - name: Publish packages (idempotent)
49
+ # Auto-discovers publishable packages via pnpm's workspace filter
50
+ # (`./packages/**`, skipping those with `private: true`). No
51
+ # per-repo list to keep in sync; adding a new package means
52
+ # adding it to `packages/` and the next tag picks it up.
53
+ env:
54
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
55
+ run: |
56
+ set -e
57
+ pnpm -r --filter "./packages/**" --if-present exec sh -c '
58
+ pkg_name=$(node -p "require(\"./package.json\").name")
59
+ pkg_version=$(node -p "require(\"./package.json\").version")
60
+ pkg_private=$(node -p "Boolean(require(\"./package.json\").private)")
61
+ if [ "$pkg_private" = "true" ]; then
62
+ echo "Skipping $pkg_name (private)"
63
+ exit 0
64
+ fi
65
+ if npm view "$pkg_name@$pkg_version" version 2>/dev/null; then
66
+ echo "Skipping $pkg_name@$pkg_version (already published)"
67
+ else
68
+ echo "Publishing $pkg_name@$pkg_version..."
69
+ pnpm publish --provenance --access public --no-git-checks
70
+ fi
71
+ '
@@ -1,8 +1,19 @@
1
1
  name: Release
2
2
 
3
+ # Release standalone para repos que publicam pacotes SEM site associado
4
+ # (ex.: tooling). Repos que têm site costumam integrar release+publish no
5
+ # ci.yml — veja ci.yml do template.
6
+
3
7
  on:
4
8
  push:
5
9
  branches: [main]
10
+ # Weekly cadence — picks up any docs/CI/chore commits that semantic-
11
+ # release normally wouldn't promote. Ensures consumers stay within
12
+ # one minor of the current HEAD even during low-commit weeks.
13
+ # semantic-release is a no-op when there are no releasable commits,
14
+ # so this is safe to run unconditionally.
15
+ schedule:
16
+ - cron: '0 9 * * 1'
6
17
  workflow_dispatch:
7
18
 
8
19
  concurrency:
@@ -13,23 +24,25 @@ jobs:
13
24
  release:
14
25
  name: semantic-release
15
26
  runs-on: ubuntu-latest
27
+ # `id-token: write` is for Sigstore attestations (`--provenance`),
28
+ # not for npm auth. npm auth uses the `NPM_TOKEN` org-secret.
16
29
  permissions:
17
30
  contents: write
18
31
  issues: write
19
32
  pull-requests: write
20
33
  id-token: write
21
34
  steps:
22
- - uses: actions/checkout@v4
35
+ - uses: actions/checkout@v6
23
36
  with:
24
37
  fetch-depth: 0
25
38
  persist-credentials: false
26
39
 
27
40
  - uses: pnpm/action-setup@v4
28
41
 
29
- - uses: actions/setup-node@v4
42
+ - uses: actions/setup-node@v6
30
43
  with:
31
44
  node-version: 22
32
- cache: pnpm
45
+ cache: 'pnpm'
33
46
  registry-url: 'https://registry.npmjs.org'
34
47
 
35
48
  - name: Install dependencies
@@ -39,11 +52,12 @@ jobs:
39
52
  run: pnpm -r build
40
53
 
41
54
  - name: Configure npm auth
42
- # actions/setup-node writes ~/.npmrc with a ${NODE_AUTH_TOKEN} placeholder
43
- # that `npm` interpolates at publish time, but `pnpm publish` (used by our
44
- # semantic-release `publishCmd`) doesn't always expand it. Write the
45
- # resolved token directly so pnpm reads it unambiguously. Lives in $HOME
46
- # so it doesn't pollute the repo working tree.
55
+ # `actions/setup-node` writes `~/.npmrc` with a literal
56
+ # `${NODE_AUTH_TOKEN}` placeholder that `npm` expands at publish
57
+ # time, but `pnpm publish` (used by semantic-release's
58
+ # `publishCmd`) doesn't always expand it. Write the resolved
59
+ # token directly so pnpm reads it unambiguously. Lives in $HOME
60
+ # so it doesn't pollute the working tree.
47
61
  env:
48
62
  NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
49
63
  run: |
@@ -1,18 +1,22 @@
1
1
  name: Code Review
2
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.
3
+ # Reusable workflow chamado pelo ci.yml para rodar o review automatizado.
4
+ # Tenta providers em ordem (Anthropic OpenAI GitHub Models) e usa o
5
+ # primeiro que retorna 200. Qualquer não-200 dispara fallback para o
6
+ # próximo. GitHub Models é o último degrau: usa o GITHUB_TOKEN built-in
7
+ # + permissão `models: read`, então funciona sem nenhum segredo externo
8
+ # — repos que não configuram ANTHROPIC_API_KEY nem OPENAI_API_KEY ainda
9
+ # têm review automatizado. Se todos falham (ou nenhum está configurado),
10
+ # sai sem postar review e sem emitir marker de round, de modo que a
11
+ # próxima atividade no PR retenta fresco.
8
12
 
9
13
  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
14
+ workflow_call:
15
+ inputs:
16
+ pr_number:
17
+ description: 'PR number to review'
18
+ type: number
19
+ required: true
16
20
 
17
21
  jobs:
18
22
  check-review-status:
@@ -26,7 +30,7 @@ jobs:
26
30
  should_run: ${{ steps.check.outputs.should_run }}
27
31
  review_round: ${{ steps.check.outputs.review_round }}
28
32
  steps:
29
- - uses: actions/checkout@v4
33
+ - uses: actions/checkout@v6
30
34
 
31
35
  - name: Decide whether to run
32
36
  id: check
@@ -34,10 +38,12 @@ jobs:
34
38
  with:
35
39
  github-token: ${{ secrets.GITHUB_TOKEN }}
36
40
  script: |
37
- const prNumber = context.issue.number;
41
+ const prNumber = ${{ inputs.pr_number }};
38
42
 
39
- // Skip docs/config-only PRs
40
- const { data: files } = await github.rest.pulls.listFiles({
43
+ // Paginamos listFiles porque em PRs grandes o .tsx/.ts real pode
44
+ // cair em página 2+ e a detecção de "código mudou" dava falso
45
+ // negativo, pulando o review.
46
+ const files = await github.paginate(github.rest.pulls.listFiles, {
41
47
  owner: context.repo.owner,
42
48
  repo: context.repo.repo,
43
49
  pull_number: prNumber,
@@ -78,12 +84,18 @@ jobs:
78
84
  permissions:
79
85
  contents: read
80
86
  pull-requests: write
87
+ # `models: read` lets this job call GitHub Models via the built-in
88
+ # GITHUB_TOKEN — no external API key needed for the fallback
89
+ # provider. Repos that don't configure ANTHROPIC_API_KEY or
90
+ # OPENAI_API_KEY still get a review via GitHub Models.
91
+ models: read
81
92
  env:
82
93
  ANTHROPIC_MODEL: claude-haiku-4-5-20251001
83
94
  OPENAI_MODEL: gpt-4o-mini
95
+ GITHUB_MODELS_MODEL: openai/gpt-4o-mini
84
96
  MAX_DIFF_SIZE: '150000'
85
97
  steps:
86
- - uses: actions/checkout@v4
98
+ - uses: actions/checkout@v6
87
99
  with:
88
100
  fetch-depth: 0
89
101
 
@@ -92,8 +104,8 @@ jobs:
92
104
  env:
93
105
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
94
106
  REVIEW_ROUND: ${{ needs.check-review-status.outputs.review_round }}
107
+ PR_NUMBER: ${{ inputs.pr_number }}
95
108
  run: |
96
- PR_NUMBER=${{ github.event.pull_request.number }}
97
109
  HEAD_SHA=$(gh pr view "$PR_NUMBER" --json headRefOid -q .headRefOid)
98
110
  echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT
99
111
  echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT
@@ -120,6 +132,9 @@ jobs:
120
132
  env:
121
133
  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
122
134
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
135
+ # GitHub Models uses the built-in GITHUB_TOKEN; `models: read`
136
+ # on the job permissions is what actually authorizes the call.
137
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
123
138
  DIFF_TRUNCATED: ${{ steps.pr.outputs.diff_truncated }}
124
139
  REVIEW_ROUND: ${{ needs.check-review-status.outputs.review_round }}
125
140
  run: |
@@ -163,8 +178,9 @@ jobs:
163
178
  '- If no issues, return an empty feedback array.',
164
179
  ].join('\n');
165
180
 
166
- // Per-1M-token USD pricing. Update when providers change rates.
167
- // Missing entries silently skip cost display (tokens still shown).
181
+ // Pricing por 1M tokens USD. Atualizar quando providers mudarem rates.
182
+ // Entradas faltando silenciosamente pulam exibição de custo (tokens
183
+ // ainda são mostrados).
168
184
  const PRICING = {
169
185
  'claude-haiku-4-5-20251001': { in: 1.00, out: 5.00 },
170
186
  'claude-sonnet-4-6': { in: 3.00, out: 15.00 },
@@ -235,10 +251,46 @@ jobs:
235
251
  }),
236
252
  extractError: (r) => r?.error?.message ?? 'see workflow logs',
237
253
  },
254
+ // GitHub Models — OpenAI-compatible API authenticated via the
255
+ // built-in GITHUB_TOKEN + `models: read` permission. No
256
+ // external API key needed, so this is the fallback when
257
+ // neither Anthropic nor OpenAI secrets are configured.
258
+ // Rate limits apply per-repo; the models catalog lists what's
259
+ // available. `openai/gpt-4o-mini` is a safe default.
260
+ 'github-models': {
261
+ label: 'GitHub Models',
262
+ envKey: 'GITHUB_TOKEN',
263
+ modelEnv: 'GITHUB_MODELS_MODEL',
264
+ defaultModel: 'openai/gpt-4o-mini',
265
+ build: (model, key) => ({
266
+ url: 'https://models.github.ai/inference/chat/completions',
267
+ headers: {
268
+ 'content-type': 'application/json',
269
+ Authorization: `Bearer ${key}`,
270
+ },
271
+ body: JSON.stringify({
272
+ model,
273
+ max_tokens: 8192,
274
+ messages: [
275
+ { role: 'system', content: system },
276
+ { role: 'user', content: user },
277
+ ],
278
+ response_format: { type: 'json_object' },
279
+ }),
280
+ }),
281
+ extractText: (r) => r?.choices?.[0]?.message?.content ?? '',
282
+ extractUsage: (r) => ({
283
+ prompt_tokens: r?.usage?.prompt_tokens ?? 0,
284
+ completion_tokens: r?.usage?.completion_tokens ?? 0,
285
+ }),
286
+ extractError: (r) => r?.error?.message ?? 'see workflow logs',
287
+ },
238
288
  };
239
289
 
240
- // Provider order: try Anthropic, fall back to OpenAI.
241
- const order = ['anthropic', 'openai'];
290
+ // Provider order: prefer Anthropic (best output quality), then
291
+ // OpenAI, then GitHub Models as a keyless fallback. Each
292
+ // provider is skipped if its auth env var isn't set.
293
+ const order = ['anthropic', 'openai', 'github-models'];
242
294
 
243
295
  const setOutput = (k, v) => {
244
296
  fs.appendFileSync(process.env.GITHUB_OUTPUT, `${k}<<EOF\n${String(v)}\nEOF\n`);
@@ -325,7 +377,6 @@ jobs:
325
377
  setOutput('completion_tokens', String(completionTokens));
326
378
  setOutput('cost_usd', cost == null ? '' : cost.toFixed(4));
327
379
 
328
- // Emit fallback details to the workflow log (not the PR comment).
329
380
  const failed = attempts.filter((a) => !a.ok && !a.skip);
330
381
  const fellBack = failed.length > 0;
331
382
  setOutput('fell_back', fellBack ? 'true' : 'false');
@@ -363,7 +414,8 @@ jobs:
363
414
  : '';
364
415
  const fallbackBadge = fellBack ? ' (fallback)' : '';
365
416
 
366
- // Failure pathsno round marker, so the next push retries fresh.
417
+ // Caminhos de falha sem marker, de modo que o próximo push
418
+ // retenta como Round 1.
367
419
  if (status !== 'ok') {
368
420
  const note = status === 'skipped'
369
421
  ? `Review skipped — ${errorReason}. No round marker emitted; the next PR activity will retry as Round 1.`
@@ -1,6 +1,6 @@
1
1
  cff-version: 1.2.0
2
2
  title: '{{REPO_NAME}}'
3
- message: 'If you use this software, please cite it as below.'
3
+ message: 'Se você utilizar este software, por favor cite-o conforme abaixo.'
4
4
  type: software
5
5
  authors:
6
6
  - name: '{{OWNER_ORG}}'