Skip to content

Commit 21ebea1

Browse files
committed
Add an end-to-end harness for bare metal job callbacks
Runs the job callback scenarios against the dev API from a test pull request, and the push scenario from a push to a dedicated branch. The submit jobs start the detached runs and exit, then a runner on a KVM machine picks up the queued Jobs, and the attach workflow on main reports each one when its callback arrives. Each submit job checks the CLI's raw output for the tokens it was given, so a leak fails the job before the log is masked. This is test tooling only and must never be merged.
1 parent f350db0 commit 21ebea1

7 files changed

Lines changed: 652 additions & 0 deletions

File tree

‎.github/e2e/image/Dockerfile‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FROM busybox:1.37
2+
COPY --chmod=755 e2e-bench /usr/local/bin/e2e-bench

‎.github/e2e/image/e2e-bench‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/sh
2+
# A stand-in benchmark: `run` prints one Bencher Metric Format result,
3+
# `fail` exits with an error, and `sleep <seconds>` runs long enough to cancel.
4+
set -eu
5+
6+
case "${1:-}" in
7+
run) ;;
8+
fail)
9+
echo "e2e-bench: failing on purpose" >&2
10+
exit 1
11+
;;
12+
sleep)
13+
sleep "${2:?sleep needs a number of seconds}"
14+
;;
15+
*)
16+
echo "usage: e2e-bench run | fail | sleep <seconds>" >&2
17+
exit 2
18+
;;
19+
esac
20+
21+
echo '{"e2e::callback": {"latency": {"value": 1000.0}}}'
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: Install Bencher from the job callbacks branch
2+
description: Install the Bencher CLI or runner built from the tip of the job callbacks branch, cached by its commit.
3+
4+
inputs:
5+
component:
6+
description: "`cli` for the `bencher` CLI, or `runner` for the runner daemon"
7+
required: true
8+
9+
runs:
10+
using: composite
11+
steps:
12+
- name: Resolve the Bencher ${{ inputs.component }} commit
13+
id: commit
14+
shell: bash
15+
env:
16+
COMPONENT: ${{ inputs.component }}
17+
run: |
18+
case "$COMPONENT" in
19+
cli | runner) ;;
20+
*)
21+
echo "::error::Unknown component: $COMPONENT"
22+
exit 1
23+
;;
24+
esac
25+
sha="$(git ls-remote https://github.com/bencherdev/bencher refs/heads/u/ep/callback/smoke | cut -f1)"
26+
test -n "$sha"
27+
echo "Bencher $COMPONENT from bencherdev/bencher@$sha"
28+
echo "sha=$sha" >> "$GITHUB_OUTPUT"
29+
echo "key=bencher-$COMPONENT-${ImageOS:?}-$RUNNER_ARCH-$sha" >> "$GITHUB_OUTPUT"
30+
- name: Restore the Bencher ${{ inputs.component }}
31+
id: restore
32+
uses: actions/cache/restore@v5
33+
with:
34+
path: ~/.bencher-${{ inputs.component }}
35+
key: ${{ steps.commit.outputs.key }}
36+
- name: Install the Bencher CLI
37+
if: inputs.component == 'cli' && steps.restore.outputs.cache-hit != 'true'
38+
shell: bash
39+
env:
40+
BENCHER_SHA: ${{ steps.commit.outputs.sha }}
41+
run: cargo install --git https://github.com/bencherdev/bencher --rev "$BENCHER_SHA" --locked --root ~/.bencher-cli bencher_cli
42+
- name: Install the Bencher runner
43+
if: inputs.component == 'runner' && steps.restore.outputs.cache-hit != 'true'
44+
shell: bash
45+
env:
46+
BENCHER_SHA: ${{ steps.commit.outputs.sha }}
47+
run: |
48+
sudo apt-get update && sudo apt-get install -y musl-tools
49+
rustup target add x86_64-unknown-linux-musl
50+
# The release runner embeds a static `bencher-init` for its VMs.
51+
cargo install --git https://github.com/bencherdev/bencher --rev "$BENCHER_SHA" --locked --target x86_64-unknown-linux-musl --root "$RUNNER_TEMP/bencher-init" bencher_init
52+
BENCHER_INIT_PATH="$RUNNER_TEMP/bencher-init/bin/bencher-init" cargo install --git https://github.com/bencherdev/bencher --rev "$BENCHER_SHA" --locked --root ~/.bencher-runner bencher_runner_cli
53+
- name: Save the Bencher ${{ inputs.component }}
54+
if: steps.restore.outputs.cache-hit != 'true'
55+
uses: actions/cache/save@v5
56+
with:
57+
path: ~/.bencher-${{ inputs.component }}
58+
key: ${{ steps.commit.outputs.key }}
59+
- name: Add the Bencher ${{ inputs.component }} to the path
60+
shell: bash
61+
env:
62+
COMPONENT: ${{ inputs.component }}
63+
run: echo "$HOME/.bencher-$COMPONENT/bin" >> "$GITHUB_PATH"

