Skip to content

Commit 67e104c

Browse files
ci(parse-server-mongo): add per-sample coverage gate workflow
Introduce a GitHub Actions workflow scoped to the parse-server-mongo sample that runs the same end-to-end record/coverage shape as the doccano-django and umami-postgres lanes. The workflow is path-filtered to `parse-server-mongo/**` (plus the workflow file and its helper script) so it does NOT trigger on changes to other samples in this repo or on changes elsewhere. Three jobs: * build-coverage — runs the sample end-to-end against the PR's HEAD ref; emits a coverage percentage. * release-coverage — same, against the PR's base ref; first-PR bootstrap escape hatch returns 0% if the sample doesn't exist on the base ref yet. * coverage-gate — fails the PR if HEAD coverage drops more than COVERAGE_THRESHOLD percentage points below base. Default 1.0pp; override via the repo variable PARSE_COVERAGE_THRESHOLD. A sticky PR comment surfaces the base-vs-PR coverage diff with a prose explanation of which parse-server REST + GraphQL routes the sample's flow.sh::parse_record_traffic exercises. Helper script .github/workflows/scripts/run-and-measure-parse-server.sh is per-sample (sibling samples will land their own run-and-measure-<sample>.sh on parallel branches). It does docker compose up -d --build, polls /parse/health, runs flow.sh bootstrap → record-traffic → coverage with PARSE_FIRED_ROUTES_FILE as the standalone numerator, parses the report, and emits coverage=PCT to GITHUB_OUTPUT. This gate runs ONLY on PRs touching parse-server-mongo/. The enterprise PR pipeline (.woodpecker/parse-server-linux.yml) calls flow.sh coverage informationally and does NOT gate; the gate lives here on the sample repo, isolated from the enterprise lane. Signed-off-by: Akash Kumar <meakash7902@gmail.com>
1 parent 1035cf5 commit 67e104c

2 files changed

