Repository navigation
150 lines (134 loc) · 5.99 KB
/
Copy pathserver-examples-build.yml
File metadata and controls
150 lines (134 loc) · 5.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
name: server-examples — build
# Builds every frontend under server-examples/. Nothing else in CI touches this
# tree: ci.yml and the e2e workflows are scoped to runner/ and examples/, and
# the starter matrix boots a dev server rather than `build`. That gap is how
# DEV-2727 and DEV-2731 both shipped — a Handsontable release tightened a public
# type, every Angular frontend stopped compiling, and no job noticed.
#
# These 21 projects are NOT part of the root pnpm workspace (the root
# package.json declares no `workspaces` and there is no pnpm-workspace.yaml
# covering them). Each carries its own npm package-lock.json, so this is a
# per-project matrix rather than one install at the root. The matrix is
# discovered at run time from the checkout, so adding a backend or a frontend
# needs no edit here.
#
# Two jobs, because they answer different questions:
#
# build deterministic — `npm ci` against the committed lock. Guards
# source edits. By construction it can NEVER see a new upstream
# release, because the lock pins one.
# build-latest canary — installs handsontable@latest (and the wrapper) before
# building. THIS is the job that would have caught 18.1.0 on the
# day it shipped. It is expected to go red on a breaking release;
# that is the signal, not a flake.
#
# `npm run build` is deliberately what runs, rather than a hand-rolled tsc
# invocation: it is what a contributor runs, and for the Angular projects it is
# `ng build --configuration development`, whose AOT pass is what actually
# type-checks the `[settings]` binding.
on:
pull_request:
paths:
- 'server-examples/**'
- '.github/workflows/server-examples-build.yml'
schedule:
# Weekly, Monday 04:00 UTC. Off-peak for the CET team, and clear of the
# nightly e2e workflows (01:00 / 03:00) — this job needs no container pool,
# but there is no reason to pile onto the same window.
- cron: '0 4 * * 1'
workflow_dispatch: {}
concurrency:
group: server-examples-build-${{ github.ref }}
cancel-in-progress: true
jobs:
# Emits the project list as a matrix. A project qualifies if it has a
# package-lock.json AND a `build` script — that excludes express/server and
# nestjs/server, which are backends with neither a build step nor a
# Handsontable dependency.
discover:
runs-on: ubuntu-latest
outputs:
projects: ${{ steps.find.outputs.projects }}
steps:
- uses: actions/checkout@v5
- id: find
run: |
projects=$(find server-examples -maxdepth 3 -name package-lock.json \
-not -path '*/node_modules/*' -exec dirname {} \; \
| sort \
| while read -r d; do
# `if`, not `&&`: Actions runs `bash -e -o pipefail`, so the
# loop's exit status is the last iteration's. With `&&`, a
# project that sorts LAST and is legitimately skipped would
# fail this whole job. Today the last entry happens to have a
# build script; that is luck, not a guarantee.
if node -e "process.exit(require('./$d/package.json').scripts?.build ? 0 : 1)"; then
echo "$d"
fi
done \
| jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "projects=$projects" >> "$GITHUB_OUTPUT"
echo "Discovered: $projects"
build:
needs: discover
runs-on: ubuntu-latest
strategy:
# One broken project must not mask the state of the other 20 — the whole
# point of this workflow is to see the full blast radius of a type change.
fail-fast: false
matrix:
project: ${{ fromJSON(needs.discover.outputs.projects) }}
defaults:
run:
working-directory: ${{ matrix.project }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
cache-dependency-path: ${{ matrix.project }}/package-lock.json
- run: npm ci
# Asserts, rather than reports: `npm ls` exits non-zero when the installed
# tree does not satisfy package.json. That is the one failure this job
# exists to catch and cannot see otherwise — a hand-edited or stale lock
# would still build fine against whatever it happened to resolve.
- name: Verify the lock resolves a satisfying Handsontable
run: npm ls handsontable --depth=0
- run: npm run build
build-latest:
# PRs get the deterministic job only. This one exists to detect upstream
# drift, which a PR cannot introduce.
if: github.event_name != 'pull_request'
needs: discover
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
project: ${{ fromJSON(needs.discover.outputs.projects) }}
defaults:
run:
working-directory: ${{ matrix.project }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
cache-dependency-path: ${{ matrix.project }}/package-lock.json
- run: npm ci
# The wrapper differs per project family (angular / react / none), so read
# it out of package.json rather than hard-coding three variants.
- name: Install handsontable@latest
run: |
wrapper=$(node -e "
const d = require('./package.json').dependencies || {};
const w = Object.keys(d).find(k => /^@handsontable\//.test(k));
process.stdout.write(w ? w + '@latest' : '');
")
echo "Installing handsontable@latest $wrapper"
# shellcheck disable=SC2086 # $wrapper may be empty; quoting it would pass "".
npm install handsontable@latest $wrapper
- name: Report resolved Handsontable version
run: npm ls handsontable --depth=0 || true
- run: npm run build