Skip to content

Commit 054c3fe

Browse files
committed
feat: Added pnpm node workflow
Refs: RATY-319
1 parent c2321a9 commit 054c3fe

2 files changed

Lines changed: 306 additions & 0 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# 🚀 Reusable CI workflow for Node (pnpm)
2+
3+
This reusable workflow is part of the City of Helsinki’s GitHub Actions setup, specifically designed to provide an opinionated and consistent CI process for City of Helsinki’s **pnpm-based** Node projects.
4+
5+
This repository has two similar Node CI workflows on purpose:
6+
7+
- `ci-node.yml` is the general workflow for yarn-based projects.
8+
- `ci-pnpm-node.yml` is the pnpm-specific workflow for projects that use `pnpm-lock.yaml`, pnpm install behavior, and pnpm commands.
9+
10+
Keeping both workflows allows projects to use the same CI structure while matching their package manager and lockfile strategy.
11+
12+
## 🌟 Key Features
13+
14+
- **Commit Linting**: Enforces commit message standards using [commitlint](https://commitlint.js.org/).
15+
- **Build and Lint**: Build and verifies code style and formatting via pnpm.
16+
- **Automated Testing**: Runs project tests via pnpm.
17+
- **Code Quality Analysis**: Performs a [SonarQube Cloud](https://sonarcloud.io/) scan.
18+
19+
## 📋 Requirements for Projects Using the Workflow
20+
21+
- **commitlint** [config file](https://commitlint.js.org/reference/configuration.html#config-via-file) is present in the root of the project.
22+
- **pnpm lockfile** (`pnpm-lock.yaml`) is present in the configured `working-directory`.
23+
- **SonarQube Cloud** is configured with `SONAR_TOKEN` set in the repository or organization secrets.
24+
25+
### 🧶 pnpm Commands
26+
27+
- **build** the project.
28+
- **lint** run [eslint](https://eslint.org/) or another lint tool.
29+
- **test:coverage** runs project tests with coverage.
30+
31+
### 🪡 Optional pnpm Commands
32+
33+
- **typecheck** run tsc check
34+
- **check-size** run browser bundle size limits check. The command is run if the `.size-limit.js` file is found in the app directory.
35+
- **check-dist**: run ecmascript checks for build files. The command is run if the `.escheckrc` file is found in the app directory.
36+
37+
## 📚 Usage Instructions
38+
39+
To use this reusable workflow, create a project-specific workflow file in your `.github/workflows` directory. Ensure the `uses` value is set to `City-of-Helsinki/.github/.github/workflows/ci-pnpm-node.yml@main` and `secrets` is set to `inherit`. Also provide the following inputs as needed:
40+
41+
### 🛑 Required Inputs
42+
43+
- **`node-version`** (string): Specifies the Node version to use in the workflow.
44+
45+
### 🔶 Optional Inputs
46+
47+
- **`extra-commands`** (string): Additional setup commands or checks to execute before running tests. Can be used to set environment variables: `echo "EXTRA_TEST_ENV_VAR=test" >> $GITHUB_ENV`.
48+
- **`typecheck`** (boolean): Run typecheck command. Default is `false`.
49+
- **`working-directory`** (string): Repository working directory where to run pnpm installation and the tests jobs. Default is `.` (the repository root).
50+
- **`app-directory`** (string): The subdirectory of the application where lint/build/tests are run. Default is **`working-directory`**.
51+
- **`upload_artifacts`** (boolean): Set to true to upload build artifacts. Default is `false`.
52+
- **`skip_sonar`** (boolean): Set to true to skip SonarQube checks. Default is `false`.
53+
- **`skip-build`** (boolean): Set to true to skip the build phase. Default is `false`.
54+
- **`ignore-scripts`** (boolean): Set to true to skip lifecycle scripts during install (`--ignore-scripts`). Default is `true`.
55+
- **`pnpm-version`** (string): pnpm major/minor version to install. Default is `10`.
56+
57+
### 📄 Example usage (`<own project>/.github/workflows/ci.yml`)
58+
59+
```yaml
60+
name: CI
61+
62+
on:
63+
push:
64+
branches: [main]
65+
pull_request:
66+
branches: [main]
67+
workflow_dispatch:
68+
69+
jobs:
70+
common:
71+
uses: City-of-Helsinki/.github/.github/workflows/ci-pnpm-node.yml@main
72+
secrets: inherit
73+
with:
74+
node-version: 20
75+
extra-commands: |
76+
echo "EXTRA_TEST_ENV_VAR=test" >> $GITHUB_ENV
77+
```

.github/workflows/ci-pnpm-node.yml

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
name: Common CI for Node (pnpm)
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
node-version:
7+
description: Node version to use
8+
required: true
9+
type: string
10+
extra-commands:
11+
description: Extra setup commands to run before lint/test/build
12+
required: false
13+
type: string
14+
app-directory:
15+
description: Application subdirectory for lint/test/build
16+
required: false
17+
type: string
18+
default: "."
19+
typecheck:
20+
description: Run type checking
21+
required: false
22+
type: boolean
23+
default: false
24+
upload_artifacts:
25+
description: Upload build artifacts
26+
required: false
27+
type: boolean
28+
default: false
29+
skip_sonar:
30+
description: Skip SonarQube
31+
required: false
32+
type: boolean
33+
default: false
34+
skip-build:
35+
description: Skip build phase
36+
required: false
37+
type: boolean
38+
default: false
39+
working-directory:
40+
description: Working directory for install/build/test jobs
41+
required: false
42+
type: string
43+
default: "."
44+
ignore-scripts:
45+
description: Pass --ignore-scripts to pnpm install
46+
required: false
47+
type: boolean
48+
default: true
49+
pnpm-version:
50+
description: pnpm major/minor version
51+
required: false
52+
type: string
53+
default: "10"
54+
55+
workflow_dispatch:
56+
57+
jobs:
58+
commitlint:
59+
name: Check commit messages
60+
runs-on: ubuntu-latest
61+
steps:
62+
- name: Checkout
63+
uses: actions/checkout@v4
64+
- name: Run commitlint
65+
uses: wagoid/commitlint-github-action@3d28780bbf0365e29b144e272b2121204d5be5f3
66+
67+
build:
68+
name: Lint and build
69+
if: ${{ !inputs.skip-build }}
70+
runs-on: ubuntu-latest
71+
defaults:
72+
run:
73+
working-directory: ${{ inputs.working-directory }}
74+
steps:
75+
- uses: actions/checkout@v4
76+
77+
- name: Setup pnpm
78+
uses: pnpm/action-setup@v4
79+
with:
80+
version: ${{ inputs.pnpm-version }}
81+
82+
- name: Setup Node.js with dependency path
83+
if: ${{ inputs.working-directory != '.' }}
84+
uses: actions/setup-node@v4
85+
with:
86+
node-version: ${{ inputs.node-version }}
87+
cache: pnpm
88+
cache-dependency-path: ${{ inputs.working-directory }}/pnpm-lock.yaml
89+
90+
- name: Setup Node.js without dependency path
91+
if: ${{ inputs.working-directory == '.' }}
92+
uses: actions/setup-node@v4
93+
with:
94+
node-version: ${{ inputs.node-version }}
95+
cache: pnpm
96+
97+
- name: Install dependencies
98+
run: |
99+
IGNORE_SCRIPTS="${{ inputs.ignore-scripts == true && '--ignore-scripts' || '' }}"
100+
pnpm install --frozen-lockfile $IGNORE_SCRIPTS
101+
102+
- name: Run extra commands
103+
if: ${{ inputs.extra-commands != '' }}
104+
run: ${{ inputs.extra-commands }}
105+
106+
- name: Typecheck application
107+
if: ${{ inputs.typecheck }}
108+
working-directory: ${{ inputs.app-directory == '.' && inputs.working-directory || inputs.app-directory }}
109+
run: pnpm typecheck
110+
111+
- name: Lint application
112+
working-directory: ${{ inputs.app-directory == '.' && inputs.working-directory || inputs.app-directory }}
113+
run: pnpm lint
114+
115+
- name: Build application
116+
working-directory: ${{ inputs.app-directory == '.' && inputs.working-directory || inputs.app-directory }}
117+
run: pnpm build
118+
119+
- name: Upload build
120+
if: ${{ inputs.upload_artifacts }}
121+
uses: actions/upload-artifact@v4
122+
with:
123+
name: build-artifacts-${{ inputs.node-version }}
124+
path: |
125+
${{ inputs.app-directory }}/dist
126+
${{ inputs.app-directory }}/build
127+
${{ inputs.app-directory }}/lib
128+
retention-days: 1
129+
130+
- name: Check browser bundle size limits
131+
if: ${{ hashFiles(format('{0}/.size-limit.js', inputs.app-directory)) != '' }}
132+
working-directory: ${{ inputs.app-directory == '.' && inputs.working-directory || inputs.app-directory }}
133+
run: pnpm check-size
134+
135+
- name: Check ecmascript checks for build files
136+
if: ${{ hashFiles(format('{0}/.escheckrc', inputs.app-directory)) != '' }}
137+
working-directory: ${{ inputs.app-directory == '.' && inputs.working-directory || inputs.app-directory }}
138+
run: pnpm check-dist
139+
140+
tests:
141+
name: Test
142+
runs-on: ubuntu-latest
143+
defaults:
144+
run:
145+
working-directory: ${{ inputs.working-directory }}
146+
steps:
147+
- uses: actions/checkout@v4
148+
149+
- name: Setup pnpm
150+
uses: pnpm/action-setup@v4
151+
with:
152+
version: ${{ inputs.pnpm-version }}
153+
154+
- name: Setup Node.js with dependency path
155+
if: ${{ inputs.working-directory != '.' }}
156+
uses: actions/setup-node@v4
157+
with:
158+
node-version: ${{ inputs.node-version }}
159+
cache: pnpm
160+
cache-dependency-path: ${{ inputs.working-directory }}/pnpm-lock.yaml
161+
162+
- name: Setup Node.js without dependency path
163+
if: ${{ inputs.working-directory == '.' }}
164+
uses: actions/setup-node@v4
165+
with:
166+
node-version: ${{ inputs.node-version }}
167+
cache: pnpm
168+
169+
- name: Install dependencies
170+
run: |
171+
IGNORE_SCRIPTS="${{ inputs.ignore-scripts == true && '--ignore-scripts' || '' }}"
172+
pnpm install --frozen-lockfile $IGNORE_SCRIPTS
173+
174+
- name: Run extra commands
175+
if: ${{ inputs.extra-commands != '' }}
176+
run: ${{ inputs.extra-commands }}
177+
178+
- name: Run tests
179+
working-directory: ${{ inputs.app-directory == '.' && inputs.working-directory || inputs.app-directory }}
180+
run: pnpm test:coverage
181+
env:
182+
CI: true
183+
184+
# Coverage path logic (keep in sync with sonarcloud job's cache/restore step):
185+
# Resolves the effective directory (app-directory if set, else working-directory),
186+
# then prefixes 'coverage' with '<dir>/' when running in a subdirectory.
187+
- name: Store coverage report
188+
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
189+
with:
190+
path: ${{ (inputs.app-directory == '.' && inputs.working-directory || inputs.app-directory) && format('{0}/', inputs.app-directory == '.' && inputs.working-directory || inputs.app-directory) || ''}}coverage
191+
key: cache-${{ github.run_id }}-${{ github.run_attempt }}
192+
193+
sonarcloud:
194+
name: Run SonarQube Cloud Scan
195+
if: ${{ !inputs.skip_sonar }}
196+
runs-on: ubuntu-latest
197+
needs: tests
198+
defaults:
199+
run:
200+
working-directory: ${{ inputs.working-directory }}
201+
steps:
202+
- uses: actions/checkout@v4
203+
with:
204+
fetch-depth: 0
205+
206+
# Coverage path logic (keep in sync with tests job's cache/save step):
207+
# Resolves the effective directory (app-directory if set, else working-directory),
208+
# then prefixes 'coverage' with '<dir>/' when running in a subdirectory.
209+
- name: Restore coverage report
210+
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
211+
with:
212+
path: ${{ (inputs.app-directory == '.' && inputs.working-directory || inputs.app-directory) && format('{0}/', inputs.app-directory == '.' && inputs.working-directory || inputs.app-directory) || ''}}coverage
213+
key: cache-${{ github.run_id }}-${{ github.run_attempt }}
214+
fail-on-cache-miss: true
215+
216+
# Without this workaround, SonarQube Cloud reports a warning about an incorrect source path.
217+
- name: Override coverage report source path for SonarQube Cloud
218+
working-directory: ${{ inputs.app-directory == '.' && inputs.working-directory || inputs.app-directory }}
219+
run: |
220+
find coverage -type f \( -name '*.info' -o -name 'lcov.info' \) 2>/dev/null | xargs -r sed -i 's@SF:'$GITHUB_WORKSPACE'@SF:/github/workspace/@g' || true
221+
find coverage -type f -name '*.json' 2>/dev/null | xargs -r sed -i 's@'$GITHUB_WORKSPACE'@/github/workspace/@g' || true
222+
223+
- name: SonarQube Cloud Scan
224+
uses: SonarSource/sonarqube-scan-action@v6
225+
with:
226+
projectBaseDir: ${{ inputs.app-directory == '.' && inputs.working-directory || inputs.app-directory }}
227+
env:
228+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
229+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

0 commit comments

Comments
 (0)