Lines changed: 297 additions & 0 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# parse-server-mongo sample CI — keploy-independent end-to-end smoke +
2+
# coverage gate.
3+
#
4+
# Triggers ONLY on changes under parse-server-mongo/ (or this workflow
5+
# file). Other samples in this repo have their own orthogonal CI;
6+
# gating the whole repo on every parse-server change would slow them
7+
# all down for no benefit.
8+
#
9+
# What it gates:
10+
# * `release-coverage` — checks out the PR's base branch (main)
11+
# and runs the sample end-to-end: docker compose up, bootstrap
12+
# the fixed user + session, drive flow.sh record-traffic with
13+
# the per-call audit log enabled, capture the route-coverage
14+
# percentage from `flow.sh coverage`. This is the baseline.
15+
# * `build-coverage` — same end-to-end against the PR's HEAD ref.
16+
# * `coverage-gate` — fails the PR if `build`'s coverage drops
17+
# more than COVERAGE_THRESHOLD percentage points below
18+
# `release`. Default threshold is 1.0pp; override via repo
19+
# variable `PARSE_COVERAGE_THRESHOLD` for a tighter or
20+
# looser bar.
21+
#
22+
# On push to main, only `build-coverage` runs (no baseline to
23+
# compare against — main IS the baseline).
24+
#
25+
# Standards-aligned choices:
26+
# * `paths:` filter on both push and pull_request triggers — the
27+
# canonical GH Actions way to scope a workflow to one
28+
# subdirectory.
29+
# * Job outputs (steps.<id>.outputs.coverage → needs.<job>.outputs)
30+
# to thread the captured percentage between jobs.
31+
# * `concurrency:` cancel-in-progress on the same ref so a stale
32+
# run doesn't waste runner minutes.
33+
# * actions/upload-artifact for the human-readable
34+
# coverage_report.txt — reviewers can inspect missing routes
35+
# directly from the PR's "checks" tab.
36+
# * marocchino/sticky-pull-request-comment for the PR-side diff
37+
# comment. Pinned-by-header so successive runs update the same
38+
# comment instead of fanning out.
39+
# * The compare step is plain bash + python3 (no external
40+
# coverage service). The sample's coverage is a single
41+
# route-based percentage, so the gate is a 3-line subtraction.
42+
#
43+
# Sample is genuinely keploy-independent here: the workflow uses
44+
# flow.sh's $PARSE_FIRED_ROUTES_FILE per-call audit log as its
45+
# numerator source, not a keploy recording. The lane scripts in
46+
# keploy/integrations and keploy/enterprise consume the same
47+
# flow.sh, but use the keploy/test-set-*/tests/*.yaml tree as
48+
# their numerator (authoritative — only calls keploy actually
49+
# CAPTURED count). Both modes are wired into
50+
# `flow.sh::parse_collect_recorded_routes`.
51+
name: parse-server-mongo sample
52+
53+
on:
54+
pull_request:
55+
paths:
56+
- 'parse-server-mongo/**'
57+
- '.github/workflows/parse-server-mongo.yml'
58+
- '.github/workflows/scripts/run-and-measure-parse-server.sh'
59+
push:
60+
branches: [main]
61+
paths:
62+
- 'parse-server-mongo/**'
63+
- '.github/workflows/parse-server-mongo.yml'
64+
- '.github/workflows/scripts/run-and-measure-parse-server.sh'
65+
workflow_dispatch: {}
66+
67+
concurrency:
68+
group: parse-server-mongo-${{ github.ref }}
69+
cancel-in-progress: true
70+
71+
env:
72+
COVERAGE_THRESHOLD: ${{ vars.PARSE_COVERAGE_THRESHOLD || '1.0' }}
73+
74+
jobs:
75+
build-coverage:
76+
name: build (current ref) coverage
77+
runs-on: ubuntu-latest
78+
timeout-minutes: 20
79+
outputs:
80+
coverage: ${{ steps.measure.outputs.coverage }}
81+
steps:
82+
- uses: actions/checkout@v4
83+
- id: measure
84+
name: Run sample end-to-end + measure coverage
85+
working-directory: parse-server-mongo
86+
env:
87+
PARSE_FIRED_ROUTES_FILE: ${{ runner.temp }}/fired-routes-build.log
88+
PARSE_PHASE: ci-build
89+
run: ../.github/workflows/scripts/run-and-measure-parse-server.sh
90+
91+
- name: Upload coverage report
92+
if: always()
93+
uses: actions/upload-artifact@v4
94+
with:
95+
name: coverage-build
96+
path: parse-server-mongo/coverage_report.txt
97+
if-no-files-found: warn
98+
99+
release-coverage:
100+
if: github.event_name == 'pull_request'
101+
name: release (base ref) coverage
102+
runs-on: ubuntu-latest
103+
timeout-minutes: 20
104+
outputs:
105+
coverage: ${{ steps.measure.outputs.coverage || steps.empty-baseline.outputs.coverage }}
106+
sample-existed: ${{ steps.detect.outputs.sample-existed }}
107+
steps:
108+
- uses: actions/checkout@v4
109+
with:
110+
ref: ${{ github.event.pull_request.base.ref }}
111+
112+
# First-PR bootstrap escape hatch: the very PR that
113+
# introduces the parse-server-mongo/ sample has no baseline
114+
# (parse-server-mongo/ doesn't exist on the base ref). Detect
115+
# that and short-circuit to coverage=0; the gate then
116+
# treats build's coverage as the new baseline and trivially
117+
# passes for any percentage > 0. After the introducing PR
118+
# merges, every subsequent PR has a real baseline to diff
119+
# against.
120+
- id: detect
121+
name: Detect baseline presence
122+
run: |
123+
if [ -d parse-server-mongo ] && [ -x parse-server-mongo/flow.sh ]; then
124+
echo "sample-existed=true" >>"$GITHUB_OUTPUT"
125+
echo "Sample exists on base ref — running full measurement."
126+
else
127+
echo "sample-existed=false" >>"$GITHUB_OUTPUT"
128+
echo "No parse-server-mongo/ on base ref — first-PR bootstrap; baseline coverage treated as 0%."
129+
fi
130+
131+
- id: measure
132+
name: Run sample end-to-end + measure coverage
133+
if: steps.detect.outputs.sample-existed == 'true'
134+
working-directory: parse-server-mongo
135+
env:
136+
PARSE_FIRED_ROUTES_FILE: ${{ runner.temp }}/fired-routes-release.log
137+
PARSE_PHASE: ci-release
138+
run: ../.github/workflows/scripts/run-and-measure-parse-server.sh
139+
140+
- id: empty-baseline
141+
name: Emit zero baseline (first-PR bootstrap)
142+
if: steps.detect.outputs.sample-existed != 'true'
143+
run: echo "coverage=0.0" >>"$GITHUB_OUTPUT"
144+
145+
- name: Upload coverage report
146+
if: always() && steps.detect.outputs.sample-existed == 'true'
147+
uses: actions/upload-artifact@v4
148+
with:
149+
name: coverage-release
150+
path: parse-server-mongo/coverage_report.txt
151+
if-no-files-found: warn
152+
153+
coverage-gate:
154+
if: github.event_name == 'pull_request'
155+
name: coverage gate
156+
needs: [build-coverage, release-coverage]
157+
runs-on: ubuntu-latest
158+
steps:
159+
- name: Compare build vs release
160+
env:
161+
BUILD: ${{ needs.build-coverage.outputs.coverage }}
162+
RELEASE: ${{ needs.release-coverage.outputs.coverage }}
163+
THRESHOLD: ${{ env.COVERAGE_THRESHOLD }}
164+
BASE_REF: ${{ github.event.pull_request.base.ref }}
165+
run: |
166+
set -Eeuo pipefail
167+
if [ -z "${BUILD:-}" ] || [ -z "${RELEASE:-}" ]; then
168+
echo "::error::missing coverage outputs — build='${BUILD:-}' release='${RELEASE:-}'"
169+
exit 1
170+
fi
171+
drop=$(python3 -c "print(round(${RELEASE} - ${BUILD}, 2))")
172+
echo "Release (${BASE_REF}): ${RELEASE}%"
173+
echo "Build (this PR): ${BUILD}%"
174+
echo "Drop: ${drop}pp (threshold ${THRESHOLD}pp)"
175+
if python3 -c "import sys; sys.exit(0 if (${RELEASE} - ${BUILD}) > ${THRESHOLD} else 1)"; then
176+
echo "::error::parse-server-mongo coverage dropped from ${RELEASE}% → ${BUILD}% (-${drop}pp), exceeding the ${THRESHOLD}pp threshold."
177+
echo "Suggested actions:"
178+
echo " * Add curl(s) to flow.sh::parse_record_traffic that exercise the routes you changed/touched."
179+
echo " * If the route(s) was intentionally retired, drop it from parse-server-mongo/flow.sh::parse_list_routes too so it's removed from the denominator."
180+
exit 1
181+
fi
182+
echo "OK — coverage delta within ${THRESHOLD}pp threshold."
183+
184+
- name: Sticky PR comment
185+
if: ${{ !cancelled() }}
186+
uses: marocchino/sticky-pull-request-comment@v2
187+
with:
188+
header: parse-server-mongo-coverage
189+
message: |
190+
### parse-server-mongo sample coverage
191+
192+
| ref | coverage |
193+
|---|---|
194+
| base (`${{ github.event.pull_request.base.ref }}`) | **${{ needs.release-coverage.outputs.coverage }}%** |
195+
| this PR | **${{ needs.build-coverage.outputs.coverage }}%** |
196+
197+
Threshold: PR may not drop coverage by more than **${{ env.COVERAGE_THRESHOLD }}pp**. Override per-repo via the `PARSE_COVERAGE_THRESHOLD` actions variable.
198+
199+
Coverage measures the parse-server REST + GraphQL surface (`/parse/users` + `/parse/sessions` + `/parse/classes/*` + `/parse/roles` + `/parse/files/*` + `/parse/functions/*` + `/parse/schemas/*` + `/parse/hooks/*` + `/parse/graphql` + `/parse/aggregate/*` + health) that `flow.sh::parse_record_traffic` exercises against the running backend. Reports are attached as artifacts on each job ("coverage-build" / "coverage-release").
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env bash
2+
#
3+
# run-and-measure-parse-server.sh — bring parse-server + mongo up via
4+
# the sample's compose, run flow.sh bootstrap + record-traffic with
5+
# the per-call audit log enabled, run flow.sh coverage, and emit
6+
# `coverage=PCT` onto $GITHUB_OUTPUT for the downstream
7+
# coverage-gate job.
8+
#
9+
# Called from .github/workflows/parse-server-mongo.yml's
10+
# build-coverage and release-coverage jobs (one per ref under
11+
# comparison). Both jobs source the same script so the
12+
# measurement is identical across refs — any drift in the
13+
# numerator definition would otherwise produce a misleading
14+
# delta.
15+
#
16+
# Inputs (all from the workflow env):
17+
# PARSE_FIRED_ROUTES_FILE — per-call audit log path; passed
18+
# through to flow.sh so its
19+
# record-traffic loop logs each
20+
# (METHOD, URL) pair, and so its
21+
# coverage subcommand uses that
22+
# file as the standalone numerator.
23+
# PARSE_PHASE — label spliced into flow.sh's
24+
# token-file slot so build vs.
25+
# release runs don't collide. Also
26+
# surfaced in the coverage report
27+
# header for log diffing.
28+
# GITHUB_OUTPUT — standard GH Actions sink for step
29+
# outputs.
30+
set -Eeuo pipefail
31+
32+
# Defaults match the sample's docker-compose.yml env-substituted
33+
# vars; exporting them makes it explicit which values the helper
34+
# is pinning so a future compose-side rename doesn't silently
35+
# desync the helper from the sample.
36+
export PARSE_APP_CONTAINER="${PARSE_APP_CONTAINER:-parse-server-mongo-app}"
37+
export PARSE_MONGO_CONTAINER="${PARSE_MONGO_CONTAINER:-parse-server-mongo-mongo}"
38+
export PARSE_HOST_PORT="${PARSE_HOST_PORT:-6100}"
39+
export PARSE_CONTAINER_PORT="${PARSE_CONTAINER_PORT:-6100}"
40+
export PARSE_APP_ID="${PARSE_APP_ID:-keploy-parse-app}"
41+
export PARSE_MASTER_KEY="${PARSE_MASTER_KEY:-keploy-parse-master}"
42+
export PARSE_MOUNT_PATH="${PARSE_MOUNT_PATH:-/parse}"
43+
# flow.sh reads APP_PORT (host-side) for the curl base; keep it
44+
# aligned with PARSE_HOST_PORT.
45+
export APP_PORT="${APP_PORT:-${PARSE_HOST_PORT}}"
46+
: "${PARSE_FIRED_ROUTES_FILE:?PARSE_FIRED_ROUTES_FILE must be set by the workflow}"
47+
48+
# Reset audit log for this run; otherwise a prior run's entries
49+
# would inflate the numerator on a re-trigger.
50+
: >"$PARSE_FIRED_ROUTES_FILE"
51+
52+
# Bring up parse-server + mongo. The sample's compose builds the
53+
# parse-server image from the Dockerfile in this directory and
54+
# wires mongo via a fixed-IP user-defined network so the URI is
55+
# stable across runs.
56+
docker compose up -d --build
57+
58+
# Wait for /parse/health to return 200. Cold parse-server boot on
59+
# a GH runner is mostly mongo init + npm start; budget ~120s.
60+
for i in $(seq 1 120); do
61+
code=$(curl -sS -o /dev/null -w '%{http_code}' \
62+
-H "X-Parse-Application-Id: ${PARSE_APP_ID}" \
63+
"http://127.0.0.1:${PARSE_HOST_PORT}${PARSE_MOUNT_PATH}/health" 2>/dev/null || echo "")
64+
if [ "$code" = "200" ]; then break; fi
65+
sleep 2
66+
done
67+
68+
# Single-phase: parse-server's compose has no SKIP_INIT-style
69+
# flag (mongo is empty on every fresh `compose up -d`), so
70+
# flow.sh::parse_bootstrap idempotently signs up the fixed user
71+
# and persists the session token under
72+
# /tmp/parse-token-${PARSE_PHASE}.
73+
bash flow.sh bootstrap 240
74+
75+
# Drive traffic. flow.sh::parse_record_traffic reads the
76+
# persisted session token and exercises the curated REST +
77+
# GraphQL surface against the running backend.
78+
bash flow.sh record-traffic
79+
80+
# Coverage report — uses PARSE_FIRED_ROUTES_FILE as numerator
81+
# since no keploy/test-set-* tree exists in the standalone case.
82+
# parse_coverage prints to stdout, so tee into the artifact path
83+
# the workflow uploads.
84+
bash flow.sh coverage | tee coverage_report.txt
85+
86+
# Pull the percentage out of the report's `Covered N/M (XX.X%)`
87+
# line. Anchored on the parenthesised form so a future change to
88+
# the report's prose doesn't break the parse.
89+
pct=$(grep -oE '\([0-9]+\.[0-9]+%\)' coverage_report.txt | head -1 | tr -d '()%')
90+
if [ -z "$pct" ]; then
91+
echo "::error::Could not parse coverage percentage from coverage_report.txt"
92+
cat coverage_report.txt || true
93+
exit 1
94+
fi
95+
echo "coverage=${pct}" >>"$GITHUB_OUTPUT"
96+
echo "coverage: ${pct}% (audit log: $PARSE_FIRED_ROUTES_FILE)"
97+
98+
docker compose down -v --remove-orphans

0 commit comments

Comments
 (0)