@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.
- package/dist/bin.js +41 -12
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +28 -9
- package/dist/index.js.map +1 -1
- package/dist/templates/.claude/agents/debugger.md +130 -0
- package/dist/templates/.claude/agents/docs-sync.md +97 -0
- package/dist/templates/.claude/agents/pr-review-responder.md +117 -0
- package/dist/templates/.claude/agents/pre-commit-check.md +85 -0
- package/dist/templates/.claude/agents/refactor-scout.md +139 -0
- package/dist/templates/AGENTS.md +45 -0
- package/dist/templates/CLAUDE.md +9 -0
- package/dist/templates/commitlintrc.cjs +10 -0
- package/dist/templates/eslint.config.js +19 -0
- package/dist/templates/github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/dist/templates/github/ISSUE_TEMPLATE/config.yml +4 -4
- package/dist/templates/github/ISSUE_TEMPLATE/feature_request.md +18 -0
- package/dist/templates/github/PULL_REQUEST_TEMPLATE.md +10 -10
- package/dist/templates/github/workflows/_checks.yml +102 -0
- package/dist/templates/github/workflows/_deploy-site.yml +168 -0
- package/dist/templates/github/workflows/_publish.yml +77 -0
- package/dist/templates/github/workflows/_release.yml +156 -0
- package/dist/templates/github/workflows/ci.yml +63 -27
- package/dist/templates/github/workflows/doctor.yml +71 -0
- package/dist/templates/github/workflows/publish-tag.yml +71 -0
- package/dist/templates/github/workflows/release.yml +22 -8
- package/dist/templates/github/workflows/review.yml +75 -23
- package/dist/templates/governance/CITATION.cff +1 -1
- package/dist/templates/governance/CONTRIBUTING.md +14 -14
- package/dist/templates/governance/CONVENTIONS.md +21 -19
- package/dist/templates/governance/SECURITY.md +13 -13
- package/dist/templates/governance/SUPPORT.md +10 -10
- package/dist/templates/husky/commit-msg +4 -2
- package/dist/templates/husky/pre-commit +10 -0
- package/dist/templates/husky/pre-push +24 -10
- package/dist/templates/package.json +71 -0
- package/dist/templates/pnpm-workspace.yaml +3 -0
- package/dist/templates/scripts/worktree.sh +5 -0
- package/dist/templates/templates.manifest.yml +127 -7
- package/dist/templates/tsconfig.json +4 -0
- package/dist/templates/turbo.json +32 -0
- package/package.json +2 -2
- package/dist/templates/github/ISSUE_TEMPLATE/bug.md +0 -33
- 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,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:
|
|
3
|
+
- name: Vulnerabilidade de segurança
|
|
4
4
|
url: 'mailto:{{SECURITY_EMAIL}}'
|
|
5
|
-
about:
|
|
6
|
-
- name:
|
|
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:
|
|
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
|
-
<!--
|
|
1
|
+
<!-- Obrigado pela contribuição. Por favor preencha as seções abaixo. -->
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Resumo
|
|
4
4
|
|
|
5
|
-
<!--
|
|
5
|
+
<!-- Uma ou duas frases sobre o que muda e por quê. -->
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Tipo de alteração
|
|
8
8
|
|
|
9
|
-
- [ ]
|
|
10
|
-
- [ ]
|
|
11
|
-
- [ ] Breaking change (major —
|
|
12
|
-
- [ ]
|
|
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
|
-
##
|
|
14
|
+
## Plano de teste
|
|
15
15
|
|
|
16
|
-
<!--
|
|
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"
|