Skip to content

Commit 22fb860

Browse files
loks0nclaude
andcommitted
ci: extract inlined smoke scripts into scripts/ci/
Move the multi-line bash blocks (Sury PHP install, NTS/ZTS/Pyroscope smoke tests) out of ci.yml into dedicated, shellcheck-clean scripts under scripts/ci/, with a shared spin.php fixture. ci.yml steps now just invoke them, and the scripts are runnable locally. Fixes a latent bug carried over from the inlined version: `! grep -q 'push failed'` is exempt from `set -e`, so a push failure never failed the job; replaced with an explicit conditional. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7239291 commit 22fb860

6 files changed

Lines changed: 138 additions & 125 deletions

File tree

.github/workflows/ci.yml

Lines changed: 8 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -105,41 +105,12 @@ jobs:
105105
- uses: Swatinem/rust-cache@v2
106106
with:
107107
key: ${{ matrix.target }}-smoke
108-
- name: install Sury PHP 8.3
109-
run: |
110-
sudo apt-get update
111-
sudo apt-get install -y --no-install-recommends \
112-
curl ca-certificates gnupg lsb-release
113-
curl -sSLo /tmp/sury.gpg https://packages.sury.org/php/apt.gpg
114-
sudo install -m 644 /tmp/sury.gpg /usr/share/keyrings/sury.gpg
115-
. /etc/os-release
116-
echo "deb [signed-by=/usr/share/keyrings/sury.gpg] https://packages.sury.org/php/ ${VERSION_CODENAME} main" \
117-
| sudo tee /etc/apt/sources.list.d/sury.list
118-
sudo apt-get update
119-
sudo apt-get install -y --no-install-recommends php8.3-cli
108+
- name: install PHP
109+
run: ./scripts/ci/install-php.sh
120110
- name: build
121111
run: cargo build --release --target ${{ matrix.target }}
122-
- name: allow ptrace
123-
run: |
124-
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope || true
125112
- name: smoke
126-
run: |
127-
cat > /tmp/spin.php <<'PHP'
128-
<?php
129-
class W { public function spin(): never { while (true) $this->a(); }
130-
public function a(): void { $this->b(); }
131-
public function b(): void { usleep(500); } }
132-
(new W())->spin();
133-
PHP
134-
php8.3 /tmp/spin.php &
135-
PID=$!
136-
sleep 1
137-
./target/${{ matrix.target }}/release/pfp -p $PID -d 2 -H 99 -o /tmp/out.txt
138-
kill $PID || true
139-
wait || true
140-
test "$(grep -c '^0 ' /tmp/out.txt)" -ge 100
141-
grep -q 'W::a' /tmp/out.txt
142-
grep -q 'W::b' /tmp/out.txt
113+
run: ./scripts/ci/smoke.sh ./target/${{ matrix.target }}/release/pfp
143114

144115
smoke-zts:
145116
name: smoke-zts (${{ matrix.target }})
@@ -167,38 +138,7 @@ jobs:
167138
- name: build
168139
run: cargo build --release --target ${{ matrix.target }}
169140
- name: smoke (ZTS)
170-
# Run the ZTS PHP inside the official php:X.Y-zts image (the only
171-
# convenient source of prebuilt ZTS binaries — Sury doesn't ship
172-
# them). pfp runs inside the same container so we don't need to
173-
# plumb pid namespaces between host and container.
174-
run: |
175-
cat > /tmp/spin.php <<'PHP'
176-
<?php
177-
class W { public function spin(): never { while (true) $this->a(); }
178-
public function a(): void { $this->b(); }
179-
public function b(): void { usleep(500); } }
180-
(new W())->spin();
181-
PHP
182-
BIN=./target/${{ matrix.target }}/release/pfp
183-
docker run -d --name php-zts \
184-
--cap-add=SYS_PTRACE \
185-
-v /tmp/spin.php:/spin.php:ro \
186-
-v "$(realpath $BIN):/pfp:ro" \
187-
${{ matrix.php_image }} \
188-
php /spin.php
189-
sleep 1
190-
docker exec php-zts php -i 2>/dev/null | grep -i 'thread safety' \
191-
| grep -qi enabled
192-
# php is the container's PID 1 — the docker-php-entrypoint execs
193-
# the command — and the php:*-zts image ships no pgrep (no procps).
194-
PHP_PID=1
195-
docker exec php-zts /pfp -p "$PHP_PID" -d 2 -H 99 -o /tmp/out.txt
196-
OUT=$(docker exec php-zts cat /tmp/out.txt)
197-
docker rm -f php-zts
198-
echo "$OUT" | head -20
199-
test "$(echo "$OUT" | grep -c '^0 ')" -ge 100
200-
echo "$OUT" | grep -q 'W::a'
201-
echo "$OUT" | grep -q 'W::b'
141+
run: ./scripts/ci/smoke-zts.sh ./target/${{ matrix.target }}/release/pfp ${{ matrix.php_image }}
202142

