-
Notifications
You must be signed in to change notification settings - Fork 216
253 lines (248 loc) · 13.1 KB
/
Copy pathbenchmark.yml
File metadata and controls
253 lines (248 loc) · 13.1 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
# IMPORTANT: this workflow runs untrusted PR code and has a non-obvious
# security model documented at ../SECURITY_MODEL.md. Anyone editing this
# file -- or reviewing changes to it -- MUST read that document first.
# Several constraints below (permissions blocks, save-if expressions, the
# step ordering around actions/checkout, the bench-pr-number upload) look
# like style choices but are load-bearing.
#
# Benchmarks run on four triggers:
# - pull_request: every push to a non-draft PR auto-runs with BENCH_TAGS=base
# - push (main only): warms the rust-cache so PR runs restore a warm target/
# - issue_comment: a /bench [--tags ...] [--filter ...] comment runs with
# the supplied flags (including on draft PRs)
# - merge_group: declared so this workflow can be added to branch protection
# as a required status check later. Currently a no-op.
# Comment posting lives in a separate workflow_run-triggered workflow at
# .github/workflows/benchmark-post-comment.yml (mirroring the pr-validator /
# comment-on-title-failure split). This workflow uploads /tmp/bench-comment.md
# as an artifact; that workflow downloads it and posts/updates the PR comment
# in base-branch context (which has the write scope this workflow lacks under
# pull_request from a fork).
#
# The run-benchmark job fails if any benchmark regresses past the fail
# threshold in benchmarks/ci/parse_critcmp.py, unless the PR carries the
# ignore-benchmark-failure label. The comment artifact is uploaded before that
# gate runs, so the result table still gets posted on a gate failure.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
push:
branches: [main]
merge_group:
issue_comment:
types: [created, edited]
name: Benchmarking PR performance
# Start from an empty default; each job opts into the minimum scopes it needs.
# Adding a new job without thinking about permissions inherits zero, not full.
permissions: {}
jobs:
warm-bench-cache:
# On every push to main, compile the bench harness (no measurement) so
# rust-cache saves a warm target/ under main's scope. PR runs then restore
# this entry instead of compiling cold every time. Rapid main pushes
# dedupe via the concurrency group.
name: Warm bench cache
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
concurrency:
group: warm-bench-cache
cancel-in-progress: true
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4
with:
cache: false
# shared-key keeps this and `run-benchmark` in the same cache namespace
# so PR runs can restore what we save here.
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
cache-bin: false
shared-key: bench
save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
- name: Compile bench harness
run: (cd benchmarks && cargo bench --locked --bench workload_bench --no-run)
bench-merge-queue-noop:
# Placeholder job for the merge_group trigger so the workflow can be wired
# up as a required status check later without restructuring. Does nothing
# meaningful today (see top-level comment for rationale).
name: Bench (merge queue no-op)
if: github.event_name == 'merge_group'
runs-on: ubuntu-latest
permissions: {}
steps:
- run: echo "Benchmark workflow is a no-op on merge queue events."
run-benchmark:
name: Run benchmarks
# Auto-trigger: PR push events, skipping drafts. Comment-trigger: a
# literal /bench, optionally followed by args (e.g. /bench --tags base).
if: >
(github.event_name == 'pull_request' && github.event.pull_request.draft == false)
|| (github.event_name == 'issue_comment' && github.event.issue.pull_request &&
(github.event.comment.body == '/bench' || startsWith(github.event.comment.body, '/bench ')))
# Job-level (not workflow-level) so unrelated PR comments don't join the
# group and cancel an in-flight bench via cancel-in-progress.
concurrency:
group: bench-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: true
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
pr_number: ${{ steps.pr.outputs.pr_number }}
steps:
- name: Get PR metadata
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
EVENT_NAME: ${{ github.event_name }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# GitHub Actions runs bash with -eo pipefail by default; -u catches
# typos / unset variables. All four fields below are then validated
# against strict allowlists before being used or emitted -- the PR
# head sha and base ref are attacker-controllable (fork PR author),
# and the issue/PR number is concatenated into a gh api URL.
set -u
if [[ "$EVENT_NAME" == "issue_comment" ]]; then
# /bench path: the issue_comment payload doesn't include the PR
# head/base, so look it up via the API. Validate the number first
# so a malformed value cannot inject path segments into the URL.
[[ "$ISSUE_NUMBER" =~ ^[1-9][0-9]*$ ]] \
|| { echo "Invalid ISSUE_NUMBER: $ISSUE_NUMBER" >&2; exit 1; }
PR_DATA=$(gh api "repos/$REPO/pulls/$ISSUE_NUMBER")
HEAD_SHA=$(echo "$PR_DATA" | jq -r .head.sha)
BASE_REF=$(echo "$PR_DATA" | jq -r .base.ref)
NUM="$ISSUE_NUMBER"
else
# Auto-trigger path: pull_request payload has everything.
HEAD_SHA="$PR_HEAD_SHA"
BASE_REF="$PR_BASE_REF"
NUM="$PR_NUMBER"
[[ "$NUM" =~ ^[1-9][0-9]*$ ]] \
|| { echo "Invalid PR_NUMBER: $NUM" >&2; exit 1; }
fi
# HEAD_SHA must be a hex Git SHA (40 char SHA-1 or 64 char SHA-256).
# The allowlist also catches the empty-string case if jq somehow
# returned no value.
[[ "$HEAD_SHA" =~ ^[a-f0-9]{40,64}$ ]] \
|| { echo "Invalid HEAD_SHA: $HEAD_SHA" >&2; exit 1; }
# BASE_REF is a Git ref name; standard ref-name allowlist (letters,
# digits, slash, dot, dash, underscore). Both regexes exclude
# newlines, so no separate newline check is needed.
[[ "$BASE_REF" =~ ^[a-zA-Z0-9/_.-]+$ ]] \
|| { echo "Invalid BASE_REF: $BASE_REF" >&2; exit 1; }
printf 'head_sha=%s\n' "$HEAD_SHA" >> "$GITHUB_OUTPUT"
printf 'base_ref=%s\n' "$BASE_REF" >> "$GITHUB_OUTPUT"
printf 'pr_number=%s\n' "$NUM" >> "$GITHUB_OUTPUT"
# Read labels here, in trusted pre-checkout context, so the
# regression gate's override decision can't be influenced by PR code
# that runs later. Capture into a variable before matching:
# `gh ... | grep -q` would let grep close the pipe early and trip
# pipefail. On any read error LABELS is empty and the gate stays
# enforced.
LABELS=$(gh pr view "$NUM" --repo "$REPO" --json labels --jq '.labels[].name' || true)
if grep -qx 'ignore-benchmark-failure' <<< "$LABELS"; then
printf 'ignore_failure=true\n' >> "$GITHUB_OUTPUT"
else
printf 'ignore_failure=false\n' >> "$GITHUB_OUTPUT"
fi
- name: Stash PR number for the post-comment workflow
# Written to /tmp/pr-number.txt and uploaded as its own artifact in
# the next step. The two-step shape (write then upload) keeps the
# value out of /tmp by the time PR-controlled code runs below; see
# the upload step for the security rationale.
env:
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
run: printf '%s\n' "$PR_NUMBER" > /tmp/pr-number.txt
- name: Upload PR number (trusted, pre-checkout)
# Uploaded BEFORE checking out the PR head so the artifact's contents
# cannot be tampered with by PR-controlled code that runs in later
# steps. Once stored on GitHub Actions storage the artifact is
# immutable, and this job's contents:read token has no actions:write
# scope, so the bench script cannot delete or replace it.
#
# The post-comment workflow trusts this artifact only for the
# `issue_comment` upstream path, where benchmark.yml itself is
# served from the default branch (trusted) -- the fork cannot
# influence what gets written. For `pull_request` upstream the
# workflow YAML is fork-controlled, so the post-comment workflow
# ignores this artifact and derives the PR number from the trusted
# workflow_run.head_branch instead.
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: bench-pr-number
path: /tmp/pr-number.txt
- name: Install critcmp
# Installed before checkout so the PR's .cargo/config.toml cannot
# redirect the registry to a malicious source. The runner's
# pre-installed Rust is sufficient -- no toolchain setup needed here.
# --locked is omitted for cargo install (same exemption as cargo miri
# setup); --version pins the top-level crate.
run: cargo install critcmp --version 0.1.8
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
# Check out the integration commit (PR head merged into base) that
# GitHub maintains at refs/pull/<N>/merge -- the same ref build.yml
# and the test jobs resolve to via their bare checkout. Benchmarking
# this measures the PR as it would land, not the raw head. An
# unmergeable PR has no maintained merge ref, so the checkout fails
# and the job stops (parity with build/test; see SECURITY_MODEL.md).
ref: refs/pull/${{ steps.pr.outputs.pr_number }}/merge
persist-credentials: false
- uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4
with:
cache: false
# See build.yml top-level comment for why save-if is restricted to main
# and why cache-bin is disabled. `shared-key: bench` keeps this aligned
# with `warm-bench-cache` so PR runs restore the entry that main pushes
# save -- without the shared-key, each job's cache lives in a separate
# namespace and the warming would not benefit PRs.
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
cache-bin: false
shared-key: bench
save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
- name: Run benchmarks
# The benchmark-post-comment workflow picks up the uploaded artifact
# and posts the PR comment in base-branch context (which has the
# write scope this workflow lacks on fork PRs).
# COMMENT is only meaningful under issue_comment; under pull_request
# the expression resolves to "" and the script treats that as bare /bench.
env:
COMMENT: ${{ github.event.comment.body }}
TRIGGER: ${{ github.event_name == 'issue_comment' && '/bench' || 'auto-push' }}
BASE_REF: ${{ steps.pr.outputs.base_ref }}
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
BENCH_IGNORE_FAILURE: ${{ steps.pr.outputs.ignore_failure }}
run: bash benchmarks/ci/run-benchmarks.sh
- name: Upload bench comment
# Default `if: success()` -- a bench failure leaves no body to post.
# Runs before the regression gate below so the artifact exists even
# when the gate fails the job.
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: bench-comment
path: /tmp/bench-comment.md
- name: Enforce benchmark regression gate
# parse_critcmp.py wrote "true" to this file iff some benchmark crossed
# its fail threshold. Fail the job on a regression unless the PR opted
# out with the ignore-benchmark-failure label (resolved pre-checkout in
# the trusted metadata step).
env:
IGNORE_FAILURE: ${{ steps.pr.outputs.ignore_failure }}
run: |
set -u
REGRESSED=$(tr -d '[:space:]' < /tmp/bench-regression.txt 2>/dev/null || echo false)
if [[ "$REGRESSED" == "true" && "$IGNORE_FAILURE" != "true" ]]; then
echo "::error::A benchmark regressed by at least 15%. Add the 'ignore-benchmark-failure' label to override this gate." >&2
exit 1
fi
echo "Regression gate passed (regressed=$REGRESSED, ignore_failure=$IGNORE_FAILURE)."