-
-
Notifications
You must be signed in to change notification settings - Fork 50
454 lines (419 loc) · 19.2 KB
/
Copy pathdotnet.yml
File metadata and controls
454 lines (419 loc) · 19.2 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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
name: build-test-coverage
# Restrict the GITHUB_TOKEN to read-only contents access. The jobs only
# need to checkout the repo, run dotnet build / test, and upload TRX +
# coverage artifacts; no commit / release / package-write privileges are
# required. Defense-in-depth against supply-chain attacks via a
# compromised dependency or a test-side RCE.
permissions:
contents: read
on:
push:
branches:
- master
paths-ignore:
- 'README.md'
- 'docs/**'
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches:
- master
paths-ignore:
- 'README.md'
- 'docs/**'
jobs:
# ------------------------------------------------------------------
# Job 1 — unsharded unit + integration sweep on both OS legs.
# Excludes Category=E2E altogether; the route walk is handled in
# job 3 below. windows-latest additionally excludes
# Category=RequiresDocker (hosted Windows runners do NOT pre-install
# Docker for Linux containers — only Windows containers via Hyper-V).
# XsdLoadStrict has no platform dependency and still runs on both
# legs. This job carries no Node / docfx / npm overhead: every step
# it runs is a pure .NET workload, so the docs-build wall-clock is
# paid exactly once across the whole workflow (in job 2 below).
# ------------------------------------------------------------------
build-and-test:
name: build-and-test-${{ matrix.os }}
# Skip drafts: run only on push-to-master + ready (non-draft) PRs.
# The pull_request `types` list above includes `ready_for_review`
# so CI fires the moment a draft is flipped to ready.
if: github.event_name == 'push' || github.event.pull_request.draft == false
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
include:
# ubuntu-latest runs every non-E2E category: Docker is pre-
# installed on GitHub-hosted Linux runners and Testcontainers
# can spawn the eclipse-mosquitto + mtconnect/agent images
# the RequiresDocker suites need; the XSD source-of-truth
# files ship in-repo, so XsdLoadStrict also has every
# prerequisite. The shared filter excludes E2E only.
- os: ubuntu-latest
testFilter: "Category!=E2E"
# windows-latest GitHub-hosted runners do NOT pre-install
# Docker for Linux containers (only Windows containers via
# Hyper-V), so the RequiresDocker suites that boot Linux
# images (eclipse-mosquitto:2.0.22, mtconnect/agent:latest)
# cannot run there. E2E is already excluded for every leg of
# this job. XsdLoadStrict has no platform dependency and
# still runs.
- os: windows-latest
testFilter: "Category!=RequiresDocker&Category!=E2E"
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup .NET 8.0 + 9.0
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: |
8.0.x
9.0.x
- name: Restore dotnet tools (ReportGenerator)
run: dotnet tool restore
- name: Restore solution
run: dotnet restore MTConnect.NET.sln
- name: Build (Debug)
run: dotnet build MTConnect.NET.sln --configuration Debug --no-restore
# MTConnect.NET-Integration-Tests is skipped here (its
# IsTestProject is false unless IntegrationCoverage=true) and run
# in the dedicated step below so it can use its own
# tests/coverlet.integration.runsettings. VSTest precedence is
# CLI --settings > RunSettingsFilePath, so a per-project settings
# file cannot win against this solution-wide --settings; running
# the project as its own step is the only way to give it
# different coverage scoping. See the integration .csproj
# comment.
- name: Run unit tests with coverage
env:
TEST_FILTER: ${{ matrix.testFilter }}
TRX_NAME: test-results-${{ matrix.os }}.trx
run: |
dotnet test MTConnect.NET.sln \
--configuration Debug \
--no-build \
--settings tests/coverlet.runsettings \
--results-directory TestResults \
--logger "trx;LogFileName=${TRX_NAME}" \
--filter "$TEST_FILTER"
shell: bash
# Same zero-test false-green guard as the integration step, on
# the solution-wide unit run. A broken adapter wiring or an
# over-eager filter that discovers nothing must hard-fail rather
# than exit 0.
- name: Guard — unit step executed > 0 tests
env:
TRX_NAME: test-results-${{ matrix.os }}.trx
run: |
set -euo pipefail
trx="TestResults/${TRX_NAME}"
if [ ! -f "$trx" ]; then
echo "::error::Unit TRX not found at $trx — the test step did not produce results."
exit 1
fi
total="$(grep -o '<Counters[^>]*total="[0-9]*"' "$trx" \
| grep -o 'total="[0-9]*"' | grep -o '[0-9]*' | head -n1)"
if [ -z "${total:-}" ]; then
echo "::error::Could not parse <Counters total=...> from $trx — refusing to false-green."
exit 1
fi
echo "Unit tests executed: $total"
if [ "$total" -eq 0 ]; then
echo "::error::Unit step executed 0 tests — discovery is broken (zero-test false-green guard)."
exit 1
fi
shell: bash
# Integration suite with its own coverlet settings: the shared
# configuration plus MTConnect.NET-Common and MTConnect.NET-HTTP
# excluded from instrumentation, and MaxCpuCount=1 (this step is
# serial; the shared file no longer pins it). Common+HTTP are
# the dominant-cost assemblies on the timing-critical in-process
# Agent/HTTP hot path this suite drives; coverlet IL
# instrumentation of them on a slow shared runner pushes sample
# delivery past the test's wait. Both are covered (faster) by
# MTConnect.NET-Common-Tests / MTConnect.NET-HTTP-Tests. The
# latter (with its AgentRunner support project
# MTConnect.NET-Tests-Agents) is a member of MTConnect.NET.sln,
# so the "Run unit tests with coverage" step above already
# discovers and runs it under the shared runsettings; it starts
# a real MTConnectAgentBroker + MTConnectHttpServer and streams
# Probe/Current/Sample through the HTTP clients, exercising the
# Common + HTTP runtime path. Its Cobertura ReportGenerator
# merges with this one, so excluding Common+HTTP here loses no
# net coverage. MTConnect.NET-SHDR stays instrumented (the
# integration suite is its only runtime coverage).
#
# The integration .csproj sets IsTestProject=false unless
# IntegrationCoverage=true, so the solution build above compiled
# it as a non-test assembly. This step therefore rebuilds the
# project WITH -p:IntegrationCoverage=true (so xunit's VSTest
# adapter is wired in and tests are discoverable) and only then
# runs it with --no-build. Without the explicit build the
# solution-built non-test assembly would discover 0 tests and
# the step would false-green.
- name: Build integration project with IntegrationCoverage flag
run: |
dotnet build tests/MTConnect.NET-Integration-Tests/MTConnect.NET-Integration-Tests.csproj \
--configuration Debug \
--no-restore \
-p:IntegrationCoverage=true
shell: bash
- name: Run integration tests with coverage
env:
TEST_FILTER: ${{ matrix.testFilter }}
TRX_NAME: test-results-integration-${{ matrix.os }}.trx
run: |
dotnet test tests/MTConnect.NET-Integration-Tests/MTConnect.NET-Integration-Tests.csproj \
--configuration Debug \
--no-build \
-p:IntegrationCoverage=true \
--settings tests/coverlet.integration.runsettings \
--results-directory TestResults \
--logger "trx;LogFileName=${TRX_NAME}" \
--filter "$TEST_FILTER"
shell: bash
# Hard-fail the job if the integration step discovered/executed
# 0 tests. A future regression that re-breaks test discovery
# (e.g. the IntegrationCoverage build wiring) would otherwise
# false-green: dotnet test exits 0 when it finds nothing to run.
# Parse the TRX <Counters .../> total and fail when it is 0 or
# the element is missing.
- name: Guard — integration step executed > 0 tests
env:
TRX_NAME: test-results-integration-${{ matrix.os }}.trx
run: |
set -euo pipefail
trx="TestResults/${TRX_NAME}"
if [ ! -f "$trx" ]; then
echo "::error::Integration TRX not found at $trx — the test step did not produce results."
exit 1
fi
total="$(grep -o '<Counters[^>]*total="[0-9]*"' "$trx" \
| grep -o 'total="[0-9]*"' | grep -o '[0-9]*' | head -n1)"
if [ -z "${total:-}" ]; then
echo "::error::Could not parse <Counters total=...> from $trx — refusing to false-green."
exit 1
fi
echo "Integration tests executed: $total"
if [ "$total" -eq 0 ]; then
echo "::error::Integration step executed 0 tests — discovery is broken (zero-test false-green guard)."
exit 1
fi
shell: bash
- name: Generate coverage HTML + summary
if: always()
run: |
dotnet reportgenerator \
-reports:"TestResults/**/coverage.cobertura.xml" \
-targetdir:"coverage-report" \
-reporttypes:"Html;TextSummary;MarkdownSummary"
shell: bash
- name: Upload TRX + coverage artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: test-results-${{ matrix.os }}
path: |
TestResults/**/*.trx
TestResults/**/coverage.cobertura.xml
coverage-report/
if-no-files-found: warn
retention-days: 14
- name: Surface coverage summary in job log
if: always()
env:
JOB_LABEL: ${{ matrix.os }}
run: |
if [ -f coverage-report/Summary.txt ]; then
echo "### Coverage summary (${JOB_LABEL})" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
cat coverage-report/Summary.txt >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
fi
shell: bash
# ------------------------------------------------------------------
# Job 2 — docs preparation. Runs once. Produces docs/.vitepress/dist
# by invoking `npm ci && npm run build`, which transitively triggers
# the `prebuild` hook (`npm run regen` → `bash scripts/generate-api-
# ref.sh` → `docfx metadata`). The built dist tree is uploaded as a
# workflow artifact so the sharded route-check job below can skip
# the npm + docfx + build wall-clock (~5 min) entirely. The test
# fixture's [OneTimeSetUp] checks for docs/.vitepress/dist/index.html
# and skips the `npm ci && npm run build` bootstrap when the file is
# present, so a shard that downloads the artifact into the right
# path bypasses the bootstrap altogether.
# ------------------------------------------------------------------
docs-prepare:
name: docs-prepare
if: github.event_name == 'push' || github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup .NET 8.0 + 9.0
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: |
8.0.x
9.0.x
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
cache: npm
cache-dependency-path: docs/package-lock.json
# `npm run build` in docs/ invokes the `prebuild` hook (`npm run
# regen`), which calls `bash scripts/generate-api-ref.sh` →
# `docfx metadata` to produce the /api/ reference tree the
# VitePress site consumes.
- name: Install docfx
run: dotnet tool install -g docfx
shell: bash
- name: npm ci
working-directory: docs
run: npm ci
- name: npm run build
working-directory: docs
run: npm run build
- name: Upload docs dist artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: docs-dist
path: docs/.vitepress/dist/
if-no-files-found: error
retention-days: 1
# ------------------------------------------------------------------
# Job 3 — sharded Category=E2E route walk. Four parallel ubuntu
# shards each carry a disjoint subset of the markdown-backed routes
# via ROUTE_SHARD_INDEX / ROUTE_SHARD_TOTAL env vars (consumed by
# RouteCheckHelpers.ReadShardEnv + ShardRoutes). The docs-dist
# artifact from job 2 is downloaded into docs/.vitepress/dist/ so
# the test fixture's [OneTimeSetUp] skips the npm bootstrap (it
# detects the pre-existing index.html). npm ci still runs to
# populate docs/node_modules/ so `npx vitepress preview` finds the
# local vitepress binary; that step is fast on the npm cache hit
# primed by `actions/setup-node`. docfx is NOT installed here — the
# prebuild hook only fires from `npm run build`, which this job
# skips. Each shard runs only `Category=E2E`, so the non-E2E
# categories run exactly once (in job 1) across the workflow.
# ------------------------------------------------------------------
route-check-e2e:
name: route-check-e2e-shard${{ matrix.shard }}of${{ matrix.shardTotal }}
if: github.event_name == 'push' || github.event.pull_request.draft == false
needs: docs-prepare
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
shardTotal: [4]
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup .NET 8.0 + 9.0
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: |
8.0.x
9.0.x
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
cache: npm
cache-dependency-path: docs/package-lock.json
- name: Restore dotnet tools (ReportGenerator)
run: dotnet tool restore
- name: Restore solution
run: dotnet restore MTConnect.NET.sln
- name: Build (Debug)
run: dotnet build MTConnect.NET.sln --configuration Debug --no-restore
# Populate docs/node_modules/ so `npx vitepress preview` resolves
# the local vitepress binary the test fixture spawns. The npm
# cache primed by actions/setup-node above makes this fast on
# warm runners; first-pull-after-bump pays the full cost once
# per shard. Skipping `npm run build` is the actual win — that's
# the ~5 min docfx + vitepress build that job 2 paid once.
- name: npm ci
working-directory: docs
run: npm ci
- name: Download docs dist artifact
uses: actions/download-artifact@v4
with:
name: docs-dist
path: docs/.vitepress/dist/
# Cache Playwright browser binaries so the Category=E2E
# [OneTimeSetUp] `playwright install chromium` call is a no-op
# on cache hit. The key hashes the `playwright-core`
# package.json that the Microsoft.Playwright NuGet copies into
# the build output — that file's `version` field directly pins
# the chromium revision the .NET binding drives. Hashing the
# csproj instead would only invalidate by coincidence (a future
# PR adding an unrelated PackageReference would invalidate; an
# SDK-side change to the chromium revision would not). The step
# therefore runs AFTER `dotnet build` so the file exists —
# placement before build would force the key off a workspace-
# internal artefact (hashFiles cannot reach the NuGet cache
# under ~/.nuget), so the micro-optimisation of overlapping
# cache restore with build is sacrificed for key correctness.
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/tests/MTConnect.NET-Docs-Tests/bin/Debug/net8.0/.playwright/package/package.json') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Run E2E route-check shard
env:
# ROUTE_SHARD_INDEX / ROUTE_SHARD_TOTAL drive
# RouteCheckTests via RouteCheckHelpers.ReadShardEnv ->
# ShardRoutes, splitting the ~1,880-route Playwright walk
# across N parallel matrix entries. Integers come from the
# static strategy.matrix definition above, never from
# github.event.* untrusted input.
ROUTE_SHARD_INDEX: ${{ matrix.shard }}
ROUTE_SHARD_TOTAL: ${{ matrix.shardTotal }}
TRX_NAME: test-results-e2e-shard${{ matrix.shard }}of${{ matrix.shardTotal }}.trx
run: |
dotnet test tests/MTConnect.NET-Docs-Tests/MTConnect.NET-Docs-Tests.csproj \
--configuration Debug \
--no-build \
--settings tests/coverlet.runsettings \
--results-directory TestResults \
--logger "trx;LogFileName=${TRX_NAME}" \
--filter "Category=E2E"
shell: bash
- name: Guard — E2E shard executed > 0 tests
env:
TRX_NAME: test-results-e2e-shard${{ matrix.shard }}of${{ matrix.shardTotal }}.trx
run: |
set -euo pipefail
trx="TestResults/${TRX_NAME}"
if [ ! -f "$trx" ]; then
echo "::error::E2E TRX not found at $trx — the test step did not produce results."
exit 1
fi
total="$(grep -o '<Counters[^>]*total="[0-9]*"' "$trx" \
| grep -o 'total="[0-9]*"' | grep -o '[0-9]*' | head -n1)"
if [ -z "${total:-}" ]; then
echo "::error::Could not parse <Counters total=...> from $trx — refusing to false-green."
exit 1
fi
echo "E2E tests executed: $total"
if [ "$total" -eq 0 ]; then
echo "::error::E2E shard executed 0 tests — discovery is broken (zero-test false-green guard)."
exit 1
fi
shell: bash
- name: Upload E2E TRX + coverage artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: test-results-e2e-shard${{ matrix.shard }}of${{ matrix.shardTotal }}
path: |
TestResults/**/*.trx
TestResults/**/coverage.cobertura.xml
if-no-files-found: warn
retention-days: 14