Skip to content

Commit 2f0d9c4

Browse files
committed
feat: Added pnpm node workflow
Refs: RATY-319
1 parent 94fb916 commit 2f0d9c4

2 files changed

Lines changed: 307 additions & 0 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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`. Also provide the following inputs and secrets 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+
56+
### 🔑 Secrets
57+
58+
- **`SONAR_TOKEN`**: Token for SonarQube Cloud Scan. Required.
59+
60+
### 📄 Example usage (`<own project>/.github/workflows/ci.yml`)
61+
62+
```yaml
63+
name: CI
64+
65+
on:
66+
push:
67+
branches: [main]
68+
pull_request:
69+
branches: [main]
70+
workflow_dispatch:
71+
72+
jobs:
73+
common:
74+
uses: City-of-Helsinki/.github/.github/workflows/ci-pnpm-node.yml@main
75+
secrets:
76+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
77+
with:
78+
node-version: 24
79+
extra-commands: |
80+
echo "EXTRA_TEST_ENV_VAR=test" >> $GITHUB_ENV
81+
```

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

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

0 commit comments

Comments
 (0)