@precisa-saude/cli 1.0.4 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +67 -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,139 @@
1
+ ---
2
+ name: refactor-scout
3
+ description: Find extraction and deduplication opportunities — large components, duplicated patterns, unused shared utilities
4
+ model: sonnet
5
+ tools: Read, Grep, Glob
6
+ ---
7
+
8
+ You are a refactoring analyst for the Precisa Saúde ecosystem. You scan for both project-specific patterns (component extraction, shared package underuse) and general code smells from established software engineering practices (Fowler's _Refactoring_, Martin's _Clean Code_). You report opportunities — you do NOT make changes.
9
+
10
+ ## Input
11
+
12
+ You will receive either:
13
+
14
+ - Specific file paths to analyze
15
+ - A feature area (e.g., "biomarker pages", "admin dashboard")
16
+ - No input — in which case, scan broadly for the biggest opportunities
17
+
18
+ ## What to Look For
19
+
20
+ ### 1. Large Components (>300 lines)
21
+
22
+ Search for React components that have grown beyond ~300 lines and could be decomposed:
23
+
24
+ ```
25
+ Glob: apps/web/src/**/*.tsx
26
+ ```
27
+
28
+ Read files and check line counts. Flag components that:
29
+
30
+ - Have multiple distinct visual sections that could be separate components
31
+ - Mix data fetching logic with rendering
32
+ - Contain inline sub-components defined within the file
33
+
34
+ ### 2. Duplicated JSX Patterns
35
+
36
+ Search for similar UI patterns used in multiple places:
37
+
38
+ - Status badges with the same color mapping logic
39
+ - Card layouts with identical structure but different data
40
+ - Form field groups with repeated validation patterns
41
+ - Table cell renderers with shared formatting
42
+
43
+ ### 3. Shared Package Underuse
44
+
45
+ The codebase has shared packages that should be used consistently:
46
+
47
+ - **`@precisa-saude/fhir`**: `normalizeCode()`, `getReferenceRange()`, biomarker definitions
48
+ - **`@precisa-saude/ui`**: Shared UI components
49
+ - **`@precisa-saude/api-types`**: TypeScript types for API responses
50
+ - **`@precisa-saude/evidence`**: Evidence-based content
51
+
52
+ Search for violations:
53
+
54
+ - Direct LOINC code strings instead of `normalizeCode()`
55
+ - Hardcoded reference ranges instead of `getReferenceRange()`
56
+ - Manual biomarker name formatting instead of `getBiomarkerDisplayName()`
57
+ - Duplicated TypeScript interfaces that exist in `api-types`
58
+
59
+ ### 4. Hook Duplication
60
+
61
+ Search for custom hooks with similar state management patterns:
62
+
63
+ - Multiple hooks doing the same fetch-cache-display pattern
64
+ - Duplicated `useEffect` patterns for keyboard, scroll, or viewport handling
65
+ - Category navigation or filtering logic repeated across pages
66
+
67
+ ### 5. Code Smells & Quality Patterns
68
+
69
+ Scan for well-known code smells adapted for TypeScript/React:
70
+
71
+ #### Complexity & Control Flow
72
+
73
+ - Deeply nested conditionals (>3 levels of if/else/ternary) — flatten with early returns or guard clauses (Fowler: _Replace Nested Conditional with Guard Clauses_)
74
+ - Long functions (>40 lines for utilities, >80 for components) — extract logical sections into named functions (Fowler: _Extract Function_)
75
+ - Complex boolean expressions — extract to named variables or predicate functions (e.g., `isEligibleForTrial` vs `user.plan === 'free' && !user.expired && user.labResults.length > 0`)
76
+ - Switch/if-else chains on type — consider lookup maps or polymorphism (Fowler: _Replace Conditional with Polymorphism_)
77
+
78
+ #### Function & Parameter Design
79
+
80
+ - Long parameter lists (>3 params) — group into an options/config object (Fowler: _Introduce Parameter Object_)
81
+ - Flag arguments (boolean params that change function behavior) — split into two clearly named functions (Martin: _Clean Code_ ch. 3)
82
+ - Output parameters — functions that mutate their arguments instead of returning a new value
83
+
84
+ #### Naming & Clarity
85
+
86
+ - Generic names that don't communicate intent — `data`, `info`, `item`, `handler`, `process`, `manage`, `utils`
87
+ - Inconsistent naming for the same concept — `user` vs `patient` vs `profile` for the same entity across files
88
+ - Abbreviated names that sacrifice readability — `btn`, `usr`, `calc`, `idx` (outside well-known conventions like `i`, `e`, `ctx`)
89
+
90
+ #### Code Organization
91
+
92
+ - Feature envy — a function that reads/writes more from another module than its own (Fowler: _Move Function_)
93
+ - Dead code — unused imports, unreachable branches after early returns, commented-out code blocks, exported functions with zero consumers
94
+ - Primitive obsession — raw strings/numbers where a union type or enum would add safety (e.g., `status: string` instead of `status: 'pending' | 'active' | 'expired'`) (Fowler: _Replace Primitive with Object_)
95
+ - Data clumps — the same group of variables always passed together (e.g., `userId`, `userName`, `userEmail` instead of a `User` type)
96
+
97
+ #### Error Handling
98
+
99
+ - Empty catch blocks — silently swallowing errors without logging or re-throwing
100
+ - Overly broad catch — catching generic `Error` when only specific exceptions are expected
101
+ - Error strings instead of types — `throw new Error('NOT_FOUND')` instead of a typed error class or union
102
+
103
+ #### React-Specific Smells
104
+
105
+ - Prop drilling (>2 levels) — props passed through components that don't use them; consider context or composition
106
+ - God components — components owning too many concerns (data fetching + complex state + rendering + side effects)
107
+ - Inline object/array literals in JSX props — creates new references every render, defeating memoization
108
+ - useEffect as lifecycle — effects that replicate mount/unmount/update patterns instead of synchronizing with specific data dependencies
109
+
110
+ ## Output Format
111
+
112
+ ```
113
+ Refactoring Opportunities
114
+ =========================
115
+
116
+ ### High Impact (>300 lines or duplicated in 3+ places)
117
+
118
+ 1. **Extract `BiomarkerStatusBadge`** from:
119
+ - `apps/web/src/components/biomarker/BiomarkerCard.tsx:142-168`
120
+ - `apps/web/src/components/biomarker/BiomarkerList.tsx:89-115`
121
+ - Same badge rendering with identical color mapping in both
122
+
123
+ 2. **Use `normalizeCode()` from @precisa-saude/fhir** in:
124
+ - `apps/api/src/services/lab-results.ts:234` — raw LOINC string comparison
125
+
126
+ ### Medium Impact (repeated in 2 places or >200 lines)
127
+ ...
128
+
129
+ ### Low Impact (cleanup opportunities)
130
+ ...
131
+ ```
132
+
133
+ ## Rules
134
+
135
+ - **Read-only** — report opportunities, never modify code
136
+ - **Reference existing shared packages** — always check if a utility exists before suggesting a new one
137
+ - **Prioritize by impact** — large components and 3+ duplications first
138
+ - **Include line numbers** — make it easy to navigate to each finding
139
+ - **Don't suggest premature abstractions** — 2 similar lines don't need a helper function
@@ -0,0 +1,45 @@
1
+ # Specific instructions — {{REPO_NAME}}
2
+
3
+ > This file holds ONLY the rules specific to this repository. The
4
+ > shared rules across the precisa-saude ecosystem (tone, git, hooks,
5
+ > reviews, worktrees, source verification, test coverage, code
6
+ > conventions) live in `@precisa-saude/agent-instructions`.
7
+ >
8
+ > **Read the shared base online:**
9
+ > https://github.com/Precisa-Saude/tooling/blob/main/packages/agent-instructions/AGENTS.md
10
+ >
11
+ > Claude Code loads both files (shared base + this one) via imports in
12
+ > `CLAUDE.md`. Update the base with:
13
+ > `pnpm update @precisa-saude/agent-instructions`.
14
+
15
+ ## Overview
16
+
17
+ <!-- One or two sentences on what this repo does. -->
18
+
19
+ ## Structure
20
+
21
+ ```
22
+ packages/
23
+ ...
24
+ site/ (if applicable)
25
+ ```
26
+
27
+ ## Commit scopes
28
+
29
+ Valid scopes: {{COMMIT_SCOPES_HUMAN}}.
30
+
31
+ ## Worktree — specific values
32
+
33
+ Worktree flow and commands are in the shared base. The canonical config
34
+ lives in `package.json` under `"worktree"`. For quick reference:
35
+
36
+ | Field | Value |
37
+ | ------------- | ------------------------------------------ |
38
+ | Port registry | `/tmp/{{REPO_NAME}}-worktree-ports.json` |
39
+ | Services | (filled in when the repo adds dev servers) |
40
+
41
+ Launch a dev server in a feature worktree:
42
+
43
+ ```bash
44
+ pnpm exec precisa-worktree dev --detach
45
+ ```
@@ -0,0 +1,9 @@
1
+ # Claude instructions
2
+
3
+ This repository follows the [AGENTS.md](https://agents.md/) convention.
4
+ Shared rules across the precisa-saude ecosystem live in
5
+ `@precisa-saude/agent-instructions` (installed as a devDependency), and
6
+ repo-specific rules live in `./AGENTS.md`.
7
+
8
+ @./node_modules/@precisa-saude/agent-instructions/AGENTS.md
9
+ @./AGENTS.md
@@ -0,0 +1,10 @@
1
+ const base = require('@precisa-saude/commitlint-config');
2
+
3
+ /** @type {import('@commitlint/types').UserConfig} */
4
+ module.exports = {
5
+ ...base,
6
+ rules: {
7
+ ...base.rules,
8
+ 'scope-enum': [2, 'always', [{{COMMIT_SCOPES_JSON}}]],
9
+ },
10
+ };
@@ -0,0 +1,19 @@
1
+ import base from '@precisa-saude/eslint-config/base';
2
+
3
+ export default [
4
+ ...base,
5
+ {
6
+ // Test files are excluded from package tsconfigs (to keep tsc --noEmit
7
+ // tight), so disable type-aware parsing for them or ESLint errors
8
+ // trying to locate a project.
9
+ files: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx', '**/__tests__/**'],
10
+ languageOptions: {
11
+ parserOptions: { project: false },
12
+ },
13
+ rules: {
14
+ '@typescript-eslint/consistent-type-imports': 'off',
15
+ 'max-lines': 'off',
16
+ 'max-lines-per-function': 'off',
17
+ },
18
+ },
19
+ ];
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: Reporte de bug
3
+ about: Algo está quebrado
4
+ title: '[bug] '
5
+ labels: [bug]
6
+ ---
7
+
8
+ ## O que aconteceu
9
+
10
+ <!-- Descrição curta do comportamento inesperado. -->
11
+
12
+ ## Reprodução
13
+
14
+ <!-- Passos mínimos. Inclua o comando exato executado, pacote + versão, versões do Node/pnpm. -->
15
+
16
+ ```bash
17
+ # comando(s)
18
+ ```
19
+
20
+ ## Esperado
21
+
22
+ <!-- O que você esperava que acontecesse. -->
23
+
24
+ ## Ambiente
25
+
26
+ - Pacote + versão:
27
+ - Node: `node --version`
28
+ - pnpm: `pnpm --version`
29
+ - SO: macOS / Linux / Windows
30
+
31
+ ## Contexto adicional
32
+
33
+ <!-- Stack trace, logs de CI, etc. -->
@@ -1,8 +1,8 @@
1
1
  blank_issues_enabled: false
2
2
  contact_links:
3
- - name: Security vulnerability
3
+ - name: Vulnerabilidade de segurança
4
4
  url: 'mailto:{{SECURITY_EMAIL}}'
5
- about: Report security issues privatelysee SECURITY.md
6
- - name: General questions and discussions
5
+ about: Reporte questões de segurança em privado veja SECURITY.md
6
+ - name: Perguntas e discussões gerais
7
7
  url: 'https://github.com/{{REPO_SLUG}}/discussions'
8
- about: Ask questions, share ideas, or discuss
8
+ about: Faça perguntas, compartilhe ideias ou discuta
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: Solicitação de feature
3
+ about: Proponha uma nova funcionalidade ou melhoria
4
+ title: '[feat] '
5
+ labels: [enhancement]
6
+ ---
7
+
8
+ ## Problema
9
+
10
+ <!-- Qual limitação ou fricção você está enfrentando? -->
11
+
12
+ ## Solução proposta
13
+
14
+ <!-- Esboce a API / comportamento. Exemplos concretos preferíveis a prosa. -->
15
+
16
+ ## Alternativas consideradas
17
+
18
+ <!-- Outras abordagens que você considerou e por que descartou. -->
@@ -1,18 +1,18 @@
1
- <!-- Thanks for contributing. Please fill out the sections below. -->
1
+ <!-- Obrigado pela contribuição. Por favor preencha as seções abaixo. -->
2
2
 
3
- ## Summary
3
+ ## Resumo
4
4
 
5
- <!-- One or two sentences on what this changes and why. -->
5
+ <!-- Uma ou duas frases sobre o que muda e por quê. -->
6
6
 
7
- ## Type of change
7
+ ## Tipo de alteração
8
8
 
9
- - [ ] Bug fix (patch)
10
- - [ ] Feature (minor)
11
- - [ ] Breaking change (major — include a `BREAKING CHANGE:` footer on the commit)
12
- - [ ] Docs / CI only (no version bump expected)
9
+ - [ ] Correção de bug (`fix` patch)
10
+ - [ ] Nova funcionalidade (`feat` — minor)
11
+ - [ ] Breaking change (major — inclua um footer `BREAKING CHANGE:` no commit)
12
+ - [ ] Apenas docs / CI (sem bump de versão esperado)
13
13
 
14
- ## Test plan
14
+ ## Plano de teste
15
15
 
16
- <!-- How a reviewer can verify this. -->
16
+ <!-- Como um revisor pode verificar essa mudança. -->
17
17
 
18
18
  - [ ]
@@ -0,0 +1,102 @@
1
+ name: CI Checks
2
+
3
+ # Reusable workflow — parallel CI checks. Each concern runs as its own
4
+ # job so feedback is fast: a lint failure doesn't wait for the test
5
+ # suite. Called by the repo's `ci.yml` orchestrator.
6
+ #
7
+ # Each repo MAY add extra checks (e.g. FHIR IG compile, benchmark
8
+ # invariant scripts) by inlining additional jobs here after sync, or
9
+ # in a separate workflow if the check is big enough to warrant it.
10
+
11
+ on:
12
+ workflow_call:
13
+
14
+ jobs:
15
+ commitlint-title:
16
+ name: PR title (Conventional Commits)
17
+ if: github.event_name == 'pull_request'
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 5
20
+ steps:
21
+ - uses: actions/checkout@v6
22
+ - uses: pnpm/action-setup@v4
23
+ - uses: actions/setup-node@v6
24
+ with:
25
+ node-version: 22
26
+ cache: 'pnpm'
27
+ - run: pnpm install --frozen-lockfile
28
+ - name: Validate PR title
29
+ env:
30
+ PR_TITLE: ${{ github.event.pull_request.title }}
31
+ run: echo "$PR_TITLE" | pnpm exec commitlint
32
+
33
+ lint:
34
+ name: Lint
35
+ runs-on: ubuntu-latest
36
+ timeout-minutes: 10
37
+ steps:
38
+ - uses: actions/checkout@v6
39
+ - uses: pnpm/action-setup@v4
40
+ - uses: actions/setup-node@v6
41
+ with:
42
+ node-version: 22
43
+ cache: 'pnpm'
44
+ - run: pnpm install --frozen-lockfile
45
+ - run: pnpm turbo run lint
46
+
47
+ typecheck:
48
+ name: Typecheck
49
+ runs-on: ubuntu-latest
50
+ timeout-minutes: 10
51
+ steps:
52
+ - uses: actions/checkout@v6
53
+ - uses: pnpm/action-setup@v4
54
+ - uses: actions/setup-node@v6
55
+ with:
56
+ node-version: 22
57
+ cache: 'pnpm'
58
+ - run: pnpm install --frozen-lockfile
59
+ - run: pnpm turbo run typecheck
60
+
61
+ format:
62
+ name: Format check
63
+ runs-on: ubuntu-latest
64
+ timeout-minutes: 5
65
+ steps:
66
+ - uses: actions/checkout@v6
67
+ - uses: pnpm/action-setup@v4
68
+ - uses: actions/setup-node@v6
69
+ with:
70
+ node-version: 22
71
+ cache: 'pnpm'
72
+ - run: pnpm install --frozen-lockfile
73
+ - run: pnpm format:check
74
+
75
+ build:
76
+ name: Build
77
+ runs-on: ubuntu-latest
78
+ timeout-minutes: 15
79
+ steps:
80
+ - uses: actions/checkout@v6
81
+ - uses: pnpm/action-setup@v4
82
+ - uses: actions/setup-node@v6
83
+ with:
84
+ node-version: 22
85
+ cache: 'pnpm'
86
+ - run: pnpm install --frozen-lockfile
87
+ - run: pnpm turbo run build
88
+
89
+ test:
90
+ name: Test
91
+ runs-on: ubuntu-latest
92
+ timeout-minutes: 15
93
+ steps:
94
+ - uses: actions/checkout@v6
95
+ - uses: pnpm/action-setup@v4
96
+ - uses: actions/setup-node@v6
97
+ with:
98
+ node-version: 22
99
+ cache: 'pnpm'
100
+ - run: pnpm install --frozen-lockfile
101
+ - run: pnpm turbo run build
102
+ - run: pnpm turbo run test
@@ -0,0 +1,168 @@
1
+ name: Deploy site
2
+
3
+ # Reusable workflow — builds the repo's site and deploys to Cloudflare
4
+ # Pages when site files changed, then sends a Slack notification.
5
+ # Guards on site file presence across the pushed range.
6
+
7
+ on:
8
+ workflow_call:
9
+ inputs:
10
+ project_name:
11
+ description: 'Cloudflare Pages project name'
12
+ type: string
13
+ required: true
14
+ site_filter:
15
+ description: 'pnpm filter to build the site (e.g. @medbench-brasil/site)'
16
+ type: string
17
+ required: true
18
+ site_path:
19
+ description: 'Output directory to deploy (default: site/dist)'
20
+ type: string
21
+ default: 'site/dist'
22
+
23
+ jobs:
24
+ build-site:
25
+ name: Build site
26
+ runs-on: ubuntu-latest
27
+ outputs:
28
+ site_changed: ${{ steps.changes.outputs.site_changed }}
29
+ steps:
30
+ - name: Check for site changes
31
+ id: changes
32
+ uses: actions/github-script@v7
33
+ with:
34
+ script: |
35
+ if (context.eventName === 'workflow_dispatch') {
36
+ core.setOutput('site_changed', 'true');
37
+ return;
38
+ }
39
+ const before = context.payload.before;
40
+ const after = context.sha;
41
+ const isInitial = !before || /^0+$/.test(before);
42
+ let filenames = [];
43
+ if (isInitial) {
44
+ const { data: commit } = await github.rest.repos.getCommit({
45
+ owner: context.repo.owner, repo: context.repo.repo, ref: after,
46
+ });
47
+ filenames = (commit.files ?? []).map(f => f.filename);
48
+ } else {
49
+ const pages = await github.paginate(
50
+ github.rest.repos.compareCommitsWithBasehead,
51
+ {
52
+ owner: context.repo.owner,
53
+ repo: context.repo.repo,
54
+ basehead: `${before}...${after}`,
55
+ per_page: 100,
56
+ },
57
+ );
58
+ filenames = pages.flatMap(p => (p.files ?? []).map(f => f.filename));
59
+ }
60
+ const siteChanged = filenames.some(f => f.startsWith('site/'));
61
+ core.setOutput('site_changed', siteChanged ? 'true' : 'false');
62
+
63
+ - uses: actions/checkout@v6
64
+ if: steps.changes.outputs.site_changed == 'true'
65
+ - uses: pnpm/action-setup@v4
66
+ if: steps.changes.outputs.site_changed == 'true'
67
+ - uses: actions/setup-node@v6
68
+ if: steps.changes.outputs.site_changed == 'true'
69
+ with:
70
+ node-version: 22
71
+ cache: 'pnpm'
72
+ - name: Install dependencies
73
+ if: steps.changes.outputs.site_changed == 'true'
74
+ run: pnpm install --frozen-lockfile
75
+ # `--filter=<site>...` includes workspace deps; without `...` tsc
76
+ # breaks not finding types/dist of consumed packages.
77
+ - name: Build site
78
+ if: steps.changes.outputs.site_changed == 'true'
79
+ run: pnpm turbo run build --filter=${{ inputs.site_filter }}...
80
+ - uses: actions/upload-artifact@v6
81
+ if: steps.changes.outputs.site_changed == 'true'
82
+ with:
83
+ name: site-dist
84
+ path: ${{ inputs.site_path }}
85
+ retention-days: 1
86
+
87
+ deploy-site:
88
+ name: Deploy site
89
+ needs: build-site
90
+ if: always() && !cancelled() && needs.build-site.outputs.site_changed == 'true' && needs.build-site.result == 'success' && github.ref == 'refs/heads/main' && github.event_name == 'push'
91
+ runs-on: ubuntu-latest
92
+ steps:
93
+ - uses: actions/download-artifact@v7
94
+ with:
95
+ name: site-dist
96
+ path: ${{ inputs.site_path }}
97
+ - uses: actions/setup-node@v6
98
+ with:
99
+ node-version: 22
100
+ - name: Deploy to Cloudflare Pages
101
+ uses: cloudflare/wrangler-action@v3
102
+ with:
103
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
104
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
105
+ packageManager: npm
106
+ command: pages deploy ${{ inputs.site_path }} --project-name=${{ inputs.project_name }} --branch=main --commit-hash=${{ github.sha }} --commit-message="${{ github.sha }}"
107
+ - name: Purge Cloudflare cache
108
+ if: env.CLOUDFLARE_ZONE_ID != ''
109
+ env:
110
+ CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
111
+ run: |
112
+ curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache" \
113
+ -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
114
+ -H "Content-Type: application/json" \
115
+ --data '{"purge_everything":true}'
116
+
117
+ notify:
118
+ name: Notify Slack
119
+ needs: [build-site, deploy-site]
120
+ if: always() && github.event_name != 'pull_request' && needs.deploy-site.result != 'skipped'
121
+ runs-on: ubuntu-latest
122
+ steps:
123
+ # Slack payload via jq -n — auto-escapes quotes, backslashes,
124
+ # newlines, control chars. Interpolating commit messages or SHAs raw
125
+ # into YAML strings is injection-prone.
126
+ - name: Build notification
127
+ env:
128
+ GH_SHA: ${{ github.sha }}
129
+ GH_SERVER: ${{ github.server_url }}
130
+ GH_REPO: ${{ github.repository }}
131
+ GH_RUN_ID: ${{ github.run_id }}
132
+ DEPLOY_RESULT: ${{ needs.deploy-site.result }}
133
+ PROJECT_NAME: ${{ inputs.project_name }}
134
+ run: |
135
+ SHORT_SHA="${GH_SHA:0:7}"
136
+ RUN_URL="${GH_SERVER}/${GH_REPO}/actions/runs/${GH_RUN_ID}"
137
+ COMMIT_URL="${GH_SERVER}/${GH_REPO}/commit/${GH_SHA}"
138
+
139
+ if [ "$DEPLOY_RESULT" = "success" ]; then
140
+ HEADER=":rocket: ${PROJECT_NAME} site deployed"
141
+ RESULT_LINE=":white_check_mark: *Site* — deployed to Cloudflare Pages"
142
+ TEXT="Deploy successful"
143
+ else
144
+ HEADER=":rotating_light: ${PROJECT_NAME} site deploy failed"
145
+ RESULT_LINE=":x: *Site* — deploy failed"
146
+ TEXT="Deploy failed"
147
+ fi
148
+
149
+ jq -n \
150
+ --arg text "$TEXT" \
151
+ --arg header "$HEADER" \
152
+ --arg result_line "$RESULT_LINE" \
153
+ --arg context "*Commit:* <${COMMIT_URL}|${SHORT_SHA}> | *Run:* <${RUN_URL}|View logs>" \
154
+ '{
155
+ text: $text,
156
+ blocks: [
157
+ { type: "header", text: { type: "plain_text", text: $header, emoji: true } },
158
+ { type: "section", text: { type: "mrkdwn", text: $result_line } },
159
+ { type: "context", elements: [ { type: "mrkdwn", text: $context } ] }
160
+ ]
161
+ }' > /tmp/slack-payload.json
162
+
163
+ - name: Send Slack notification
164
+ uses: slackapi/slack-github-action@v2.1.0
165
+ with:
166
+ webhook: ${{ secrets.SLACK_DEPLOY_WEBHOOK_URL }}
167
+ webhook-type: incoming-webhook
168
+ payload-file-path: /tmp/slack-payload.json
@@ -0,0 +1,77 @@
1
+ name: Publish to npm
2
+
3
+ # Reusable workflow — publishes workspace packages to npm after release.
4
+ # Consumers pass the list of package dirs as a newline-separated input;
5
+ # each dir is published idempotently (checks the registry first).
6
+ #
7
+ # Pins to `release_sha` so a concurrent push can't change the tree
8
+ # between release creation and publish.
9
+
10
+ on:
11
+ workflow_call:
12
+ inputs:
13
+ release_sha:
14
+ description: 'Commit SHA to check out (the chore(release) commit from _release.yml)'
15
+ type: string
16
+ required: true
17
+ packages:
18
+ description: 'Newline-separated list of package directories to publish (e.g. "packages/core\npackages/calculators")'
19
+ type: string
20
+ required: true
21
+
22
+ jobs:
23
+ publish:
24
+ name: Publish
25
+ runs-on: ubuntu-latest
26
+ # `id-token: write` is for Sigstore attestations (`--provenance`),
27
+ # NOT for npm auth. Auth uses `NPM_TOKEN` org-secret — OIDC trusted
28
+ # publishing was evaluated and rejected because it requires manual
29
+ # per-package click-through in the npm web UI (no CLI/API to
30
+ # automate). `--provenance` gives us supply chain attestations
31
+ # regardless of auth method.
32
+ permissions:
33
+ contents: read
34
+ id-token: write
35
+ steps:
36
+ - uses: actions/checkout@v6
37
+ with:
38
+ ref: ${{ inputs.release_sha }}
39
+
40
+ - uses: pnpm/action-setup@v4
41
+
42
+ - uses: actions/setup-node@v6
43
+ with:
44
+ node-version: 22
45
+ cache: 'pnpm'
46
+ registry-url: 'https://registry.npmjs.org'
47
+
48
+ - name: Install dependencies
49
+ run: pnpm install --frozen-lockfile
50
+
51
+ - name: Build
52
+ run: pnpm turbo run build
53
+
54
+ - name: Publish packages
55
+ env:
56
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
57
+ PACKAGES: ${{ inputs.packages }}
58
+ run: |
59
+ set -e
60
+ publish_if_needed() {
61
+ local dir="$1"
62
+ local pkg_name pkg_version
63
+ pkg_name=$(node -p "require('./$dir/package.json').name")
64
+ pkg_version=$(node -p "require('./$dir/package.json').version")
65
+
66
+ if npm view "$pkg_name@$pkg_version" version 2>/dev/null; then
67
+ echo "Skipping $pkg_name@$pkg_version (already published)"
68
+ else
69
+ echo "Publishing $pkg_name@$pkg_version..."
70
+ ( cd "$dir" && pnpm publish --provenance --access public --no-git-checks )
71
+ fi
72
+ }
73
+
74
+ while IFS= read -r dir; do
75
+ [ -z "$dir" ] && continue
76
+ publish_if_needed "$dir"
77
+ done <<< "$PACKAGES"