‎.github/e2e/runner.sh‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env bash
2+
# Host a dev runner for the scenarios' Jobs, after every submit job has finished:
3+
#
4+
# runner.sh serve serve `test-spec` as `test-runner` for 30 minutes
5+
# runner.sh cancel serve `no-sandbox-spec` as `test-runner-no-sandbox`, and kill the runner
6+
# once the cancel scenario's Job is running, so the server cancels that Job
7+
# when its timeout and grace period pass
8+
set -euo pipefail
9+
10+
now() {
11+
date -u +%Y-%m-%dT%H:%M:%SZ
12+
}
13+
14+
rotate_key() {
15+
local key
16+
key="$(bencher runner key --token "$BENCHER_ADMIN_API_TOKEN" "$1" | jq -r '.key // empty')"
17+
if [ -z "$key" ]; then
18+
echo "::error::Rotating the key of $1 returned no key"
19+
return 1
20+
fi
21+
echo "::add-mask::$key"
22+
export BENCHER_RUNNER_KEY="$key"
23+
}
24+
25+
serve() {
26+
rotate_key test-runner
27+
echo "Runner up at $(now)"
28+
local status=0
29+
timeout --kill-after 60s 30m runner up --runner test-runner --no-auto-update || status=$?
30+
# `timeout` exits 124 after it stops the runner, or 137 if the runner needed a SIGKILL.
31+
if [ "$status" -eq 124 ] || [ "$status" -eq 137 ]; then
32+
echo "Runner down at $(now)"
33+
return 0
34+
fi
35+
return "$status"
36+
}
37+
38+
cancel() {
39+
rotate_key test-runner-no-sandbox
40+
local jobs count job
41+
jobs="$(bencher job list "$PAID_PROJECT" --status pending --sort created --direction desc --per-page 255)"
42+
count="$(jq '[.[] | select(.spec.slug == "no-sandbox-spec")] | length' <<< "$jobs")"
43+
job="$(jq -r '[.[] | select(.spec.slug == "no-sandbox-spec")][0].uuid // empty' <<< "$jobs")"
44+
if [ -z "$job" ]; then
45+
echo "::error::There is no pending no-sandbox-spec Job in $PAID_PROJECT to cancel"
46+
return 1
47+
fi
48+
if [ "$count" -gt 1 ]; then
49+
echo "::warning::$count no-sandbox-spec Jobs are pending in $PAID_PROJECT, and the runner may run older ones before $job"
50+
fi
51+
52+
local log="$RUNNER_TEMP/runner.log"
53+
echo "Runner up at $(now), waiting for Job $job to start"
54+
runner up --runner test-runner-no-sandbox --danger-allow-no-sandbox --no-auto-update > "$log" 2>&1 &
55+
local pid=$!
56+
local deadline=$((SECONDS + 900))
57+
until grep -qF "Starting iteration 1/1 for job $job" "$log"; do
58+
if ! kill -0 "$pid" 2> /dev/null; then
59+
cat "$log"
60+
echo "::error::The runner exited before Job $job started"
61+
return 1
62+
fi
63+
if [ "$SECONDS" -ge "$deadline" ]; then
64+
kill -KILL "$pid"
65+
cat "$log"
66+
echo "::error::Job $job did not start within 15 minutes"
67+
return 1
68+
fi
69+
sleep 1
70+
done
71+
# A SIGTERM would let the runner finish the Job first, so the runner gets no chance to.
72+
kill -KILL "$pid"
73+
wait "$pid" || true
74+
cat "$log"
75+
echo "Killed the runner at $(now) while Job $job was running"
76+
}
77+
78+
case "${1:-}" in
79+
serve) serve ;;
80+
cancel) cancel ;;
81+
*)
82+
echo "usage: runner.sh serve | cancel" >&2
83+
exit 2
84+
;;
85+
esac