203143
smoke-pyroscope:
204144
name: smoke-pyroscope
@@ -218,66 +158,9 @@ jobs:
218158
- uses: Swatinem/rust-cache@v2
219159
with:
220160
key: x86_64-unknown-linux-gnu-smoke-pyroscope
221-
- name: install Sury PHP 8.3
222-
run: |
223-
sudo apt-get update
224-
sudo apt-get install -y --no-install-recommends \
225-
curl ca-certificates gnupg lsb-release
226-
curl -sSLo /tmp/sury.gpg https://packages.sury.org/php/apt.gpg
227-
sudo install -m 644 /tmp/sury.gpg /usr/share/keyrings/sury.gpg
228-
. /etc/os-release
229-
echo "deb [signed-by=/usr/share/keyrings/sury.gpg] https://packages.sury.org/php/ ${VERSION_CODENAME} main" \
230-
| sudo tee /etc/apt/sources.list.d/sury.list
231-
sudo apt-get update
232-
sudo apt-get install -y --no-install-recommends php8.3-cli
161+
- name: install PHP
162+
run: ./scripts/ci/install-php.sh
233163
- name: build
234164
run: cargo build --release
235-
- name: allow ptrace
236-
run: echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope || true
237-
- name: push profiles to pyroscope and query them back
238-
run: |
239-
set -euo pipefail
240-
cat > /tmp/spin.php <<'PHP'
241-
<?php
242-
class W { public function spin(): never { while (true) $this->a(); }
243-
public function a(): void { $this->b(); }
244-
public function b(): void { usleep(500); } }
245-
(new W())->spin();
246-
PHP
247-
php8.3 /tmp/spin.php &
248-
PHP_PID=$!
249-
sleep 1
250-
# Run pfp continuously in sidecar/push mode; keep pushing while we poll.
251-
RUST_LOG=pfp=debug ./target/release/pfp \
252-
-p "$PHP_PID" --pyroscope-url http://localhost:4040 \
253-
--pyroscope-app pfp-smoke --push-interval-secs 2 -H 99 \
254-
> /tmp/pfp.log 2>&1 &
255-
PFP_PID=$!
256-
257-
# 1) Hard gate: pfp attached and pushed without errors. Pyroscope
258-
# answers /ingest with 422 on a malformed pprof (which the sink
259-
# logs as "push failed"), so a clean push proves the profile was
260-
# parsed and accepted end-to-end by the server.
261-
sleep 12
262-
cat /tmp/pfp.log
263-
grep -q 'attached pid=' /tmp/pfp.log
264-
grep -q 'pushed profile to pyroscope' /tmp/pfp.log
265-
! grep -q 'push failed' /tmp/pfp.log
266-
267-
# 2) Best-effort: confirm the profile is queryable with our PHP frames.
268-
# Pyroscope's ingest->queryable lag is timing-dependent, so this is
269-
# logged but does not gate the job (the push gate above is the
270-
# deterministic e2e signal).
271-
for i in $(seq 1 30); do
272-
now=$(date +%s%3N); from=$((now - 600000))
273-
body=$(printf '{"profile_typeID":"process_cpu:samples:count::","label_selector":"{service_name=\\"pfp-smoke\\"}","start":%s,"end":%s}' "$from" "$now")
274-
names=$(curl -s -X POST \
275-
http://localhost:4040/querier.v1.QuerierService/SelectMergeStacktraces \
276-
-H 'Content-Type: application/json' -d "$body" \
277-
| python3 -c 'import sys,json; print("\n".join(json.load(sys.stdin).get("flamegraph",{}).get("names",[])))' 2>/dev/null || true)
278-
if echo "$names" | grep -q 'W::a' && echo "$names" | grep -q 'W::b'; then
279-
echo "queried PHP frames back from pyroscope:"; echo "$names"; break
280-
fi
281-
sleep 3
282-
done
283-
kill "$PFP_PID" "$PHP_PID" 2>/dev/null || true
165+
- name: smoke (pyroscope)
166+
run: ./scripts/ci/smoke-pyroscope.sh ./target/release/pfp

