Skip to content

Commit 5e3b591

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

2 files changed

Lines changed: 300 additions & 0 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
56+
### 📄 Example usage (`<own project>/.github/workflows/ci.yml`)
57+
58+
```yaml
59+
name: CI
60+
61+
on:
62+
push:
63+
branches: [main]
64+
pull_request:
65+
branches: [main]
66+
workflow_dispatch:
67+
68+
jobs:
69+
common:
70+
uses: City-of-Helsinki/.github/.github/workflows/ci-pnpm-node.yml@main
71+
secrets: inherit
72+
with:
73+
node-version: 20
74+
extra-commands: |
75+
echo "EXTRA_TEST_ENV_VAR=test" >> $GITHUB_ENV
76+
```

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

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

0 commit comments

Comments
 (0)