‎.github/e2e/setup.sh‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env bash
2+
# Make sure dev has what the scenarios need. Every step is idempotent,
3+
# so it runs as is after every deploy wipes the dev database.
4+
set -euo pipefail
5+
6+
ensure_project() {
7+
local organization="$1" slug="$2" name="$3"
8+
if bencher project view "$slug" > /dev/null 2>&1; then
9+
echo "Project $slug exists"
10+
else
11+
bencher project create "$organization" --name "$name" --slug "$slug" > /dev/null
12+
echo "Created project $slug in $organization"
13+
fi
14+
}
15+
16+
# The seed recreates the paid organization on every deploy, and only a plan makes it paid.
17+
bencher organization view "$PAID_ORGANIZATION" > /dev/null
18+
if bencher plan view --attempts 3 "$PAID_ORGANIZATION" > /dev/null 2>&1; then
19+
echo "Organization $PAID_ORGANIZATION has a plan"
20+
elif [ -z "${BENCHER_DEV_SUBSCRIPTION:-}" ]; then
21+
echo "::warning::The BENCHER_DEV_SUBSCRIPTION repository variable is not set, so $PAID_ORGANIZATION has no plan and every callback is skipped"
22+
else
23+
# The level only matters to a licensed plan: a metered plan reads its level from the subscription.
24+
bencher plan create "$PAID_ORGANIZATION" \
25+
--checkout "$BENCHER_DEV_SUBSCRIPTION" \
26+
--level "${BENCHER_DEV_PLAN_LEVEL:-team}" \
27+
--skip-remote > /dev/null
28+
echo "Attached the subscription to $PAID_ORGANIZATION"
29+
fi
30+
ensure_project "$PAID_ORGANIZATION" "$PAID_PROJECT" "Callback E2E"
31+
32+
if bencher organization view "$FREE_ORGANIZATION" > /dev/null 2>&1; then
33+
echo "Organization $FREE_ORGANIZATION exists"
34+
else
35+
bencher organization create --name "Callback E2E Free" --slug "$FREE_ORGANIZATION" > /dev/null
36+
echo "Created organization $FREE_ORGANIZATION"
37+
fi
38+
if bencher plan view --attempts 3 "$FREE_ORGANIZATION" > /dev/null 2>&1; then
39+
echo "::error::Organization $FREE_ORGANIZATION has a plan, but the no plan scenario needs one without"
40+
exit 1
41+
fi
42+
ensure_project "$FREE_ORGANIZATION" "$FREE_PROJECT" "Callback E2E Free"
43+
44+
docker build --tag e2e-bench "$(dirname "$0")/image"
45+
printf '%s' "$BENCHER_API_TOKEN" | docker login "$BENCHER_REGISTRY" --username "$DEV_USER_EMAIL" --password-stdin
46+
for project in "$PAID_PROJECT" "$FREE_PROJECT"; do
47+
docker tag e2e-bench "$BENCHER_REGISTRY/$project:$IMAGE_TAG"
48+
docker push "$BENCHER_REGISTRY/$project:$IMAGE_TAG"
49+
done
50+
docker logout "$BENCHER_REGISTRY"

‎.github/e2e/submit.sh‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
# Run a detached `bencher run` and check its raw output, before GitHub masks the log:
3+
#
4+
# submit.sh <scenario> bencher run ...
5+
#
6+
# REDACTED lists the environment variables whose values must never be printed,
7+
# and EXPECT_SKIPPED says whether the server should skip the callback for want of a plan.
8+
set -euo pipefail
9+
10+
scenario="$1"
11+
shift
12+
13+
out="$RUNNER_TEMP/bencher-run.stdout"
14+
err="$RUNNER_TEMP/bencher-run.stderr"
15+
status=0
16+
"$@" > "$out" 2> "$err" || status=$?
17+
cat "$out"
18+
cat "$err" >&2
19+
20+
failed=0
21+
fail() {
22+
echo "::error title=$scenario::$1"
23+
failed=1
24+
}
25+
26+
for name in $REDACTED; do
27+
value="${!name:-}"
28+
if [ -z "$value" ]; then
29+
fail "$name is empty, so there is nothing to check its redaction against"
30+
elif grep -qF -- "$value" "$out" "$err"; then
31+
fail "the CLI printed the raw value of $name"
32+
fi
33+
done
34+
if ! grep -qF '"authorization": "************"' "$out"; then
35+
fail "the Bencher New Report echo does not show the CLI's mask for the authorization header"
36+
fi
37+
if grep -qF '/dispatches' "$out" "$err"; then
38+
fail "the CLI printed the callback URL past its origin"
39+
fi
40+
41+
skipped=false
42+
if grep -qxF 'callback skipped: requires a Bencher Plus plan' "$err"; then
43+
skipped=true
44+
fi
45+
if [ "$skipped" != "$EXPECT_SKIPPED" ]; then
46+
fail "expected a skipped callback to be $EXPECT_SKIPPED, but it was $skipped"
47+
fi
48+
49+
job="$(sed -n 's/^Remote job submitted successfully: //p' "$err" | head -n 1)"
50+
check="$(grep -oE '"check": [0-9]+' "$out" | head -n 1 | grep -oE '[0-9]+' || true)"
51+
if [ -z "$job" ]; then
52+
fail "the CLI did not print the submitted Job"
53+
fi
54+
echo "::notice title=$scenario::Job ${job:-none}, check run ${check:-none}, commit $HEAD_SHA"
55+
{
56+
echo "| Scenario | Job | Check run | Commit |"
57+
echo "| --- | --- | --- | --- |"
58+
echo "| $scenario | ${job:-none} | ${check:-none} | $HEAD_SHA |"
59+
} >> "$GITHUB_STEP_SUMMARY"
60+
61+
if [ "$status" -ne 0 ]; then
62+
exit "$status"
63+
fi
64+
exit "$failed"

0 commit comments

Comments
 (0)