-
Notifications
You must be signed in to change notification settings - Fork 987
Expand file tree
/
Copy pathaction.yml
More file actions
215 lines (205 loc) · 13.4 KB
/
Copy pathaction.yml
File metadata and controls
215 lines (205 loc) · 13.4 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
name: 'Select relevant tests'
description: >
Compute the CI test subset (and the non-.NET job booleans) relevant to a PR's changed files,
using the SelectTests tool (Layer 1 MSBuild graph + Layer 2 curated trigger map). In enforce mode
with a non-ALL selection it writes an OverrideProjectToBuild props file that the enumerate-tests
action consumes (beforeBuildPropsPath) to enumerate only the selected projects.
inputs:
checkout:
required: false
default: 'true'
description: 'Whether to check out the repo. Set false when the caller has already checked out and the same working tree must be reused by a later step (e.g. enumerate-tests with checkout=false).'
setupDotNet:
required: false
default: 'true'
description: 'Whether to bootstrap the .NET SDK and the runtime required by SelectTests. Set false when the caller already installed the toolchain in the same job. Selection does not require a full repo restore because it builds an MSBuild ProjectGraph, which auto-resolves SDKs.'
enforce:
required: false
default: 'false'
description: 'The audit/enforce knob. false (audit): no restriction props are written, so enumerate-tests builds the FULL matrix and run_* are all true; the summary still reports what enforcing would have skipped. true (enforce): a non-ALL selection writes the restriction props and run_* gate the non-.NET jobs.'
slnx:
required: false
default: ''
description: 'Path to the solution that defines the project universe (forwarded as --slnx). Empty defaults to <repo-root>/Aspire.slnx.'
triggerMap:
required: false
default: ''
description: 'Path to the curated trigger map (forwarded as --map). Empty defaults to <repo-root>/eng/github-ci/test-trigger-map.yml.'
beforeBuildProps:
required: false
default: ''
description: 'Where to write the OverrideProjectToBuild restriction props (consumed by eng/Build.props via $(BeforeBuildPropsPath)). Empty defaults to $GITHUB_WORKSPACE/artifacts/BeforeBuildProps.props -- absolute so eng/Build.props resolves it regardless of cwd, under artifacts/ (gitignored) so it is not seen as a source change.'
commentFile:
required: false
default: ''
description: 'Where the selector writes the human-readable selection summary (also goes to the job step summary) so the caller can post it as a PR comment. Empty defaults to $GITHUB_WORKSPACE/artifacts/select-tests-comment.md.'
selectionJsonFile:
required: false
default: ''
description: 'Where the selector writes the machine-readable selection record (mode, inputs, and every selected test/job with its per-item causes incl. the Layer 1 decision path). Uploaded as an artifact for post-hoc debugging. Empty defaults to $GITHUB_WORKSPACE/artifacts/select-tests-selection.json.'
forceAll:
required: false
default: 'false'
description: 'Kill switch: when ''true'' the selector returns the full matrix and all jobs regardless of the diff. The caller computes this -- on PRs from the run-full-ci label. Non-PR events (no base SHA) force the full set regardless.'
prBaseSha:
required: false
default: ''
description: 'The PR base SHA. When set, selection diffs base..head; the base commit is fetched first. Empty (non-PR events) selects everything.'
headSha:
required: false
# The PR's REAL head, NOT github.sha (the synthetic refs/pull/N/merge commit, regenerated as the base
# advances -- feeding it lets base-branch churn leak into the diff and over-select). Only consumed when
# prBaseSha is set (PR events), where pull_request.head.sha is populated; empty on non-PR events, where
# headSha is unused because prBaseSha is empty too. Default kept correct so a caller can't reintroduce
# the github.sha bug by omitting it.
default: ${{ github.event.pull_request.head.sha }}
description: 'The PR head SHA to diff to (the PR''s real head, not the synthetic merge ref).'
outputs:
project_override_props:
description: 'Path to the restriction props that was written, or empty when enumerate-tests should enumerate everything.'
value: ${{ steps.select.outputs.project_override_props }}
has_dotnet_tests:
description: 'false only for an enforcing, non-ALL selection with no buildable test project (the caller then skips enumerate-tests and emits an empty matrix). ALL and audit are always true.'
value: ${{ steps.select.outputs.has_dotnet_tests }}
selection:
description: >
JSON object of the non-.NET job gates, one boolean per job the trigger map knows, keyed
run_<job> (e.g. {"run_polyglot":true,"run_extension_e2e":false,...}). Consume via fromJSON(),
e.g. fromJSON(steps.select.outputs.selection).run_polyglot. One object (rather than an output
per job) means adding a trigger-map job needs no change here -- only the consuming if: names
the new key. Audit mode forces every value true (the full matrix still runs).
value: ${{ steps.select.outputs.selection }}
runs:
using: "composite"
steps:
- name: Checkout code
if: ${{ inputs.checkout == 'true' }}
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# This action runs PR-authored code (the SelectTests build); don't leave the checkout's
# GITHUB_TOKEN persisted in .git/config where that code could read it. The base-commit fetch
# below is unauthenticated (public repo) and nothing here pushes, so dropping it is safe.
with:
persist-credentials: false
# Minimal toolchain bootstrap -- not a full ./restore.sh. The repo SDK is .NET 11 while
# SelectTests intentionally targets .NET 10 for its Microsoft.Build ProjectGraph dependency.
# Install a .NET 10 SDK because MSBuildLocator needs its MSBuild assemblies; a runtime alone can
# execute SelectTests but leaves RegisterDefaults with no MSBuild instance to discover.
- name: Install toolchain (minimal)
if: ${{ inputs.setupDotNet == 'true' }}
shell: bash
run: |
set -euo pipefail
./dotnet.sh --version
sdk_version="$(sed -n 's:.*<DotNetSdkNet10VersionForTesting>\(.*\)</DotNetSdkNet10VersionForTesting>.*:\1:p' eng/Versions.props)"
if [ -z "$sdk_version" ]; then
echo "::error::Could not read DotNetSdkNet10VersionForTesting from eng/Versions.props." >&2
exit 1
fi
./eng/common/dotnet-install.sh \
-runtime sdk \
-version "$sdk_version"
- name: Select relevant tests
id: select
shell: bash
env:
FORCE_ALL: ${{ inputs.forceAll }}
PR_BASE_SHA: ${{ inputs.prBaseSha }}
HEAD_SHA: ${{ inputs.headSha }}
BEFORE_BUILD_PROPS: ${{ inputs.beforeBuildProps }}
SELECT_TESTS_COMMENT_FILE: ${{ inputs.commentFile }}
SELECT_TESTS_JSON_FILE: ${{ inputs.selectionJsonFile }}
ENFORCE_SELECTION: ${{ inputs.enforce }}
SLNX: ${{ inputs.slnx }}
TRIGGER_MAP: ${{ inputs.triggerMap }}
run: |
set -euo pipefail
# Resolve the side-channel paths under the workspace when the caller didn't override them.
# Absolute (under artifacts/, gitignored) so eng/Build.props's $(BeforeBuildPropsPath) resolves
# it regardless of cwd and it is never seen as a source change.
BEFORE_BUILD_PROPS="${BEFORE_BUILD_PROPS:-$GITHUB_WORKSPACE/artifacts/BeforeBuildProps.props}"
SELECT_TESTS_COMMENT_FILE="${SELECT_TESTS_COMMENT_FILE:-$GITHUB_WORKSPACE/artifacts/select-tests-comment.md}"
export SELECT_TESTS_COMMENT_FILE
# The machine-readable selection record (per-item causes + decision paths), uploaded below for
# post-hoc debugging of "why did this test/job run?" without re-running CI.
SELECT_TESTS_JSON_FILE="${SELECT_TESTS_JSON_FILE:-$GITHUB_WORKSPACE/artifacts/select-tests-selection.json}"
export SELECT_TESTS_JSON_FILE
args=(--repo-root . --before-build-props "$BEFORE_BUILD_PROPS")
[ -n "$SLNX" ] && args+=(--slnx "$SLNX")
[ -n "$TRIGGER_MAP" ] && args+=(--map "$TRIGGER_MAP")
[ "$ENFORCE_SELECTION" = "true" ] && args+=(--enforce)
# Kill switch: the caller sets forceAll (on PRs, from the run-full-ci label) to force the whole
# matrix regardless of which files changed.
if [ "$FORCE_ALL" = "true" ]; then
args+=(--force-all)
elif [ -n "$PR_BASE_SHA" ]; then
# Pull requests select on the PR's own commits: SelectTests diffs HEAD against the merge-base
# of base..head (the branch point), so commits that landed on the base branch after the PR
# branched don't get mis-attributed to it. This job's checkout is shallow, so we must make
# enough history of BOTH endpoints available locally for git to find their common ancestor.
#
# Start with the base commit itself. If it can't be fetched we FAIL rather than fall back to
# --force-all: base.sha is always reachable on origin, so a failure here is a real problem (a
# bad fetch or a rewritten base), and masking it with run-all would teach the audit nothing.
if ! git fetch --no-tags --depth=1 origin "$PR_BASE_SHA"; then
echo "::error::Failed to fetch PR base commit $PR_BASE_SHA; cannot compute the changed-file diff." >&2
exit 1
fi
if ! git cat-file -e "${PR_BASE_SHA}^{commit}" 2>/dev/null; then
echo "::error::PR base commit $PR_BASE_SHA is unavailable after fetch; cannot compute the changed-file diff." >&2
exit 1
fi
# Deepen both endpoints until their merge-base is reachable. A shallow checkout truncates
# ancestry, so git can't see the branch point yet. Re-fetch both SHAs at a growing depth until
# `git merge-base` succeeds. If it never resolves within the bound, DON'T fail the PR: fall back
# to --force-all (run every test) -- the fail-SAFE outcome, matching SelectTests' own fallback --
# and warn loudly so a systemic regression is visible instead of blocking PRs while it's fixed.
# The bound also prevents an unbounded fetch on pathological histories.
merge_base_found=true
depth=1
until git merge-base "$PR_BASE_SHA" "$HEAD_SHA" >/dev/null 2>&1; do
if [ "$depth" -ge 4096 ]; then
echo "::warning::Could not find a merge-base of base $PR_BASE_SHA and head $HEAD_SHA within $depth commits of history; running ALL tests for this PR." >&2
merge_base_found=false
break
fi
depth=$((depth * 4))
echo "Merge-base of base..head not yet reachable; deepening history to depth $depth."
# Fetch both SHAs in one shot. '|| true' so a deepen that can't make progress (e.g. one side
# is already complete) doesn't abort the loop -- the merge-base check above is the real gate,
# and the depth bound guarantees termination.
git fetch --no-tags --depth="$depth" origin "$PR_BASE_SHA" "$HEAD_SHA" || true
done
if [ "$merge_base_found" = "true" ]; then
args+=(--from "$PR_BASE_SHA" --to "$HEAD_SHA")
else
# Unresolved merge-base -> fail-safe run-all rather than blocking the PR. Pass the reason so the
# run summary records WHY this is a full run -- distinct from a run-full-ci kill switch -- to
# match SelectTests' own tool-level fallback. Without it a systemic shallow-history regression
# hides behind a "(kill switch)" label in the audit's durable record (the summary, not the logs).
args+=(--force-all --force-all-reason "git merge-base of base $PR_BASE_SHA and head $HEAD_SHA was unreachable within $depth commits of CI checkout history")
fi
else
# Non-PR events (e.g. push to main) have no reliable base -> run everything.
args+=(--force-all)
fi
# SelectTests computes the Layer 1 affected-projects graph in-process from the solution
# (Microsoft.Build via MSBuildLocator -- needs only the minimal SDK above, not a full repo
# restore) and, when enforcing a non-ALL selection, writes the OverrideProjectToBuild props at
# $BEFORE_BUILD_PROPS (consumed by the enumerate-tests action's beforeBuildPropsPath). It also
# writes the `selection` job-boolean object and project_override_props (to $GITHUB_OUTPUT) and the summary.
# The tool selects ALL *by design* when told to (--force-all from the kill switch, or a non-PR
# event with no base), so a non-zero exit means the selector itself crashed (incl. a fatal
# Layer 1 graph failure -- under-selecting would silently skip tests). Let that fail the run
# rather than mask the bug behind a silent fallback.
./dotnet.sh run --project tools/SelectTests/SelectTests.csproj -- "${args[@]}"
# Publish the machine-readable selection record so a maintainer can answer "why did this test/job
# run?" weeks later without re-running CI. always() so it is captured even when a later step fails;
# if no path was set, the default under artifacts/ is used. A missing file (e.g. the selector crashed
# before writing it) is tolerated rather than failing the upload.
- name: Upload selection record
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: select-tests-selection-${{ runner.os }}
path: ${{ inputs.selectionJsonFile != '' && inputs.selectionJsonFile || 'artifacts/select-tests-selection.json' }}
if-no-files-found: ignore