scripts/ci/install-php.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env bash
2+
# Install the Sury-packaged PHP 8.3 CLI on a Debian/Ubuntu runner.
3+
set -euo pipefail
4+
5+
sudo apt-get update
6+
sudo apt-get install -y --no-install-recommends \
7+
curl ca-certificates gnupg lsb-release
8+
curl -sSLo /tmp/sury.gpg https://packages.sury.org/php/apt.gpg
9+
sudo install -m 644 /tmp/sury.gpg /usr/share/keyrings/sury.gpg
10+
# shellcheck disable=SC1091 # runtime file, not present at lint time
11+
. /etc/os-release
12+
echo "deb [signed-by=/usr/share/keyrings/sury.gpg] https://packages.sury.org/php/ ${VERSION_CODENAME} main" \
13+
| sudo tee /etc/apt/sources.list.d/sury.list
14+
sudo apt-get update
15+
sudo apt-get install -y --no-install-recommends php8.3-cli

scripts/ci/smoke-pyroscope.sh

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env bash
2+
# End-to-end test of continuous Pyroscope export: run pfp in push mode against a
3+
# live PHP and a Pyroscope server.
4+
#
5+
# Usage: scripts/ci/smoke-pyroscope.sh <pfp-binary> [pyroscope-url]
6+
set -euo pipefail
7+
8+
PFP=${1:?usage: smoke-pyroscope.sh <pfp-binary> [pyroscope-url]}
9+
URL=${2:-http://localhost:4040}
10+
here=$(cd "$(dirname "$0")" && pwd)
11+
12+
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope >/dev/null || true
13+
14+
php8.3 "$here/spin.php" &
15+
PHP_PID=$!
16+
# Run pfp continuously in sidecar/push mode; keep pushing while we poll.
17+
RUST_LOG=pfp=debug "$PFP" -p "$PHP_PID" --pyroscope-url "$URL" \
18+
--pyroscope-app pfp-smoke --push-interval-secs 2 -H 99 \
19+
> /tmp/pfp.log 2>&1 &
20+
PFP_PID=$!
21+
trap 'kill "$PFP_PID" "$PHP_PID" 2>/dev/null || true' EXIT
22+
23+
# 1) Hard gate: pfp attached and pushed without errors. Pyroscope answers
24+
# /ingest with 422 on a malformed pprof (which the sink logs as "push
25+
# failed"), so a clean push proves the profile was parsed and accepted
26+
# end-to-end by the server.
27+
sleep 12
28+
cat /tmp/pfp.log
29+
grep -q 'attached pid=' /tmp/pfp.log
30+
grep -q 'pushed profile to pyroscope' /tmp/pfp.log
31+
if grep -q 'push failed' /tmp/pfp.log; then
32+
echo "pfp reported push failures" >&2
33+
exit 1
34+
fi
35+
36+
# 2) Best-effort: confirm the profile is queryable with our PHP frames.
37+
# Pyroscope's ingest->queryable lag is timing-dependent, so this is logged
38+
# but does not gate the job (the push gate above is the deterministic
39+
# e2e signal).
40+
for _ in $(seq 1 30); do
41+
now=$(date +%s%3N)
42+
from=$((now - 600000))
43+
body=$(printf '{"profile_typeID":"process_cpu:samples:count::","label_selector":"{service_name=\\"pfp-smoke\\"}","start":%s,"end":%s}' "$from" "$now")
44+
names=$(curl -s -X POST "$URL/querier.v1.QuerierService/SelectMergeStacktraces" \
45+
-H 'Content-Type: application/json' -d "$body" \
46+
| python3 -c 'import sys,json; print("\n".join(json.load(sys.stdin).get("flamegraph",{}).get("names",[])))' 2>/dev/null || true)
47+
if echo "$names" | grep -q 'W::a' && echo "$names" | grep -q 'W::b'; then
48+
echo "queried PHP frames back from pyroscope:"
49+
echo "$names"
50+
break
51+
fi
52+
sleep 3
53+
done

scripts/ci/smoke-zts.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env bash
2+
# Smoke test against a ZTS PHP. Runs the ZTS PHP inside the official php:X.Y-zts
3+
# image (the only convenient source of prebuilt ZTS binaries — Sury doesn't ship
4+
# them); pfp runs inside the same container so there's no pid-namespace plumbing
5+
# between host and container.
6+
#
7+
# Usage: scripts/ci/smoke-zts.sh <pfp-binary> <php-zts-image>
8+
set -euo pipefail
9+
10+
PFP=${1:?usage: smoke-zts.sh <pfp-binary> <php-zts-image>}
11+
IMAGE=${2:?usage: smoke-zts.sh <pfp-binary> <php-zts-image>}
12+
spin=$(cd "$(dirname "$0")" && pwd)/spin.php
13+
14+
docker run -d --name php-zts --cap-add=SYS_PTRACE \
15+
-v "$spin:/spin.php:ro" \
16+
-v "$(realpath "$PFP"):/pfp:ro" \
17+
"$IMAGE" php /spin.php
18+
trap 'docker rm -f php-zts >/dev/null 2>&1 || true' EXIT
19+
sleep 1
20+
21+
docker exec php-zts php -i 2>/dev/null | grep -i 'thread safety' | grep -qi enabled
22+
# php is the container's PID 1 — the docker-php-entrypoint execs the command —
23+
# and the php:*-zts image ships no pgrep (no procps).
24+
docker exec php-zts /pfp -p 1 -d 2 -H 99 -o /tmp/out.txt
25+
OUT=$(docker exec php-zts cat /tmp/out.txt)
26+
27+
echo "$OUT" | head -20
28+
test "$(echo "$OUT" | grep -c '^0 ')" -ge 100
29+
echo "$OUT" | grep -q 'W::a'
30+
echo "$OUT" | grep -q 'W::b'

scripts/ci/smoke.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env bash
2+
# Smoke test against a live NTS PHP 8.3: profile a busy loop and assert the
3+
# named frames show up.
4+
#
5+
# Usage: scripts/ci/smoke.sh <pfp-binary>
6+
set -euo pipefail
7+
8+
PFP=${1:?usage: smoke.sh <pfp-binary>}
9+
here=$(cd "$(dirname "$0")" && pwd)
10+
11+
# Same-UID process_vm_readv needs ptrace_scope relaxed on the runner.
12+
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope >/dev/null || true
13+
14+
php8.3 "$here/spin.php" &
15+
PID=$!
16+
trap 'kill "$PID" 2>/dev/null || true' EXIT
17+
sleep 1
18+
19+
"$PFP" -p "$PID" -d 2 -H 99 -o /tmp/out.txt
20+
21+
test "$(grep -c '^0 ' /tmp/out.txt)" -ge 100
22+
grep -q 'W::a' /tmp/out.txt
23+
grep -q 'W::b' /tmp/out.txt

scripts/ci/spin.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
// A tiny, deterministic CPU-busy workload for the smoke tests: a known class
3+
// (W) with named methods (a/b) so we can assert the profiler captured them.
4+
class W {
5+
public function spin(): never { while (true) $this->a(); }
6+
public function a(): void { $this->b(); }
7+
public function b(): void { usleep(500); }
8+
}
9+
(new W())->spin();

0 commit comments

Comments
 (0)