@plainconceptsplatform/workflows 0.19.2 → 0.20.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/catalog-installation.js +12 -1
- package/dist/index.js +0 -0
- package/dist/stack-defaults.js +16 -16
- package/dist/worker-env.js +14 -0
- package/loops/actions/add-issue-labels/action.yml +50 -50
- package/loops/actions/agent-output.cjs +17 -17
- package/loops/actions/apply-agent-bundle/action.yml +24 -24
- package/loops/actions/apply-agent-comments/action.yml +42 -42
- package/loops/actions/apply-agent-labels/action.yml +55 -55
- package/loops/actions/apply-agent-output/action.yml +108 -108
- package/loops/actions/assess-blast-radius/action.yml +148 -0
- package/loops/actions/assess-blast-radius/assess-blast-radius.sh +138 -0
- package/loops/actions/classify-route/action.yml +100 -100
- package/loops/actions/cleanup-artifacts/action.yml +91 -91
- package/loops/actions/close-agent-issues/action.yml +43 -43
- package/loops/actions/create-agent-issues/action.yml +52 -52
- package/loops/actions/create-issue-comment/action.yml +29 -29
- package/loops/actions/download-agent-output/action.yml +53 -53
- package/loops/actions/housekeeping/action.yml +55 -2
- package/loops/actions/link-pr-to-issue/action.yml +40 -40
- package/loops/actions/list-open-issues/action.yml +33 -33
- package/loops/actions/load-issue-context/action.yml +45 -45
- package/loops/actions/merge-agent-pr/action.yml +49 -49
- package/loops/actions/push-agent-branch/action.yml +45 -45
- package/loops/actions/remove-issue-labels/action.yml +37 -37
- package/loops/actions/update-agent-issues/action.yml +58 -58
- package/loops/actions/validate-merge-gate-output/action.yml +62 -40
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +147 -31
- package/loops/actions/validate-refine-output/action.yml +48 -44
- package/loops/actions/validate-refine-output/validate-refine-output.sh +15 -4
- package/loops/actions/validate-review-output/action.yml +35 -35
- package/loops/actions/validate-triage-output/action.yml +36 -36
- package/loops/actions/verify-composite-actions/action.yml +9 -9
- package/loops/actions/verify-refine-output/action.yml +9 -9
- package/loops/actions/verify-refine-output/verify-refine-output.sh +6 -1
- package/loops/actions/verify-route-matrix/action.yml +9 -9
- package/loops/actions/verify-route-matrix/verify-gate-metrics.mjs +51 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +329 -29
- package/loops/scripts/compile-agent-workflows.mjs +331 -331
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/ci/app-ci-dotnet-next.yml +330 -330
- package/loops/templates/ci/app-ci-node-monorepo.yml +260 -260
- package/loops/templates/issues/bug_report.yml +109 -109
- package/loops/templates/issues/feature_request.yml +75 -75
- package/loops/templates/opencode/opencode.ci.json +55 -49
- package/loops/templates/opencode/opencode.ci.json.md +59 -49
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-merge-gate.md +367 -148
- package/loops/workflows/agent-refine.md +60 -16
- package/loops/workflows/authorize-bot-work.yml +105 -105
- package/loops/workflows/shared/opencode-ci.md +206 -206
- package/loops/workflows/shared/platform-defaults.md +19 -19
- package/package.json +12 -11
|
@@ -1,330 +1,330 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/ci/app-ci-dotnet-next.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
-
name: "App: CI"
|
|
3
|
-
|
|
4
|
-
on:
|
|
5
|
-
pull_request:
|
|
6
|
-
schedule:
|
|
7
|
-
- cron: "0 6 * * 1"
|
|
8
|
-
workflow_dispatch:
|
|
9
|
-
workflow_call:
|
|
10
|
-
secrets:
|
|
11
|
-
NPM_REGISTRY_TOKEN:
|
|
12
|
-
required: false
|
|
13
|
-
|
|
14
|
-
env:
|
|
15
|
-
DOTNET_VERSION: "10.0.x"
|
|
16
|
-
NODE_VERSION: "22"
|
|
17
|
-
PNPM_VERSION: "10"
|
|
18
|
-
API_WORKING_DIRECTORY: apps/api
|
|
19
|
-
DOTNET_SOLUTION: App.slnx
|
|
20
|
-
UNIT_TEST_PROJECT: tests/App.UnitTests/App.UnitTests.csproj
|
|
21
|
-
PARITY_TEST_PROJECT: tests/App.ParityTests/App.ParityTests.csproj
|
|
22
|
-
INTEGRATION_TEST_PROJECT: tests/App.IntegrationTests/App.IntegrationTests.csproj
|
|
23
|
-
# Per-PR mutation gate. Stryker mutates only files changed since the PR base (--since),
|
|
24
|
-
# so a green unit suite that never actually pins behaviour fails here instead of shipping.
|
|
25
|
-
# dotnet-stryker is pinned in the api tools manifest (.config/dotnet-tools.json / dotnet-tools.json).
|
|
26
|
-
MUTATION_TARGET_PROJECT: src/App.Domain/App.Domain.csproj
|
|
27
|
-
MUTATION_TEST_PROJECT: tests/App.UnitTests/App.UnitTests.csproj
|
|
28
|
-
MUTATION_THRESHOLD: "60"
|
|
29
|
-
COVERAGE_SETTINGS: coverage.runsettings
|
|
30
|
-
COVERAGE_CHECK_COMMAND: node ../../scripts/check-coverage.mjs TestResults/merged/Cobertura.xml
|
|
31
|
-
MIGRATION_PROJECT: src/App.Infrastructure/App.Infrastructure.csproj
|
|
32
|
-
MIGRATION_STARTUP_PROJECT: src/App.Api/App.Api.csproj
|
|
33
|
-
SEED_PROJECT: src/App.Seed
|
|
34
|
-
PRIMARY_DATABASE: app
|
|
35
|
-
DATA_LAYER_DATABASE: numa_dl
|
|
36
|
-
# Derived per run rather than written down. The container is ephemeral, bound to a random
|
|
37
|
-
# host port and reachable only from this job, so the value protects nothing, but a literal
|
|
38
|
-
# password in a config file is a policy finding wherever it sits, and this file is public.
|
|
39
|
-
# It has to be a workflow-level expression: service containers receive their environment
|
|
40
|
-
# before any step runs, so a value generated in a step would arrive too late.
|
|
41
|
-
#
|
|
42
|
-
# The same expression is repeated inside the service's `options` above rather than read
|
|
43
|
-
# from here, because the `env` context is not available in `jobs.<id>.services` — only
|
|
44
|
-
# inside that service's own `env:`. Written as ${{ env.SQL_SA_PASSWORD }} there, the health
|
|
45
|
-
# check probes with an empty password and the service never reports healthy.
|
|
46
|
-
SQL_SA_PASSWORD: "Aa1!${{ github.run_id }}"
|
|
47
|
-
WEB_PACKAGE: "@app/web"
|
|
48
|
-
NPM_REGISTRY_URL: "//npm.pkg.github.com/"
|
|
49
|
-
TRUFFLEHOG_IMAGE: "trufflesecurity/trufflehog:latest"
|
|
50
|
-
SEMGREP_IMAGE: "semgrep/semgrep:1.168.0"
|
|
51
|
-
SBOM_ARTIFACT_NAME: app-sbom.cyclonedx.json
|
|
52
|
-
MIGRATION_CONTEXT: AppDbContext
|
|
53
|
-
|
|
54
|
-
permissions:
|
|
55
|
-
contents: read
|
|
56
|
-
|
|
57
|
-
concurrency:
|
|
58
|
-
group: ci-${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
|
59
|
-
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
60
|
-
|
|
61
|
-
jobs:
|
|
62
|
-
api:
|
|
63
|
-
name: API (.NET)
|
|
64
|
-
if: github.event_name != 'schedule'
|
|
65
|
-
runs-on: agents-arc
|
|
66
|
-
timeout-minutes: 30
|
|
67
|
-
services:
|
|
68
|
-
sqlserver:
|
|
69
|
-
image: mcr.microsoft.com/mssql/server:2022-latest
|
|
70
|
-
env:
|
|
71
|
-
ACCEPT_EULA: "Y"
|
|
72
|
-
MSSQL_SA_PASSWORD: ${{ env.SQL_SA_PASSWORD }}
|
|
73
|
-
ports:
|
|
74
|
-
# Ephemeral host port. Four runners share this machine, so a fixed 1433 means the
|
|
75
|
-
# second concurrent job fails at Initialize containers with "Docker start fail with
|
|
76
|
-
# exit code 1". Steps read the assigned port from the job.services context.
|
|
77
|
-
- 1433
|
|
78
|
-
options: >-
|
|
79
|
-
--health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Aa1!${{ github.run_id }}' -Q 'SELECT 1' -C || exit 1"
|
|
80
|
-
--health-interval 10s --health-timeout 5s --health-retries 12 --health-start-period 30s
|
|
81
|
-
defaults:
|
|
82
|
-
run:
|
|
83
|
-
working-directory: ${{ env.API_WORKING_DIRECTORY }}
|
|
84
|
-
steps:
|
|
85
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
86
|
-
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
|
|
87
|
-
with:
|
|
88
|
-
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
89
|
-
- name: Restore tools, solution, and build
|
|
90
|
-
run: |
|
|
91
|
-
dotnet tool restore
|
|
92
|
-
dotnet restore "$DOTNET_SOLUTION"
|
|
93
|
-
dotnet build "$DOTNET_SOLUTION" --no-restore -c Release
|
|
94
|
-
- name: Unit tests with coverage
|
|
95
|
-
run: dotnet test "$UNIT_TEST_PROJECT" --no-build -c Release --collect:"XPlat Code Coverage" --settings "$COVERAGE_SETTINGS" --results-directory TestResults
|
|
96
|
-
- name: Parity tests
|
|
97
|
-
run: dotnet test "$PARITY_TEST_PROJECT" --no-build -c Release
|
|
98
|
-
- name: Create test databases
|
|
99
|
-
run: |
|
|
100
|
-
for database in "$PRIMARY_DATABASE" "$DATA_LAYER_DATABASE"; do
|
|
101
|
-
docker exec ${{ job.services.sqlserver.id }} /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$SQL_SA_PASSWORD" -C -Q "IF DB_ID('$database') IS NULL CREATE DATABASE [$database];"
|
|
102
|
-
done
|
|
103
|
-
- name: Apply migrations and seed test data
|
|
104
|
-
env:
|
|
105
|
-
ConnectionStrings__App: Server=localhost,${{ job.services.sqlserver.ports['1433'] }};Database=${{ env.PRIMARY_DATABASE }};User Id=sa;Password=${{ env.SQL_SA_PASSWORD }};TrustServerCertificate=true
|
|
106
|
-
DataLayer__ConnectionString: Server=localhost,${{ job.services.sqlserver.ports['1433'] }};Database=${{ env.DATA_LAYER_DATABASE }};User Id=sa;Password=${{ env.SQL_SA_PASSWORD }};TrustServerCertificate=true
|
|
107
|
-
DATALAYER_SEED: "true"
|
|
108
|
-
run: |
|
|
109
|
-
dotnet ef database update -p "$MIGRATION_PROJECT" -s "$MIGRATION_STARTUP_PROJECT" --context "$MIGRATION_CONTEXT"
|
|
110
|
-
dotnet run --project "$SEED_PROJECT" --no-build -c Release
|
|
111
|
-
- name: Integration tests with coverage
|
|
112
|
-
env:
|
|
113
|
-
ConnectionStrings__App: Server=localhost,${{ job.services.sqlserver.ports['1433'] }};Database=${{ env.PRIMARY_DATABASE }};User Id=sa;Password=${{ env.SQL_SA_PASSWORD }};TrustServerCertificate=true
|
|
114
|
-
DataLayer__ConnectionString: Server=localhost,${{ job.services.sqlserver.ports['1433'] }};Database=${{ env.DATA_LAYER_DATABASE }};User Id=sa;Password=${{ env.SQL_SA_PASSWORD }};TrustServerCertificate=true
|
|
115
|
-
run: dotnet test "$INTEGRATION_TEST_PROJECT" --no-build -c Release --collect:"XPlat Code Coverage" --settings "$COVERAGE_SETTINGS" --results-directory TestResults
|
|
116
|
-
- name: Enforce merged coverage floor
|
|
117
|
-
run: |
|
|
118
|
-
dotnet tool install --global dotnet-reportgenerator-globaltool
|
|
119
|
-
export PATH="$PATH:$HOME/.dotnet/tools" # ephemeral ARC runners do not have dotnet tools on PATH
|
|
120
|
-
reportgenerator -reports:"TestResults/**/coverage.cobertura.xml" -targetdir:TestResults/merged -reporttypes:Cobertura
|
|
121
|
-
bash -c "$COVERAGE_CHECK_COMMAND"
|
|
122
|
-
|
|
123
|
-
mutation-api:
|
|
124
|
-
name: Mutation (.NET, diff)
|
|
125
|
-
if: github.event_name == 'pull_request'
|
|
126
|
-
runs-on: agents-arc
|
|
127
|
-
timeout-minutes: 25
|
|
128
|
-
defaults:
|
|
129
|
-
run:
|
|
130
|
-
working-directory: ${{ env.API_WORKING_DIRECTORY }}
|
|
131
|
-
steps:
|
|
132
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
133
|
-
with:
|
|
134
|
-
# Stryker --since diffs against the PR base; it needs history, not a shallow tip.
|
|
135
|
-
fetch-depth: 0
|
|
136
|
-
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
|
|
137
|
-
with:
|
|
138
|
-
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
139
|
-
- name: Detect backend changes
|
|
140
|
-
id: scope
|
|
141
|
-
working-directory: ${{ github.workspace }}
|
|
142
|
-
run: |
|
|
143
|
-
git fetch origin "$GITHUB_BASE_REF" --depth=1
|
|
144
|
-
if git diff --name-only "origin/$GITHUB_BASE_REF" HEAD | grep -qE "^${API_WORKING_DIRECTORY}/"; then
|
|
145
|
-
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
146
|
-
else
|
|
147
|
-
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
148
|
-
echo "No ${API_WORKING_DIRECTORY} changes in this PR; skipping .NET mutation."
|
|
149
|
-
fi
|
|
150
|
-
- name: Restore tools and build
|
|
151
|
-
if: steps.scope.outputs.run == 'true'
|
|
152
|
-
run: |
|
|
153
|
-
dotnet tool restore
|
|
154
|
-
dotnet build "$DOTNET_SOLUTION" -c Release
|
|
155
|
-
- name: Stryker.NET (changed files only)
|
|
156
|
-
if: steps.scope.outputs.run == 'true'
|
|
157
|
-
run: |
|
|
158
|
-
export PATH="$PATH:$HOME/.dotnet/tools" # ephemeral ARC runners do not have dotnet tools on PATH
|
|
159
|
-
dotnet stryker \
|
|
160
|
-
--project "$MUTATION_TARGET_PROJECT" \
|
|
161
|
-
--test-projects "$MUTATION_TEST_PROJECT" \
|
|
162
|
-
--since:"origin/$GITHUB_BASE_REF" \
|
|
163
|
-
--concurrency 2 \
|
|
164
|
-
--threshold-high "$MUTATION_THRESHOLD" \
|
|
165
|
-
--threshold-low "$MUTATION_THRESHOLD" \
|
|
166
|
-
--break-at "$MUTATION_THRESHOLD" \
|
|
167
|
-
--reporter progress --reporter json
|
|
168
|
-
|
|
169
|
-
web:
|
|
170
|
-
name: Web (Next.js)
|
|
171
|
-
if: github.event_name != 'schedule'
|
|
172
|
-
runs-on: agents-arc
|
|
173
|
-
timeout-minutes: 15
|
|
174
|
-
steps:
|
|
175
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
176
|
-
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
|
|
177
|
-
with:
|
|
178
|
-
version: ${{ env.PNPM_VERSION }}
|
|
179
|
-
run_install: false
|
|
180
|
-
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
181
|
-
with:
|
|
182
|
-
node-version: ${{ env.NODE_VERSION }}
|
|
183
|
-
cache: pnpm
|
|
184
|
-
cache-dependency-path: pnpm-lock.yaml
|
|
185
|
-
- name: Configure optional private registry authentication
|
|
186
|
-
env:
|
|
187
|
-
NPM_REGISTRY_TOKEN: ${{ secrets.NPM_REGISTRY_TOKEN }}
|
|
188
|
-
run: |
|
|
189
|
-
if [ -n "$NPM_REGISTRY_TOKEN" ]; then
|
|
190
|
-
echo "${NPM_REGISTRY_URL}:_authToken=$NPM_REGISTRY_TOKEN" >> .npmrc
|
|
191
|
-
fi
|
|
192
|
-
- run: pnpm install --frozen-lockfile
|
|
193
|
-
- name: Lint, test, verify boundaries, and build
|
|
194
|
-
run: |
|
|
195
|
-
pnpm --filter "$WEB_PACKAGE" run lint
|
|
196
|
-
pnpm --filter "$WEB_PACKAGE" run test
|
|
197
|
-
pnpm --filter "$WEB_PACKAGE" run lint:fsd
|
|
198
|
-
pnpm --filter "$WEB_PACKAGE" run build
|
|
199
|
-
|
|
200
|
-
mutation-web:
|
|
201
|
-
name: Mutation (web, diff)
|
|
202
|
-
if: github.event_name == 'pull_request'
|
|
203
|
-
runs-on: agents-arc
|
|
204
|
-
timeout-minutes: 25
|
|
205
|
-
steps:
|
|
206
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
207
|
-
with:
|
|
208
|
-
fetch-depth: 0
|
|
209
|
-
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
|
|
210
|
-
with:
|
|
211
|
-
version: ${{ env.PNPM_VERSION }}
|
|
212
|
-
run_install: false
|
|
213
|
-
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
214
|
-
with:
|
|
215
|
-
node-version: ${{ env.NODE_VERSION }}
|
|
216
|
-
cache: pnpm
|
|
217
|
-
cache-dependency-path: pnpm-lock.yaml
|
|
218
|
-
- name: Detect web changes
|
|
219
|
-
id: scope
|
|
220
|
-
run: |
|
|
221
|
-
git fetch origin "$GITHUB_BASE_REF" --depth=1
|
|
222
|
-
if git diff --name-only "origin/$GITHUB_BASE_REF" HEAD | grep -qE "^apps/web/"; then
|
|
223
|
-
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
224
|
-
else
|
|
225
|
-
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
226
|
-
echo "No apps/web changes in this PR; skipping web mutation."
|
|
227
|
-
fi
|
|
228
|
-
- name: Configure optional private registry authentication
|
|
229
|
-
if: steps.scope.outputs.run == 'true'
|
|
230
|
-
env:
|
|
231
|
-
NPM_REGISTRY_TOKEN: ${{ secrets.NPM_REGISTRY_TOKEN }}
|
|
232
|
-
run: |
|
|
233
|
-
if [ -n "$NPM_REGISTRY_TOKEN" ]; then
|
|
234
|
-
echo "${NPM_REGISTRY_URL}:_authToken=$NPM_REGISTRY_TOKEN" >> .npmrc
|
|
235
|
-
fi
|
|
236
|
-
- name: Install and run StrykerJS (changed files only)
|
|
237
|
-
if: steps.scope.outputs.run == 'true'
|
|
238
|
-
run: |
|
|
239
|
-
pnpm install --frozen-lockfile
|
|
240
|
-
pnpm --filter "$WEB_PACKAGE" exec stryker run --since="origin/$GITHUB_BASE_REF"
|
|
241
|
-
|
|
242
|
-
secret-scan:
|
|
243
|
-
name: Secret scan (TruffleHog)
|
|
244
|
-
runs-on: agents-arc
|
|
245
|
-
steps:
|
|
246
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
247
|
-
- run: docker run --rm -v "${{ github.workspace }}:/repo" "$TRUFFLEHOG_IMAGE" filesystem /repo --only-verified --fail --no-update
|
|
248
|
-
|
|
249
|
-
deps-and-iac-scan:
|
|
250
|
-
name: Dependencies and IaC (Trivy)
|
|
251
|
-
runs-on: agents-arc
|
|
252
|
-
steps:
|
|
253
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
254
|
-
- uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
|
255
|
-
with:
|
|
256
|
-
scan-type: fs
|
|
257
|
-
scanners: vuln,misconfig,secret
|
|
258
|
-
severity: CRITICAL,HIGH
|
|
259
|
-
ignore-unfixed: true
|
|
260
|
-
exit-code: "1"
|
|
261
|
-
format: table
|
|
262
|
-
|
|
263
|
-
sast-scan:
|
|
264
|
-
name: SAST (Semgrep)
|
|
265
|
-
runs-on: agents-arc
|
|
266
|
-
steps:
|
|
267
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
268
|
-
- run: docker run --rm -v "${{ github.workspace }}:/src" -w /src "$SEMGREP_IMAGE" semgrep scan --error --metrics=off --oss-only --disable-version-check --config p/default --config p/security-audit --config p/secrets --config p/csharp --config p/typescript --config p/react --exclude-rule yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
|
|
269
|
-
|
|
270
|
-
sbom:
|
|
271
|
-
name: SBOM (CycloneDX)
|
|
272
|
-
runs-on: agents-arc
|
|
273
|
-
steps:
|
|
274
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
275
|
-
- uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0
|
|
276
|
-
with:
|
|
277
|
-
format: cyclonedx-json
|
|
278
|
-
artifact-name: ${{ env.SBOM_ARTIFACT_NAME }}
|
|
279
|
-
|
|
280
|
-
# A bot pull request is opened with GITHUB_TOKEN, and GitHub delivers no workflow_run event
|
|
281
|
-
# for the CI runs that follow (the run's actor is the bot). The router's CI-completion route
|
|
282
|
-
# never sees them, so this job hands the verdict over from inside CI. workflow_dispatch is
|
|
283
|
-
# one of the two events GITHUB_TOKEN may raise, so no App token is needed here, and the
|
|
284
|
-
# router treats a dispatch as trusted. Human pull requests are left to the workflow_run
|
|
285
|
-
# route, which does fire for them. The reconcile belt remains the hourly fallback.
|
|
286
|
-
#
|
|
287
|
-
# If a deploy pipeline calls this workflow with `uses:`, that caller must also grant
|
|
288
|
-
# `actions: write`. A called workflow may not request a scope its caller withheld, and the
|
|
289
|
-
# whole run fails to start with zero jobs and no annotation when it does.
|
|
290
|
-
dispatch-merge-gate:
|
|
291
|
-
name: Dispatch merge gate
|
|
292
|
-
needs: [api, mutation-api, web, mutation-web]
|
|
293
|
-
if: >
|
|
294
|
-
always() &&
|
|
295
|
-
github.event_name == 'pull_request' &&
|
|
296
|
-
github.event.pull_request.head.repo.full_name == github.repository &&
|
|
297
|
-
github.event.pull_request.draft == false &&
|
|
298
|
-
github.event.pull_request.user.type == 'Bot'
|
|
299
|
-
runs-on: agents-arc
|
|
300
|
-
timeout-minutes: 5
|
|
301
|
-
permissions:
|
|
302
|
-
contents: read
|
|
303
|
-
actions: write
|
|
304
|
-
steps:
|
|
305
|
-
- name: Dispatch the merge gate with this run's verdict
|
|
306
|
-
env:
|
|
307
|
-
GH_TOKEN: ${{ github.token }}
|
|
308
|
-
REPO: ${{ github.repository }}
|
|
309
|
-
REF: ${{ github.event.repository.default_branch }}
|
|
310
|
-
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
311
|
-
RUN_ID: ${{ github.run_id }}
|
|
312
|
-
RESULTS: ${{ join(needs.*.result, ' ') }}
|
|
313
|
-
run: |
|
|
314
|
-
set -euo pipefail
|
|
315
|
-
conclusion=success
|
|
316
|
-
for result in $RESULTS; do
|
|
317
|
-
case "$result" in
|
|
318
|
-
failure) conclusion=failure; break ;;
|
|
319
|
-
cancelled | timed_out) conclusion=cancelled ;;
|
|
320
|
-
esac
|
|
321
|
-
done
|
|
322
|
-
# A cancelled run was superseded by a newer push, whose own run dispatches.
|
|
323
|
-
if [ "$conclusion" = cancelled ]; then
|
|
324
|
-
echo "CI was cancelled; not dispatching the merge gate."
|
|
325
|
-
exit 0
|
|
326
|
-
fi
|
|
327
|
-
echo "Dispatching the merge gate for PR #$PR_NUMBER with CI $conclusion (run $RUN_ID)."
|
|
328
|
-
gh workflow run work-router.yml --repo "$REPO" --ref "$REF" \
|
|
329
|
-
-f operation=merge-gate -f pr-number="$PR_NUMBER" \
|
|
330
|
-
-f ci-conclusion="$conclusion" -f ci-run-id="$RUN_ID"
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/ci/app-ci-dotnet-next.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: "App: CI"
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
pull_request:
|
|
6
|
+
schedule:
|
|
7
|
+
- cron: "0 6 * * 1"
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
workflow_call:
|
|
10
|
+
secrets:
|
|
11
|
+
NPM_REGISTRY_TOKEN:
|
|
12
|
+
required: false
|
|
13
|
+
|
|
14
|
+
env:
|
|
15
|
+
DOTNET_VERSION: "10.0.x"
|
|
16
|
+
NODE_VERSION: "22"
|
|
17
|
+
PNPM_VERSION: "10"
|
|
18
|
+
API_WORKING_DIRECTORY: apps/api
|
|
19
|
+
DOTNET_SOLUTION: App.slnx
|
|
20
|
+
UNIT_TEST_PROJECT: tests/App.UnitTests/App.UnitTests.csproj
|
|
21
|
+
PARITY_TEST_PROJECT: tests/App.ParityTests/App.ParityTests.csproj
|
|
22
|
+
INTEGRATION_TEST_PROJECT: tests/App.IntegrationTests/App.IntegrationTests.csproj
|
|
23
|
+
# Per-PR mutation gate. Stryker mutates only files changed since the PR base (--since),
|
|
24
|
+
# so a green unit suite that never actually pins behaviour fails here instead of shipping.
|
|
25
|
+
# dotnet-stryker is pinned in the api tools manifest (.config/dotnet-tools.json / dotnet-tools.json).
|
|
26
|
+
MUTATION_TARGET_PROJECT: src/App.Domain/App.Domain.csproj
|
|
27
|
+
MUTATION_TEST_PROJECT: tests/App.UnitTests/App.UnitTests.csproj
|
|
28
|
+
MUTATION_THRESHOLD: "60"
|
|
29
|
+
COVERAGE_SETTINGS: coverage.runsettings
|
|
30
|
+
COVERAGE_CHECK_COMMAND: node ../../scripts/check-coverage.mjs TestResults/merged/Cobertura.xml
|
|
31
|
+
MIGRATION_PROJECT: src/App.Infrastructure/App.Infrastructure.csproj
|
|
32
|
+
MIGRATION_STARTUP_PROJECT: src/App.Api/App.Api.csproj
|
|
33
|
+
SEED_PROJECT: src/App.Seed
|
|
34
|
+
PRIMARY_DATABASE: app
|
|
35
|
+
DATA_LAYER_DATABASE: numa_dl
|
|
36
|
+
# Derived per run rather than written down. The container is ephemeral, bound to a random
|
|
37
|
+
# host port and reachable only from this job, so the value protects nothing, but a literal
|
|
38
|
+
# password in a config file is a policy finding wherever it sits, and this file is public.
|
|
39
|
+
# It has to be a workflow-level expression: service containers receive their environment
|
|
40
|
+
# before any step runs, so a value generated in a step would arrive too late.
|
|
41
|
+
#
|
|
42
|
+
# The same expression is repeated inside the service's `options` above rather than read
|
|
43
|
+
# from here, because the `env` context is not available in `jobs.<id>.services` — only
|
|
44
|
+
# inside that service's own `env:`. Written as ${{ env.SQL_SA_PASSWORD }} there, the health
|
|
45
|
+
# check probes with an empty password and the service never reports healthy.
|
|
46
|
+
SQL_SA_PASSWORD: "Aa1!${{ github.run_id }}"
|
|
47
|
+
WEB_PACKAGE: "@app/web"
|
|
48
|
+
NPM_REGISTRY_URL: "//npm.pkg.github.com/"
|
|
49
|
+
TRUFFLEHOG_IMAGE: "trufflesecurity/trufflehog:latest"
|
|
50
|
+
SEMGREP_IMAGE: "semgrep/semgrep:1.168.0"
|
|
51
|
+
SBOM_ARTIFACT_NAME: app-sbom.cyclonedx.json
|
|
52
|
+
MIGRATION_CONTEXT: AppDbContext
|
|
53
|
+
|
|
54
|
+
permissions:
|
|
55
|
+
contents: read
|
|
56
|
+
|
|
57
|
+
concurrency:
|
|
58
|
+
group: ci-${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
|
59
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
60
|
+
|
|
61
|
+
jobs:
|
|
62
|
+
api:
|
|
63
|
+
name: API (.NET)
|
|
64
|
+
if: github.event_name != 'schedule'
|
|
65
|
+
runs-on: agents-arc
|
|
66
|
+
timeout-minutes: 30
|
|
67
|
+
services:
|
|
68
|
+
sqlserver:
|
|
69
|
+
image: mcr.microsoft.com/mssql/server:2022-latest
|
|
70
|
+
env:
|
|
71
|
+
ACCEPT_EULA: "Y"
|
|
72
|
+
MSSQL_SA_PASSWORD: ${{ env.SQL_SA_PASSWORD }}
|
|
73
|
+
ports:
|
|
74
|
+
# Ephemeral host port. Four runners share this machine, so a fixed 1433 means the
|
|
75
|
+
# second concurrent job fails at Initialize containers with "Docker start fail with
|
|
76
|
+
# exit code 1". Steps read the assigned port from the job.services context.
|
|
77
|
+
- 1433
|
|
78
|
+
options: >-
|
|
79
|
+
--health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Aa1!${{ github.run_id }}' -Q 'SELECT 1' -C || exit 1"
|
|
80
|
+
--health-interval 10s --health-timeout 5s --health-retries 12 --health-start-period 30s
|
|
81
|
+
defaults:
|
|
82
|
+
run:
|
|
83
|
+
working-directory: ${{ env.API_WORKING_DIRECTORY }}
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
86
|
+
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
|
|
87
|
+
with:
|
|
88
|
+
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
89
|
+
- name: Restore tools, solution, and build
|
|
90
|
+
run: |
|
|
91
|
+
dotnet tool restore
|
|
92
|
+
dotnet restore "$DOTNET_SOLUTION"
|
|
93
|
+
dotnet build "$DOTNET_SOLUTION" --no-restore -c Release
|
|
94
|
+
- name: Unit tests with coverage
|
|
95
|
+
run: dotnet test "$UNIT_TEST_PROJECT" --no-build -c Release --collect:"XPlat Code Coverage" --settings "$COVERAGE_SETTINGS" --results-directory TestResults
|
|
96
|
+
- name: Parity tests
|
|
97
|
+
run: dotnet test "$PARITY_TEST_PROJECT" --no-build -c Release
|
|
98
|
+
- name: Create test databases
|
|
99
|
+
run: |
|
|
100
|
+
for database in "$PRIMARY_DATABASE" "$DATA_LAYER_DATABASE"; do
|
|
101
|
+
docker exec ${{ job.services.sqlserver.id }} /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$SQL_SA_PASSWORD" -C -Q "IF DB_ID('$database') IS NULL CREATE DATABASE [$database];"
|
|
102
|
+
done
|
|
103
|
+
- name: Apply migrations and seed test data
|
|
104
|
+
env:
|
|
105
|
+
ConnectionStrings__App: Server=localhost,${{ job.services.sqlserver.ports['1433'] }};Database=${{ env.PRIMARY_DATABASE }};User Id=sa;Password=${{ env.SQL_SA_PASSWORD }};TrustServerCertificate=true
|
|
106
|
+
DataLayer__ConnectionString: Server=localhost,${{ job.services.sqlserver.ports['1433'] }};Database=${{ env.DATA_LAYER_DATABASE }};User Id=sa;Password=${{ env.SQL_SA_PASSWORD }};TrustServerCertificate=true
|
|
107
|
+
DATALAYER_SEED: "true"
|
|
108
|
+
run: |
|
|
109
|
+
dotnet ef database update -p "$MIGRATION_PROJECT" -s "$MIGRATION_STARTUP_PROJECT" --context "$MIGRATION_CONTEXT"
|
|
110
|
+
dotnet run --project "$SEED_PROJECT" --no-build -c Release
|
|
111
|
+
- name: Integration tests with coverage
|
|
112
|
+
env:
|
|
113
|
+
ConnectionStrings__App: Server=localhost,${{ job.services.sqlserver.ports['1433'] }};Database=${{ env.PRIMARY_DATABASE }};User Id=sa;Password=${{ env.SQL_SA_PASSWORD }};TrustServerCertificate=true
|
|
114
|
+
DataLayer__ConnectionString: Server=localhost,${{ job.services.sqlserver.ports['1433'] }};Database=${{ env.DATA_LAYER_DATABASE }};User Id=sa;Password=${{ env.SQL_SA_PASSWORD }};TrustServerCertificate=true
|
|
115
|
+
run: dotnet test "$INTEGRATION_TEST_PROJECT" --no-build -c Release --collect:"XPlat Code Coverage" --settings "$COVERAGE_SETTINGS" --results-directory TestResults
|
|
116
|
+
- name: Enforce merged coverage floor
|
|
117
|
+
run: |
|
|
118
|
+
dotnet tool install --global dotnet-reportgenerator-globaltool
|
|
119
|
+
export PATH="$PATH:$HOME/.dotnet/tools" # ephemeral ARC runners do not have dotnet tools on PATH
|
|
120
|
+
reportgenerator -reports:"TestResults/**/coverage.cobertura.xml" -targetdir:TestResults/merged -reporttypes:Cobertura
|
|
121
|
+
bash -c "$COVERAGE_CHECK_COMMAND"
|
|
122
|
+
|
|
123
|
+
mutation-api:
|
|
124
|
+
name: Mutation (.NET, diff)
|
|
125
|
+
if: github.event_name == 'pull_request'
|
|
126
|
+
runs-on: agents-arc
|
|
127
|
+
timeout-minutes: 25
|
|
128
|
+
defaults:
|
|
129
|
+
run:
|
|
130
|
+
working-directory: ${{ env.API_WORKING_DIRECTORY }}
|
|
131
|
+
steps:
|
|
132
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
133
|
+
with:
|
|
134
|
+
# Stryker --since diffs against the PR base; it needs history, not a shallow tip.
|
|
135
|
+
fetch-depth: 0
|
|
136
|
+
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
|
|
137
|
+
with:
|
|
138
|
+
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
139
|
+
- name: Detect backend changes
|
|
140
|
+
id: scope
|
|
141
|
+
working-directory: ${{ github.workspace }}
|
|
142
|
+
run: |
|
|
143
|
+
git fetch origin "$GITHUB_BASE_REF" --depth=1
|
|
144
|
+
if git diff --name-only "origin/$GITHUB_BASE_REF" HEAD | grep -qE "^${API_WORKING_DIRECTORY}/"; then
|
|
145
|
+
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
146
|
+
else
|
|
147
|
+
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
148
|
+
echo "No ${API_WORKING_DIRECTORY} changes in this PR; skipping .NET mutation."
|
|
149
|
+
fi
|
|
150
|
+
- name: Restore tools and build
|
|
151
|
+
if: steps.scope.outputs.run == 'true'
|
|
152
|
+
run: |
|
|
153
|
+
dotnet tool restore
|
|
154
|
+
dotnet build "$DOTNET_SOLUTION" -c Release
|
|
155
|
+
- name: Stryker.NET (changed files only)
|
|
156
|
+
if: steps.scope.outputs.run == 'true'
|
|
157
|
+
run: |
|
|
158
|
+
export PATH="$PATH:$HOME/.dotnet/tools" # ephemeral ARC runners do not have dotnet tools on PATH
|
|
159
|
+
dotnet stryker \
|
|
160
|
+
--project "$MUTATION_TARGET_PROJECT" \
|
|
161
|
+
--test-projects "$MUTATION_TEST_PROJECT" \
|
|
162
|
+
--since:"origin/$GITHUB_BASE_REF" \
|
|
163
|
+
--concurrency 2 \
|
|
164
|
+
--threshold-high "$MUTATION_THRESHOLD" \
|
|
165
|
+
--threshold-low "$MUTATION_THRESHOLD" \
|
|
166
|
+
--break-at "$MUTATION_THRESHOLD" \
|
|
167
|
+
--reporter progress --reporter json
|
|
168
|
+
|
|
169
|
+
web:
|
|
170
|
+
name: Web (Next.js)
|
|
171
|
+
if: github.event_name != 'schedule'
|
|
172
|
+
runs-on: agents-arc
|
|
173
|
+
timeout-minutes: 15
|
|
174
|
+
steps:
|
|
175
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
176
|
+
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
|
|
177
|
+
with:
|
|
178
|
+
version: ${{ env.PNPM_VERSION }}
|
|
179
|
+
run_install: false
|
|
180
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
181
|
+
with:
|
|
182
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
183
|
+
cache: pnpm
|
|
184
|
+
cache-dependency-path: pnpm-lock.yaml
|
|
185
|
+
- name: Configure optional private registry authentication
|
|
186
|
+
env:
|
|
187
|
+
NPM_REGISTRY_TOKEN: ${{ secrets.NPM_REGISTRY_TOKEN }}
|
|
188
|
+
run: |
|
|
189
|
+
if [ -n "$NPM_REGISTRY_TOKEN" ]; then
|
|
190
|
+
echo "${NPM_REGISTRY_URL}:_authToken=$NPM_REGISTRY_TOKEN" >> .npmrc
|
|
191
|
+
fi
|
|
192
|
+
- run: pnpm install --frozen-lockfile
|
|
193
|
+
- name: Lint, test, verify boundaries, and build
|
|
194
|
+
run: |
|
|
195
|
+
pnpm --filter "$WEB_PACKAGE" run lint
|
|
196
|
+
pnpm --filter "$WEB_PACKAGE" run test
|
|
197
|
+
pnpm --filter "$WEB_PACKAGE" run lint:fsd
|
|
198
|
+
pnpm --filter "$WEB_PACKAGE" run build
|
|
199
|
+
|
|
200
|
+
mutation-web:
|
|
201
|
+
name: Mutation (web, diff)
|
|
202
|
+
if: github.event_name == 'pull_request'
|
|
203
|
+
runs-on: agents-arc
|
|
204
|
+
timeout-minutes: 25
|
|
205
|
+
steps:
|
|
206
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
207
|
+
with:
|
|
208
|
+
fetch-depth: 0
|
|
209
|
+
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
|
|
210
|
+
with:
|
|
211
|
+
version: ${{ env.PNPM_VERSION }}
|
|
212
|
+
run_install: false
|
|
213
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
214
|
+
with:
|
|
215
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
216
|
+
cache: pnpm
|
|
217
|
+
cache-dependency-path: pnpm-lock.yaml
|
|
218
|
+
- name: Detect web changes
|
|
219
|
+
id: scope
|
|
220
|
+
run: |
|
|
221
|
+
git fetch origin "$GITHUB_BASE_REF" --depth=1
|
|
222
|
+
if git diff --name-only "origin/$GITHUB_BASE_REF" HEAD | grep -qE "^apps/web/"; then
|
|
223
|
+
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
224
|
+
else
|
|
225
|
+
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
226
|
+
echo "No apps/web changes in this PR; skipping web mutation."
|
|
227
|
+
fi
|
|
228
|
+
- name: Configure optional private registry authentication
|
|
229
|
+
if: steps.scope.outputs.run == 'true'
|
|
230
|
+
env:
|
|
231
|
+
NPM_REGISTRY_TOKEN: ${{ secrets.NPM_REGISTRY_TOKEN }}
|
|
232
|
+
run: |
|
|
233
|
+
if [ -n "$NPM_REGISTRY_TOKEN" ]; then
|
|
234
|
+
echo "${NPM_REGISTRY_URL}:_authToken=$NPM_REGISTRY_TOKEN" >> .npmrc
|
|
235
|
+
fi
|
|
236
|
+
- name: Install and run StrykerJS (changed files only)
|
|
237
|
+
if: steps.scope.outputs.run == 'true'
|
|
238
|
+
run: |
|
|
239
|
+
pnpm install --frozen-lockfile
|
|
240
|
+
pnpm --filter "$WEB_PACKAGE" exec stryker run --since="origin/$GITHUB_BASE_REF"
|
|
241
|
+
|
|
242
|
+
secret-scan:
|
|
243
|
+
name: Secret scan (TruffleHog)
|
|
244
|
+
runs-on: agents-arc
|
|
245
|
+
steps:
|
|
246
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
247
|
+
- run: docker run --rm -v "${{ github.workspace }}:/repo" "$TRUFFLEHOG_IMAGE" filesystem /repo --only-verified --fail --no-update
|
|
248
|
+
|
|
249
|
+
deps-and-iac-scan:
|
|
250
|
+
name: Dependencies and IaC (Trivy)
|
|
251
|
+
runs-on: agents-arc
|
|
252
|
+
steps:
|
|
253
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
254
|
+
- uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
|
255
|
+
with:
|
|
256
|
+
scan-type: fs
|
|
257
|
+
scanners: vuln,misconfig,secret
|
|
258
|
+
severity: CRITICAL,HIGH
|
|
259
|
+
ignore-unfixed: true
|
|
260
|
+
exit-code: "1"
|
|
261
|
+
format: table
|
|
262
|
+
|
|
263
|
+
sast-scan:
|
|
264
|
+
name: SAST (Semgrep)
|
|
265
|
+
runs-on: agents-arc
|
|
266
|
+
steps:
|
|
267
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
268
|
+
- run: docker run --rm -v "${{ github.workspace }}:/src" -w /src "$SEMGREP_IMAGE" semgrep scan --error --metrics=off --oss-only --disable-version-check --config p/default --config p/security-audit --config p/secrets --config p/csharp --config p/typescript --config p/react --exclude-rule yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
|
|
269
|
+
|
|
270
|
+
sbom:
|
|
271
|
+
name: SBOM (CycloneDX)
|
|
272
|
+
runs-on: agents-arc
|
|
273
|
+
steps:
|
|
274
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
275
|
+
- uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0
|
|
276
|
+
with:
|
|
277
|
+
format: cyclonedx-json
|
|
278
|
+
artifact-name: ${{ env.SBOM_ARTIFACT_NAME }}
|
|
279
|
+
|
|
280
|
+
# A bot pull request is opened with GITHUB_TOKEN, and GitHub delivers no workflow_run event
|
|
281
|
+
# for the CI runs that follow (the run's actor is the bot). The router's CI-completion route
|
|
282
|
+
# never sees them, so this job hands the verdict over from inside CI. workflow_dispatch is
|
|
283
|
+
# one of the two events GITHUB_TOKEN may raise, so no App token is needed here, and the
|
|
284
|
+
# router treats a dispatch as trusted. Human pull requests are left to the workflow_run
|
|
285
|
+
# route, which does fire for them. The reconcile belt remains the hourly fallback.
|
|
286
|
+
#
|
|
287
|
+
# If a deploy pipeline calls this workflow with `uses:`, that caller must also grant
|
|
288
|
+
# `actions: write`. A called workflow may not request a scope its caller withheld, and the
|
|
289
|
+
# whole run fails to start with zero jobs and no annotation when it does.
|
|
290
|
+
dispatch-merge-gate:
|
|
291
|
+
name: Dispatch merge gate
|
|
292
|
+
needs: [api, mutation-api, web, mutation-web]
|
|
293
|
+
if: >
|
|
294
|
+
always() &&
|
|
295
|
+
github.event_name == 'pull_request' &&
|
|
296
|
+
github.event.pull_request.head.repo.full_name == github.repository &&
|
|
297
|
+
github.event.pull_request.draft == false &&
|
|
298
|
+
github.event.pull_request.user.type == 'Bot'
|
|
299
|
+
runs-on: agents-arc
|
|
300
|
+
timeout-minutes: 5
|
|
301
|
+
permissions:
|
|
302
|
+
contents: read
|
|
303
|
+
actions: write
|
|
304
|
+
steps:
|
|
305
|
+
- name: Dispatch the merge gate with this run's verdict
|
|
306
|
+
env:
|
|
307
|
+
GH_TOKEN: ${{ github.token }}
|
|
308
|
+
REPO: ${{ github.repository }}
|
|
309
|
+
REF: ${{ github.event.repository.default_branch }}
|
|
310
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
311
|
+
RUN_ID: ${{ github.run_id }}
|
|
312
|
+
RESULTS: ${{ join(needs.*.result, ' ') }}
|
|
313
|
+
run: |
|
|
314
|
+
set -euo pipefail
|
|
315
|
+
conclusion=success
|
|
316
|
+
for result in $RESULTS; do
|
|
317
|
+
case "$result" in
|
|
318
|
+
failure) conclusion=failure; break ;;
|
|
319
|
+
cancelled | timed_out) conclusion=cancelled ;;
|
|
320
|
+
esac
|
|
321
|
+
done
|
|
322
|
+
# A cancelled run was superseded by a newer push, whose own run dispatches.
|
|
323
|
+
if [ "$conclusion" = cancelled ]; then
|
|
324
|
+
echo "CI was cancelled; not dispatching the merge gate."
|
|
325
|
+
exit 0
|
|
326
|
+
fi
|
|
327
|
+
echo "Dispatching the merge gate for PR #$PR_NUMBER with CI $conclusion (run $RUN_ID)."
|
|
328
|
+
gh workflow run work-router.yml --repo "$REPO" --ref "$REF" \
|
|
329
|
+
-f operation=merge-gate -f pr-number="$PR_NUMBER" \
|
|
330
|
+
-f ci-conclusion="$conclusion" -f ci-run-id="$RUN_ID"
|