@plainconceptsplatform/workflows 1.16.2 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/workflow-catalog.d.ts +1 -1
- package/dist/workflow-catalog.js +2 -0
- package/loops/templates/ci/app-ci-dotnet-next.yml +7 -107
- package/loops/templates/ci/app-ci-node-monorepo.yml +4 -31
- package/loops/templates/ci/quality-mutation.yml +268 -0
- package/loops/workflows/agent-implement.md +13 -4
- package/package.json +1 -1
|
@@ -19,7 +19,7 @@ export interface MandatoryFile {
|
|
|
19
19
|
}
|
|
20
20
|
export declare const mandatoryFiles: readonly MandatoryFile[];
|
|
21
21
|
export declare const generatedConsumerTargets: readonly [".github/workflows/agent-*.lock.yml", ".github/aw/actions-lock.json"];
|
|
22
|
-
export declare const templateNames: readonly ["agentics-app-errors", "agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "bug-report", "dependabot-merge", "feature-request", "github-release", "opencode.ci.json", "platform-template", "quality-gates"];
|
|
22
|
+
export declare const templateNames: readonly ["agentics-app-errors", "agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "bug-report", "dependabot-merge", "feature-request", "github-release", "opencode.ci.json", "platform-template", "quality-gates", "quality-mutation"];
|
|
23
23
|
export type TemplateName = (typeof templateNames)[number];
|
|
24
24
|
export interface CatalogTemplate {
|
|
25
25
|
readonly name: TemplateName;
|
package/dist/workflow-catalog.js
CHANGED
|
@@ -62,6 +62,7 @@ export const templateNames = [
|
|
|
62
62
|
"opencode.ci.json",
|
|
63
63
|
"platform-template",
|
|
64
64
|
"quality-gates",
|
|
65
|
+
"quality-mutation",
|
|
65
66
|
];
|
|
66
67
|
export const catalogTemplates = [
|
|
67
68
|
{ name: "agentics-app-errors", file: "agentics-app-errors.yml", description: "Daily application error report: asks Application Insights what the deployed app threw, groups it, and files one issue per distinct problem HERE, labelled bug + refine so the belt picks it up. Deterministic, no model; the report never leaves this repository, and a leak scanner withholds anything carrying an identifier." },
|
|
@@ -76,6 +77,7 @@ export const catalogTemplates = [
|
|
|
76
77
|
{ name: "opencode.ci.json", file: "opencode.ci.json", description: "Standalone OpenCode CI config: plainconcepts provider, GLM model registration, ci-workflow-agent, and LSP defaults for consumer repositories." },
|
|
77
78
|
{ name: "platform-template", file: "scripts/check-platform-conformance.mjs", directory: "platform-template", bundle: true, description: "Template sync driven by the platform contract (.platform/contract.yml): a weekly workflow that runs its sync.update command and opens or updates one pull request, and a CI guard that keeps its owned paths equal to the template outside a sync. Does nothing without a contract." },
|
|
78
79
|
{ name: "quality-gates", file: "scripts/check-coverage.mjs", directory: "quality-gates", bundle: true, description: "Platform family quality gates: coverage, complexity, duplication and mutation gates, the Engineering Excellence facets, the Semgrep hygiene rules, jscpd and ZAP triage. Floors come from the app's quality.config.json; baselines stay app files." },
|
|
80
|
+
{ name: "quality-mutation", file: "quality-mutation.yml", directory: "ci", description: "Weekly out-of-band mutation testing, the Atlas shape: one Stryker.NET job per .NET scope and one StrykerJS web job, each gated on its covered-code floor from quality.config.json, folded into the engineering-excellence-mutation facet. Never on pull requests; App: CI keeps the coverage floor as the per-change gate. Needs the quality-gates template." },
|
|
79
81
|
];
|
|
80
82
|
/** Bundle templates: several package-managed files, refreshed by `update` where installed. */
|
|
81
83
|
export const bundleTemplateNames = catalogTemplates
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
# every push and through its own weekly schedule. The platform-conformance job runs the
|
|
12
12
|
# platform-template guard where the app declares a platform contract.
|
|
13
13
|
#
|
|
14
|
+
# No mutation testing here, on purpose: a pull request's quality gate is the coverage floor, and
|
|
15
|
+
# Stryker over even one changed page took longer than the rest of CI together. Mutation runs out of
|
|
16
|
+
# band, weekly, in `Quality: Mutation testing (scheduled)` (`workflows add --template quality-mutation`).
|
|
17
|
+
#
|
|
14
18
|
# Everything that differs between apps is a top-level `env:` value below or a floor in
|
|
15
19
|
# quality.config.json. Job names are required status checks on main: do not rename them.
|
|
16
20
|
name: "App: CI"
|
|
@@ -279,51 +283,6 @@ jobs:
|
|
|
279
283
|
if-no-files-found: ignore
|
|
280
284
|
retention-days: 7
|
|
281
285
|
|
|
282
|
-
mutation-api:
|
|
283
|
-
name: Mutation (.NET, diff)
|
|
284
|
-
if: ${{ (github.event_name == 'pull_request') && needs.depth.outputs.full == 'true' }}
|
|
285
|
-
needs: [depth]
|
|
286
|
-
runs-on: ${{ vars.CI_RUNNER || 'agents-arc' }}
|
|
287
|
-
timeout-minutes: 25
|
|
288
|
-
defaults:
|
|
289
|
-
run:
|
|
290
|
-
working-directory: ${{ env.API_WORKING_DIRECTORY }}
|
|
291
|
-
steps:
|
|
292
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
293
|
-
with:
|
|
294
|
-
# Stryker --since diffs against the PR base; it needs history, not a shallow tip.
|
|
295
|
-
fetch-depth: 0
|
|
296
|
-
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
|
|
297
|
-
with:
|
|
298
|
-
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
299
|
-
env:
|
|
300
|
-
DOTNET_INSTALL_DIR: ${{ runner.tool_cache }}/dotnet
|
|
301
|
-
- name: Detect backend changes
|
|
302
|
-
id: scope
|
|
303
|
-
working-directory: ${{ github.workspace }}
|
|
304
|
-
run: |
|
|
305
|
-
git fetch origin "$GITHUB_BASE_REF" --depth=1
|
|
306
|
-
if git diff --name-only "origin/$GITHUB_BASE_REF" HEAD | grep -qE "^${API_WORKING_DIRECTORY}/"; then
|
|
307
|
-
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
308
|
-
else
|
|
309
|
-
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
310
|
-
echo "No ${API_WORKING_DIRECTORY} changes in this PR; skipping .NET mutation."
|
|
311
|
-
fi
|
|
312
|
-
- name: Restore tools and build
|
|
313
|
-
if: steps.scope.outputs.run == 'true'
|
|
314
|
-
run: |
|
|
315
|
-
dotnet tool restore
|
|
316
|
-
dotnet build "$DOTNET_SOLUTION" -c Release
|
|
317
|
-
# The project, test projects and thresholds come from the app's stryker-config.json.
|
|
318
|
-
- name: Stryker.NET (changed files only)
|
|
319
|
-
if: steps.scope.outputs.run == 'true'
|
|
320
|
-
run: |
|
|
321
|
-
export PATH="$PATH:$HOME/.dotnet/tools"
|
|
322
|
-
dotnet stryker \
|
|
323
|
-
--since:"origin/$GITHUB_BASE_REF" \
|
|
324
|
-
--concurrency 2 \
|
|
325
|
-
--reporter progress --reporter json
|
|
326
|
-
|
|
327
286
|
web:
|
|
328
287
|
name: Web (Next.js static export)
|
|
329
288
|
needs: [depth, scope]
|
|
@@ -582,65 +541,6 @@ jobs:
|
|
|
582
541
|
node scripts/check-platform-conformance.mjs --compare | tee -a "$GITHUB_STEP_SUMMARY"
|
|
583
542
|
fi
|
|
584
543
|
|
|
585
|
-
mutation-web:
|
|
586
|
-
name: Mutation (web, diff)
|
|
587
|
-
if: ${{ (github.event_name == 'pull_request') && needs.depth.outputs.full == 'true' }}
|
|
588
|
-
needs: [depth]
|
|
589
|
-
runs-on: ${{ vars.CI_RUNNER || 'agents-arc' }}
|
|
590
|
-
timeout-minutes: 60
|
|
591
|
-
steps:
|
|
592
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
593
|
-
with:
|
|
594
|
-
fetch-depth: 0
|
|
595
|
-
- name: Detect web changes
|
|
596
|
-
id: scope
|
|
597
|
-
run: |
|
|
598
|
-
git fetch origin "$GITHUB_BASE_REF" --depth=1
|
|
599
|
-
if git diff --name-only "origin/$GITHUB_BASE_REF" HEAD | grep -qE "^apps/web/"; then
|
|
600
|
-
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
601
|
-
else
|
|
602
|
-
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
603
|
-
echo "No apps/web changes in this PR; skipping web mutation."
|
|
604
|
-
fi
|
|
605
|
-
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
|
606
|
-
if: steps.scope.outputs.run == 'true'
|
|
607
|
-
with:
|
|
608
|
-
run_install: false
|
|
609
|
-
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
610
|
-
if: steps.scope.outputs.run == 'true'
|
|
611
|
-
with:
|
|
612
|
-
node-version: ${{ env.NODE_VERSION }}
|
|
613
|
-
cache: pnpm
|
|
614
|
-
cache-dependency-path: pnpm-lock.yaml
|
|
615
|
-
- name: Configure optional private registry authentication
|
|
616
|
-
if: steps.scope.outputs.run == 'true'
|
|
617
|
-
env:
|
|
618
|
-
NPM_REGISTRY_TOKEN: ${{ secrets.NPM_REGISTRY_TOKEN }}
|
|
619
|
-
run: |
|
|
620
|
-
if [ -n "$NPM_REGISTRY_TOKEN" ]; then
|
|
621
|
-
echo "${NPM_REGISTRY_URL}:_authToken=$NPM_REGISTRY_TOKEN" >> .npmrc
|
|
622
|
-
fi
|
|
623
|
-
- name: Install and run StrykerJS (changed files only)
|
|
624
|
-
if: steps.scope.outputs.run == 'true'
|
|
625
|
-
run: |
|
|
626
|
-
pnpm install --frozen-lockfile
|
|
627
|
-
# StrykerJS v9 removed --since. Compute changed TS/TSX files and pass them to --mutate.
|
|
628
|
-
# The config already excludes test files; we only need to scope to changed source files.
|
|
629
|
-
changed_files=$(git diff --name-only "origin/$GITHUB_BASE_REF" HEAD -- 'apps/web/src/**/*.ts' 'apps/web/src/**/*.tsx' \
|
|
630
|
-
| sed 's|^apps/web/||' \
|
|
631
|
-
| grep -v '\.test\.' \
|
|
632
|
-
| grep -v '\.spec\.' \
|
|
633
|
-
| grep -v '/__mocks__/' \
|
|
634
|
-
| paste -sd, -)
|
|
635
|
-
if [ -z "$changed_files" ]; then
|
|
636
|
-
echo "No changed web source files to mutate; skipping."
|
|
637
|
-
exit 0
|
|
638
|
-
fi
|
|
639
|
-
echo "Mutating: $changed_files"
|
|
640
|
-
# StrykerJS v9 child worker cannot auto-discover @stryker-mutator/vitest-runner
|
|
641
|
-
# under pnpm hoisting. --appendPlugins forces the plugin import from the workspace.
|
|
642
|
-
cd apps/web && pnpm exec stryker run --mutate "$changed_files" --appendPlugins "@stryker-mutator/vitest-runner"
|
|
643
|
-
|
|
644
544
|
secret-scan:
|
|
645
545
|
name: Secret scan (TruffleHog, verified only)
|
|
646
546
|
runs-on: ${{ vars.CI_RUNNER || 'agents-arc' }}
|
|
@@ -956,7 +856,7 @@ jobs:
|
|
|
956
856
|
# The merge train reads this job by its name to tell a full run from a light one: do not rename it.
|
|
957
857
|
full-ci:
|
|
958
858
|
name: Full CI
|
|
959
|
-
needs: [depth, api, web, e2e,
|
|
859
|
+
needs: [depth, api, web, e2e, engineering-excellence]
|
|
960
860
|
if: ${{ always() && needs.depth.outputs.full == 'true' }}
|
|
961
861
|
runs-on: ${{ vars.CI_RUNNER || 'agents-arc' }}
|
|
962
862
|
timeout-minutes: 5
|
|
@@ -986,7 +886,7 @@ jobs:
|
|
|
986
886
|
# and the router's maintenance run merges Dependabot updates instead of the train.
|
|
987
887
|
dispatch-merge-gate:
|
|
988
888
|
name: Dispatch merge gate
|
|
989
|
-
needs: [api,
|
|
889
|
+
needs: [api, web, secret-scan, deps-and-iac-scan, sast-scan, sbom, engineering-excellence, platform-conformance, full-ci]
|
|
990
890
|
if: >
|
|
991
891
|
always() &&
|
|
992
892
|
vars.AGENTIC_WORKFLOWS_DISABLED == '' &&
|
|
@@ -1004,7 +904,7 @@ jobs:
|
|
|
1004
904
|
- name: Hand this run's verdict to the merge train after CI settles
|
|
1005
905
|
env:
|
|
1006
906
|
GH_TOKEN: ${{ github.token }}
|
|
1007
|
-
# The jobs the apps judged before this template
|
|
907
|
+
# The jobs the apps judged before this template.
|
|
1008
908
|
RESULTS: >-
|
|
1009
909
|
${{ needs.api.result }} ${{ needs.web.result }} ${{ needs.secret-scan.result }}
|
|
1010
910
|
${{ needs.deps-and-iac-scan.result }} ${{ needs.sast-scan.result }} ${{ needs.sbom.result }}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/ci/app-ci-node-monorepo.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
# No mutation testing here, on purpose: a pull request's quality gate is the test suite, and Stryker
|
|
3
|
+
# over even one changed page took longer than the rest of CI together. Mutation runs out of band,
|
|
4
|
+
# weekly, in `Quality: Mutation testing (scheduled)` (`workflows add --template quality-mutation`).
|
|
2
5
|
name: "App: CI (Node monorepo)"
|
|
3
6
|
|
|
4
7
|
on:
|
|
@@ -27,7 +30,6 @@ env:
|
|
|
27
30
|
TRUFFLEHOG_IMAGE: "trufflesecurity/trufflehog:latest"
|
|
28
31
|
SEMGREP_IMAGE: "semgrep/semgrep:1.168.0"
|
|
29
32
|
SBOM_ARTIFACT_NAME: app-sbom.cyclonedx.json
|
|
30
|
-
MUTATION_THRESHOLD: "60"
|
|
31
33
|
|
|
32
34
|
permissions:
|
|
33
35
|
contents: read
|
|
@@ -72,35 +74,6 @@ jobs:
|
|
|
72
74
|
retention-days: 3
|
|
73
75
|
if-no-files-found: error
|
|
74
76
|
|
|
75
|
-
mutation-web:
|
|
76
|
-
name: Mutation (web, diff)
|
|
77
|
-
if: github.event_name == 'pull_request'
|
|
78
|
-
runs-on: agents-arc
|
|
79
|
-
timeout-minutes: 25
|
|
80
|
-
steps:
|
|
81
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
82
|
-
with:
|
|
83
|
-
fetch-depth: 0
|
|
84
|
-
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
|
85
|
-
with: { version: "${{ env.PNPM_VERSION }}", run_install: false }
|
|
86
|
-
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
87
|
-
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
|
|
88
|
-
- name: Detect web changes
|
|
89
|
-
id: scope
|
|
90
|
-
run: |
|
|
91
|
-
git fetch origin "$GITHUB_BASE_REF" --depth=1
|
|
92
|
-
if git diff --name-only "origin/$GITHUB_BASE_REF" HEAD | grep -qE "^apps/web/"; then
|
|
93
|
-
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
94
|
-
else
|
|
95
|
-
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
96
|
-
echo "No apps/web changes in this PR; skipping web mutation."
|
|
97
|
-
fi
|
|
98
|
-
- run: pnpm install --frozen-lockfile
|
|
99
|
-
if: steps.scope.outputs.run == 'true'
|
|
100
|
-
- name: StrykerJS (changed files only)
|
|
101
|
-
if: steps.scope.outputs.run == 'true'
|
|
102
|
-
run: pnpm --filter "$WEB_PACKAGE" exec stryker run --since="origin/$GITHUB_BASE_REF"
|
|
103
|
-
|
|
104
77
|
e2e:
|
|
105
78
|
name: E2E (Playwright)
|
|
106
79
|
if: github.event_name != 'schedule' && (github.event_name != 'pull_request' || vars.MERGE_QUEUE != 'true')
|
|
@@ -263,7 +236,7 @@ jobs:
|
|
|
263
236
|
# and the router's maintenance run merges Dependabot updates instead of the train.
|
|
264
237
|
dispatch-merge-train:
|
|
265
238
|
name: Dispatch merge train
|
|
266
|
-
needs: [lint, web,
|
|
239
|
+
needs: [lint, web, e2e, desktop, mobile]
|
|
267
240
|
if: >
|
|
268
241
|
always() &&
|
|
269
242
|
github.event_name == 'pull_request' &&
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/ci/quality-mutation.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: "Quality: Mutation testing (scheduled)"
|
|
3
|
+
|
|
4
|
+
# Out-of-band mutation run, the shape PlainConceptsPlatform/Atlas runs in
|
|
5
|
+
# .github/workflows/quality-mutation.yml. Deliberately NOT on pull_request: the per-change quality
|
|
6
|
+
# gate is the coverage floor in App: CI, which stays fast. This measures something different,
|
|
7
|
+
# whether the covered lines are actually ASSERTED, and it only needs to move weekly. Stryker over
|
|
8
|
+
# one changed React page took 15 minutes on a pull request (the dry run alone reruns the whole
|
|
9
|
+
# suite), which is why App: CI no longer mutates anything.
|
|
10
|
+
#
|
|
11
|
+
# Each scope is its own job: the .NET scopes in a matrix (Stryker.NET, one project each), the web
|
|
12
|
+
# scope on its own (StrykerJS needs Node, not .NET). Each is gated on its covered-code floor from
|
|
13
|
+
# quality.config.json by scripts/check-mutation.mjs, and a full run on main publishes the
|
|
14
|
+
# engineering-excellence-mutation facet. Both scripts come with the quality-gates template.
|
|
15
|
+
#
|
|
16
|
+
# Everything that differs between apps is a top-level `env:` value below, a floor in
|
|
17
|
+
# quality.config.json, or the app's own Stryker config files.
|
|
18
|
+
|
|
19
|
+
on:
|
|
20
|
+
schedule:
|
|
21
|
+
# Spread the apps over the shared fleet: give each repository its own minute.
|
|
22
|
+
- cron: "0 4 * * 6" # Sat 04:00 UTC
|
|
23
|
+
workflow_dispatch:
|
|
24
|
+
inputs:
|
|
25
|
+
scope:
|
|
26
|
+
description: "Which scope to run: all, web, or one of DOTNET_SCOPES"
|
|
27
|
+
required: false
|
|
28
|
+
default: all
|
|
29
|
+
type: string
|
|
30
|
+
|
|
31
|
+
permissions:
|
|
32
|
+
contents: read
|
|
33
|
+
|
|
34
|
+
concurrency:
|
|
35
|
+
group: quality-mutation
|
|
36
|
+
cancel-in-progress: true
|
|
37
|
+
|
|
38
|
+
env:
|
|
39
|
+
DOTNET_VERSION: "10.0.x"
|
|
40
|
+
NODE_VERSION: "22"
|
|
41
|
+
API_WORKING_DIRECTORY: apps/api
|
|
42
|
+
DOTNET_SOLUTION: App.slnx
|
|
43
|
+
# Space-separated scope=project-directory pairs, relative to API_WORKING_DIRECTORY. Each scope
|
|
44
|
+
# mutates one project with the config at stryker/<scope>.json. Empty runs no .NET scope.
|
|
45
|
+
DOTNET_SCOPES: "domain=src/App.Domain"
|
|
46
|
+
# The web scope's working directory and StrykerJS config. An empty WEB_DIRECTORY runs no web scope.
|
|
47
|
+
WEB_DIRECTORY: apps/web
|
|
48
|
+
WEB_STRYKER_CONFIG: stryker.config.json
|
|
49
|
+
NPM_REGISTRY_URL: "//npm.pkg.github.com/"
|
|
50
|
+
|
|
51
|
+
jobs:
|
|
52
|
+
# A matrix context is not available to a job-level `if`, so the workflow_dispatch selection is
|
|
53
|
+
# resolved here and fed in via fromJSON.
|
|
54
|
+
plan:
|
|
55
|
+
name: Plan scopes
|
|
56
|
+
runs-on: ${{ vars.CI_RUNNER || 'agents-arc' }}
|
|
57
|
+
timeout-minutes: 5
|
|
58
|
+
outputs:
|
|
59
|
+
scopes: ${{ steps.pick.outputs.scopes }}
|
|
60
|
+
web: ${{ steps.pick.outputs.web }}
|
|
61
|
+
steps:
|
|
62
|
+
- id: pick
|
|
63
|
+
env:
|
|
64
|
+
SCOPE: ${{ inputs.scope || 'all' }}
|
|
65
|
+
run: |
|
|
66
|
+
set -euo pipefail
|
|
67
|
+
known=""
|
|
68
|
+
for pair in $DOTNET_SCOPES; do known="$known ${pair%%=*}"; done
|
|
69
|
+
to_json() { printf '%s\n' "$@" | sed '/^$/d' | jq -R . | jq -cs .; }
|
|
70
|
+
# `web` is a separate flag rather than a matrix entry: that job needs Node, not .NET, and
|
|
71
|
+
# shares none of the setup the backend scopes do.
|
|
72
|
+
# shellcheck disable=SC2086 # $known is a list of scope names.
|
|
73
|
+
case "$SCOPE" in
|
|
74
|
+
all) SCOPES=$(to_json $known); WEB=$([ -n "$WEB_DIRECTORY" ] && echo true || echo false) ;;
|
|
75
|
+
web) SCOPES='[]'; WEB=true ;;
|
|
76
|
+
*)
|
|
77
|
+
grep -qw -- "$SCOPE" <<<"$known" || { echo "unknown scope $SCOPE (known:$known web)" >&2; exit 1; }
|
|
78
|
+
SCOPES=$(to_json "$SCOPE"); WEB=false ;;
|
|
79
|
+
esac
|
|
80
|
+
echo "scopes=$SCOPES" >> "$GITHUB_OUTPUT"
|
|
81
|
+
echo "web=$WEB" >> "$GITHUB_OUTPUT"
|
|
82
|
+
echo "Scopes: $SCOPES, web: $WEB"
|
|
83
|
+
|
|
84
|
+
mutate:
|
|
85
|
+
name: Mutants (${{ matrix.scope }})
|
|
86
|
+
needs: plan
|
|
87
|
+
if: ${{ needs.plan.outputs.scopes != '[]' }}
|
|
88
|
+
runs-on: ${{ vars.CI_RUNNER || 'agents-arc' }}
|
|
89
|
+
# Atlas measured ~3.5 min per scope locally, but its application scope takes ~90 min on the
|
|
90
|
+
# runner and was cancelled at 90, which left the published report without it. 150 keeps
|
|
91
|
+
# headroom while still stopping a runaway job.
|
|
92
|
+
timeout-minutes: 150
|
|
93
|
+
strategy:
|
|
94
|
+
fail-fast: false
|
|
95
|
+
matrix:
|
|
96
|
+
scope: ${{ fromJSON(needs.plan.outputs.scopes) }}
|
|
97
|
+
defaults:
|
|
98
|
+
run:
|
|
99
|
+
working-directory: ${{ env.API_WORKING_DIRECTORY }}
|
|
100
|
+
steps:
|
|
101
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
102
|
+
# Without DOTNET_INSTALL_DIR setup-dotnet installs into /usr/share/dotnet, which the runner
|
|
103
|
+
# user does not own; GitHub-hosted runners keep the default.
|
|
104
|
+
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
|
|
105
|
+
env:
|
|
106
|
+
DOTNET_INSTALL_DIR: ${{ runner.environment == 'self-hosted' && format('{0}/dotnet', runner.tool_cache) || '' }}
|
|
107
|
+
with:
|
|
108
|
+
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
109
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
110
|
+
with:
|
|
111
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
112
|
+
|
|
113
|
+
# RESTORE-ONLY, and deliberately no build cache. Stryker does not sandbox: it swaps mutated
|
|
114
|
+
# assemblies straight into bin/Release. If this job ever saved a build cache App: CI restores,
|
|
115
|
+
# PR CI would test MUTATED binaries and still report green. Do not "optimise" this into one.
|
|
116
|
+
- name: Restore .NET tools
|
|
117
|
+
run: dotnet tool restore
|
|
118
|
+
- name: Restore solution
|
|
119
|
+
run: dotnet restore "$DOTNET_SOLUTION"
|
|
120
|
+
|
|
121
|
+
# Run from the mutated project's own directory, with no `solution` key in the config.
|
|
122
|
+
# Solution mode builds via --property:Platform="Any CPU"; the space splits the argument and
|
|
123
|
+
# MSBuild fails with MSB1008 before a single mutant is generated (Atlas#1531). Per-project
|
|
124
|
+
# mode uses Platform=AnyCPU and works.
|
|
125
|
+
- name: Run Stryker
|
|
126
|
+
env:
|
|
127
|
+
SCOPE: ${{ matrix.scope }}
|
|
128
|
+
run: |
|
|
129
|
+
set -euo pipefail
|
|
130
|
+
export PATH="$PATH:$HOME/.dotnet/tools"
|
|
131
|
+
PROJECT_DIR=""
|
|
132
|
+
for pair in $DOTNET_SCOPES; do
|
|
133
|
+
[ "${pair%%=*}" = "$SCOPE" ] && PROJECT_DIR="${pair#*=}"
|
|
134
|
+
done
|
|
135
|
+
[ -n "$PROJECT_DIR" ] || { echo "unknown scope $SCOPE" >&2; exit 1; }
|
|
136
|
+
root="$GITHUB_WORKSPACE/$API_WORKING_DIRECTORY"
|
|
137
|
+
cd "$PROJECT_DIR"
|
|
138
|
+
dotnet stryker \
|
|
139
|
+
--config-file "$root/stryker/$SCOPE.json" \
|
|
140
|
+
--output "$root/StrykerOutput/$SCOPE" \
|
|
141
|
+
| tee "$root/stryker-$SCOPE.log"
|
|
142
|
+
|
|
143
|
+
# perTest is the difference between minutes and hours: without it every mutant reruns the
|
|
144
|
+
# whole suite. The timeout is what actually stops that from burning a runner, so this stays a
|
|
145
|
+
# warning; its job is to say WHY, in the run where it happened. Stryker prints the mode to
|
|
146
|
+
# stdout and writes no log file, hence the tee above.
|
|
147
|
+
- name: Assert coverage analysis stayed perTest
|
|
148
|
+
if: always()
|
|
149
|
+
env:
|
|
150
|
+
SCOPE: ${{ matrix.scope }}
|
|
151
|
+
run: |
|
|
152
|
+
if ! grep -q "CoverageBasedTest" "stryker-$SCOPE.log" 2>/dev/null; then
|
|
153
|
+
echo "::warning::coverage-analysis may have degraded from perTest for $SCOPE"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# `--output <dir>` writes <dir>/reports/ directly, with no timestamped subdirectory.
|
|
157
|
+
- name: Upload mutation report
|
|
158
|
+
if: always()
|
|
159
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
160
|
+
with:
|
|
161
|
+
name: mutation-${{ matrix.scope }}
|
|
162
|
+
path: ${{ env.API_WORKING_DIRECTORY }}/StrykerOutput/${{ matrix.scope }}/reports/
|
|
163
|
+
if-no-files-found: error
|
|
164
|
+
retention-days: 30
|
|
165
|
+
|
|
166
|
+
# A weekly cron nobody reads is how mutation testing dies everywhere, so the scores go into
|
|
167
|
+
# the run summary whether the gate passes or fails.
|
|
168
|
+
#
|
|
169
|
+
# `set -o pipefail` is what makes this a gate at all. The Actions default shell is `bash -e`,
|
|
170
|
+
# where the exit status of a pipeline is its LAST command, so `node … | tee` reports tee's
|
|
171
|
+
# success and the step passes however low the score is. Do not remove it, and do not drop the
|
|
172
|
+
# `tee`: without pipefail the two cancel out silently.
|
|
173
|
+
- name: Mutation score floor
|
|
174
|
+
if: always()
|
|
175
|
+
working-directory: ${{ github.workspace }}
|
|
176
|
+
env:
|
|
177
|
+
SCOPE: ${{ matrix.scope }}
|
|
178
|
+
run: |
|
|
179
|
+
set -euo pipefail
|
|
180
|
+
node scripts/check-mutation.mjs "$SCOPE" \
|
|
181
|
+
"$API_WORKING_DIRECTORY/StrykerOutput/$SCOPE/reports/mutation-report.json" \
|
|
182
|
+
| tee -a "$GITHUB_STEP_SUMMARY"
|
|
183
|
+
|
|
184
|
+
mutate-web:
|
|
185
|
+
name: Mutants (web)
|
|
186
|
+
needs: plan
|
|
187
|
+
if: ${{ needs.plan.outputs.web == 'true' }}
|
|
188
|
+
runs-on: ${{ vars.CI_RUNNER || 'agents-arc' }}
|
|
189
|
+
# Atlas: 10 min locally over its allowlist but >45 on a 4-vCPU runner. jsdom rendering is the
|
|
190
|
+
# bottleneck and it does not shrink the way the .NET scopes do. Its first run was killed at 45.
|
|
191
|
+
timeout-minutes: 90
|
|
192
|
+
defaults:
|
|
193
|
+
run:
|
|
194
|
+
working-directory: ${{ env.WEB_DIRECTORY }}
|
|
195
|
+
steps:
|
|
196
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
197
|
+
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
|
198
|
+
with:
|
|
199
|
+
run_install: false
|
|
200
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
201
|
+
with:
|
|
202
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
203
|
+
cache: pnpm
|
|
204
|
+
cache-dependency-path: pnpm-lock.yaml
|
|
205
|
+
- name: Configure optional private registry authentication
|
|
206
|
+
working-directory: ${{ github.workspace }}
|
|
207
|
+
env:
|
|
208
|
+
NPM_REGISTRY_TOKEN: ${{ secrets.NPM_REGISTRY_TOKEN }}
|
|
209
|
+
run: |
|
|
210
|
+
if [ -n "$NPM_REGISTRY_TOKEN" ]; then
|
|
211
|
+
echo "${NPM_REGISTRY_URL}:_authToken=$NPM_REGISTRY_TOKEN" >> .npmrc
|
|
212
|
+
fi
|
|
213
|
+
- name: Install workspace
|
|
214
|
+
working-directory: ${{ github.workspace }}
|
|
215
|
+
run: pnpm install --frozen-lockfile
|
|
216
|
+
- name: Run Stryker
|
|
217
|
+
run: pnpm exec stryker run "$WEB_STRYKER_CONFIG"
|
|
218
|
+
- name: Upload mutation report
|
|
219
|
+
if: always()
|
|
220
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
221
|
+
with:
|
|
222
|
+
name: mutation-web
|
|
223
|
+
path: ${{ env.WEB_DIRECTORY }}/reports/mutation
|
|
224
|
+
if-no-files-found: error
|
|
225
|
+
retention-days: 30
|
|
226
|
+
# See the note on the .NET floor step: without pipefail this reports tee's exit status and
|
|
227
|
+
# gates nothing.
|
|
228
|
+
- name: Mutation score floor
|
|
229
|
+
if: always()
|
|
230
|
+
working-directory: ${{ github.workspace }}
|
|
231
|
+
run: |
|
|
232
|
+
set -euo pipefail
|
|
233
|
+
node scripts/check-mutation.mjs web "$WEB_DIRECTORY/reports/mutation/mutation-report.json" \
|
|
234
|
+
| tee -a "$GITHUB_STEP_SUMMARY"
|
|
235
|
+
|
|
236
|
+
# Folds every scope's report into the `engineering-excellence-mutation` artifact Project-Health
|
|
237
|
+
# collects, with the per-module breakdown. Runs when a floor failed too: a drop is the week most
|
|
238
|
+
# worth recording. Uploads only for a full run on main: a single-scope dispatch would read as a
|
|
239
|
+
# partial week.
|
|
240
|
+
engineering-excellence:
|
|
241
|
+
name: Engineering Excellence (mutation)
|
|
242
|
+
needs: [mutate, mutate-web]
|
|
243
|
+
if: ${{ !cancelled() && (needs.mutate.result != 'skipped' || needs.mutate-web.result != 'skipped') }}
|
|
244
|
+
runs-on: ${{ vars.CI_RUNNER || 'agents-arc' }}
|
|
245
|
+
timeout-minutes: 10
|
|
246
|
+
steps:
|
|
247
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
248
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
249
|
+
with:
|
|
250
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
251
|
+
# One subdirectory per artifact: reports/mutation-<scope>/mutation-report.json.
|
|
252
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
253
|
+
with:
|
|
254
|
+
pattern: mutation-*
|
|
255
|
+
path: reports
|
|
256
|
+
- name: Build facet
|
|
257
|
+
run: |
|
|
258
|
+
set -euo pipefail
|
|
259
|
+
node scripts/engineering-excellence.mjs mutation --out mutation.json --reports reports \
|
|
260
|
+
| tee -a "$GITHUB_STEP_SUMMARY"
|
|
261
|
+
- name: Upload mutation facet
|
|
262
|
+
if: ${{ github.ref == 'refs/heads/main' && (inputs.scope || 'all') == 'all' }}
|
|
263
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
264
|
+
with:
|
|
265
|
+
name: engineering-excellence-mutation
|
|
266
|
+
path: mutation.json
|
|
267
|
+
if-no-files-found: error
|
|
268
|
+
retention-days: 90
|
|
@@ -845,10 +845,19 @@ timeout-minutes: 180
|
|
|
845
845
|
the code you changed (web: `pnpm exec vitest related --run <changed files>`; .NET:
|
|
846
846
|
`dotnet test <the test project> --filter <the test classes>`). Do not run integration,
|
|
847
847
|
end-to-end, mutation or coverage suites here, and do not run the whole test suite to be safe:
|
|
848
|
-
the repository's CI runs
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
848
|
+
the repository's CI runs them, on a runner that has the database and the browsers this
|
|
849
|
+
sandbox lacks. The verification commands above are the repository's quick check for that
|
|
850
|
+
reason. A full suite in the agent is the slowest part of a run and repeats what CI does
|
|
851
|
+
anyway.
|
|
852
|
+
|
|
853
|
+
**Not running the end-to-end tests does not make them someone else's.** When your change
|
|
854
|
+
moves, renames or removes something a person clicks or reads (a button, a label, a heading, a
|
|
855
|
+
route, a table), or changes what happens after a click, search the end-to-end tests for it
|
|
856
|
+
(their selectors, the text they assert, the routes they open) and update every test that
|
|
857
|
+
uses it in this pull request. Do this even when the repository runs those tests only at night
|
|
858
|
+
or on a schedule: then nothing on the pull request fails, and the suite breaks the morning
|
|
859
|
+
after the merge with no author attached. Pliny-Bot #1506 was ten journeys broken that way by
|
|
860
|
+
Bot Studio changes that merged without their end-to-end tests.
|
|
852
861
|
|
|
853
862
|
**Run them once, on the final tree.** They are slow: Pliny-Bot's `pnpm verify` is about
|
|
854
863
|
twenty minutes of work on this runner. `pc-plan-apply` runs them in its verify step, and
|