Skip to content

Commit 5094f03

Browse files
feat(parse-server-mongo): compat-lane-ready sample + JS line coverage gate (#97)
* feat(parse-server-mongo): subcommand flow + env-driven compose + noise template Make the parse-server-mongo sample directly consumable as a keploy compat-lane sample, on par with samples-typescript/umami-postgres and samples-python/ doccano-django. - flow.sh refactored to subcommand form: bootstrap | record-traffic | coverage | list-routes. bootstrap is idempotent (4xx during signup is treated as already-exists), persists the session token to /tmp/parse-token-${PARSE_PHASE}, and preserves the 3-second post-/health pause that the boot-phase _SCHEMA divergence reproducer needs. - record-traffic ports the broad parse-server REST + GraphQL surface (51 fire calls across users/sessions/classes/roles/files/cloud-functions/ schemas/hooks/graphql/aggregate). Every curl is preceded by log_fired so PARSE_FIRED_ROUTES_FILE captures (method,route) for coverage. The multi-class _SCHEMA mutation (GameScore, PlayerStats, Achievement) is retained so the boot-phase tiebreaker reproducer still triggers. - coverage subcommand reports (method,route) coverage with numerator from keploy/test-set-*/tests/*.yaml when present, else from the fired-routes log. Denominator from a curated 35-route table in parse_list_routes. - docker-compose.yml takes ${VAR:-default} for project name, network name, network subnet, IPs, container names, host:container port, and the internal PORT env. Defaults preserve standalone `docker compose up`; overrides let concurrent matrix cells share a single docker daemon. - keploy.yml.template carries the parse-server noise filter (header.Date, body.objectId, body.sessionToken, body.createdAt, body.updatedAt) so a lane consumer can apply it to keploy.yml after `keploy config --generate`. - README rewritten to document the four subcommands, all env vars, the route surface covered, the boot-phase divergence rationale, and a concurrent-cell run recipe. Signed-off-by: Akash Kumar <meakash7902@gmail.com> * 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> * ci(parse-server-mongo): detect step also requires helper script on base ref parse-server-mongo/ already exists on main (PRs #94/#95), so the prior detect step returned true on every PR's release-coverage job and the measure step then tried to invoke a helper script that didn't exist on the base ref. Extending detect to require the helper script too lets the first-PR escape hatch fire when the workflow itself is the change being introduced. Signed-off-by: Akash Kumar <meakash7902@gmail.com> * feat(parse-server-mongo): real JS line coverage via NODE_V8_COVERAGE Replaces the prior API-route-surface "coverage" (counting fired routes / curated route table) with actual V8 line coverage of the parse-server library code under node_modules/parse-server/lib. Architecture: - `Dockerfile.coverage` extends the base image with a graceful- shutdown shim (coverage-entrypoint.js installs SIGTERM/SIGINT handlers calling process.exit(0)) so V8 actually flushes its coverage data on `compose stop`. Without that shim, express's app.listen pins the loop and the kernel signal-kills node (exit 143) before NODE_V8_COVERAGE writes anything. - `docker-compose.coverage.yml` is an OVERLAY: applied via `-f docker-compose.yml -f docker-compose.coverage.yml`. It sets NODE_V8_COVERAGE=/coverage and bind-mounts the host ./coverage dir. The base `Dockerfile` and `docker-compose.yml` are untouched, so keploy/integrations and keploy/enterprise CI lanes consume the base compose and pay zero coverage cost. - `coverage-report.js` reads V8's per-process JSON dumps and produces a `Covered N/M (XX.X%)` summary plus a c8-shaped coverage-summary.json. We don't use `c8 report` because c8 filters node_modules even when --include is set, and parse-server lives entirely under node_modules/parse-server. The custom tool walks V8's nested ranges (block coverage) and resolves per-line execution count to the deepest range. - `flow.sh::parse_coverage` shells into the coverage image to run coverage-report.js against the dumped V8 data; when called against the base image (no overlay) it prints "INFO: ... uninstrumented" and exits 0 so enterprise lanes' `flow.sh coverage || true` informational calls keep working. Removed: - `parse_list_routes` (curated route table denominator). - `parse_collect_recorded_routes` (keploy-tests / fired-routes reader). - The legacy route-surface `parse_coverage` body. - `list-routes` subcommand. Validated locally: helper produced `coverage=38.7` to GITHUB_OUTPUT against a fresh stack (signup + record-traffic + clean stop). 38.7% reflects coverage of node_modules/parse-server/lib — mongo paths ~46%, REST ~52%, Auth ~40%, schema ~58%; postgres adapter ~7% (mongo storage selected) and LiveQuery ~3% (not exercised), which is the expected distribution. Signed-off-by: Akash Kumar <meakash7902@gmail.com> * ci(parse-server-mongo): drop trailing prose from sticky comment Signed-off-by: Akash Kumar <meakash7902@gmail.com> * docs(parse-server-mongo): document coverage overlay; drop list-routes/FIRED_ROUTES refs Signed-off-by: Akash Kumar <meakash7902@gmail.com> --------- Signed-off-by: Akash Kumar <meakash7902@gmail.com>
1 parent ac33825 commit 5094f03

10 files changed

Lines changed: 1145 additions & 136 deletions

File tree

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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 ] && [ -f .github/workflows/scripts/run-and-measure-parse-server.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.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env bash
2+
#
3+
# run-and-measure-parse-server.sh — bring parse-server + mongo up
4+
# under the coverage overlay, run flow.sh bootstrap + record-traffic,
5+
# stop parse-server cleanly so V8 flushes NODE_V8_COVERAGE, run
6+
# flow.sh coverage, and emit `coverage=PCT` onto $GITHUB_OUTPUT
7+
# for the downstream coverage-gate job.
8+
#
9+
# Coverage isolation contract:
10+
# * Base `Dockerfile` and `docker-compose.yml` are untouched.
11+
# * The overlay `Dockerfile.coverage` + `docker-compose.coverage.yml`
12+
# installs the V8 coverage entrypoint shim and sets
13+
# NODE_V8_COVERAGE. ONLY this script applies the overlay; the
14+
# keploy/integrations and keploy/enterprise CI lanes consume
15+
# the base compose and pay zero coverage-instrumentation cost.
16+
#
17+
# Inputs (from the workflow env):
18+
# PARSE_PHASE — label spliced into flow.sh's token-file slot
19+
# so build vs. release runs don't collide.
20+
# GITHUB_OUTPUT — standard GH Actions sink for step outputs.
21+
set -Eeuo pipefail
22+
23+
export PARSE_APP_CONTAINER="${PARSE_APP_CONTAINER:-parse-server-mongo-app}"
24+
export PARSE_MONGO_CONTAINER="${PARSE_MONGO_CONTAINER:-parse-server-mongo-mongo}"
25+
export PARSE_HOST_PORT="${PARSE_HOST_PORT:-6100}"
26+
export PARSE_CONTAINER_PORT="${PARSE_CONTAINER_PORT:-6100}"
27+
export PARSE_APP_ID="${PARSE_APP_ID:-keploy-parse-app}"
28+
export PARSE_MASTER_KEY="${PARSE_MASTER_KEY:-keploy-parse-master}"
29+
export PARSE_MOUNT_PATH="${PARSE_MOUNT_PATH:-/parse}"
30+
export APP_PORT="${APP_PORT:-${PARSE_HOST_PORT}}"
31+
32+
mkdir -p coverage
33+
chmod 777 coverage # node UID inside container differs from runner UID
34+
sudo rm -rf coverage/coverage-* coverage/coverage_report.txt coverage/coverage-summary.json 2>/dev/null \
35+
|| rm -rf coverage/coverage-* coverage/coverage_report.txt coverage/coverage-summary.json 2>/dev/null \
36+
|| true
37+
38+
COMPOSE=(docker compose -f docker-compose.yml -f docker-compose.coverage.yml)
39+
40+
# Bring up parse-server + mongo under the coverage overlay. The
41+
# Dockerfile.coverage layer wraps node so SIGTERM produces a clean
42+
# `process.exit(0)` (otherwise express's app.listen pins the loop
43+
# and signal-kills bypass V8's coverage flush).
44+
"${COMPOSE[@]}" up -d --build
45+
46+
# Wait for /parse/health to return 200.
47+
for i in $(seq 1 120); do
48+
code=$(curl -sS -o /dev/null -w '%{http_code}' \
49+
-H "X-Parse-Application-Id: ${PARSE_APP_ID}" \
50+
"http://127.0.0.1:${PARSE_HOST_PORT}${PARSE_MOUNT_PATH}/health" 2>/dev/null || echo "")
51+
if [ "$code" = "200" ]; then break; fi
52+
sleep 2
53+
done
54+
55+
# Idempotent signup + session-token persistence under
56+
# /tmp/parse-token-${PARSE_PHASE}.
57+
bash flow.sh bootstrap 240
58+
59+
# Exercise the REST + GraphQL surface.
60+
bash flow.sh record-traffic
61+
62+
# Stop parse-server cleanly so the SIGTERM handler's process.exit(0)
63+
# fires and V8 flushes NODE_V8_COVERAGE.
64+
"${COMPOSE[@]}" stop -t 30 parse-server
65+
66+
# Generate the coverage report from the V8 dumps. flow.sh::parse_coverage
67+
# launches a one-off container against the same coverage volume.
68+
bash flow.sh coverage
69+
70+
if [ ! -f coverage_report.txt ]; then
71+
echo "::error::flow.sh coverage produced no coverage_report.txt"
72+
exit 1
73+
fi
74+
75+
# Parse `Covered N/M (XX.X%)` — anchored on the parenthesised form
76+
# so a future report-prose change doesn't break the parse.
77+
pct=$(grep -oE '\([0-9]+\.[0-9]+%\)' coverage_report.txt | head -1 | tr -d '()%')
78+
if [ -z "$pct" ]; then
79+
echo "::error::Could not parse coverage percentage from coverage_report.txt"
80+
cat coverage_report.txt || true
81+
exit 1
82+
fi
83+
echo "coverage=${pct}" >>"$GITHUB_OUTPUT"
84+
echo "coverage: ${pct}% (JS line coverage via NODE_V8_COVERAGE + custom report)"
85+
86+
"${COMPOSE[@]}" down -v --remove-orphans

parse-server-mongo/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
coverage/
2+
coverage_report.txt
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Coverage overlay image for parse-server-mongo.
2+
#
3+
# Extends the base sample image build chain (node:20-bookworm-slim +
4+
# parse-server deps + index.js) with c8 (for `c8 report`) and a
5+
# tiny JavaScript entrypoint shim that registers SIGTERM/SIGINT
6+
# handlers calling process.exit(0) — without that, parse-server's
7+
# express server pins the event loop and signal-driven kills
8+
# bypass V8's coverage flush, leaving NODE_V8_COVERAGE empty.
9+
#
10+
# IMPORTANT: this image is only consumed by docker-compose.coverage.yml.
11+
# The base Dockerfile and docker-compose.yml stay uninstrumented so
12+
# enterprise's keploy compat lane pays no coverage-instrumentation
13+
# cost.
14+
FROM node:20-bookworm-slim
15+
16+
RUN apt-get update && \
17+
apt-get install -y --no-install-recommends ca-certificates curl dumb-init && \
18+
rm -rf /var/lib/apt/lists/*
19+
20+
WORKDIR /usr/src/app
21+
22+
COPY package*.json ./
23+
RUN npm install --omit=dev
24+
25+
COPY index.js ./
26+
27+
# c8 is the report generator (we use NODE_V8_COVERAGE for raw data
28+
# collection at runtime, then `c8 report` post-hoc to produce
29+
# json-summary / lcov). Installing globally keeps the app's own
30+
# node_modules byte-identical to the base image.
31+
RUN npm install -g c8@10.1.2
32+
33+
# Graceful-shutdown shim: parse-server's app.listen() pins the
34+
# event loop, so a `compose stop` (SIGTERM) would kill node by
35+
# signal — exit code 143 — before V8's NODE_V8_COVERAGE writer
36+
# runs. Calling process.exit(0) from a SIGTERM handler turns the
37+
# kill into a clean exit, so V8 dumps coverage to NODE_V8_COVERAGE
38+
# before the process terminates.
39+
RUN printf "process.on('SIGTERM', () => process.exit(0));\nprocess.on('SIGINT', () => process.exit(0));\nrequire('/usr/src/app/index.js');\n" \
40+
> /usr/src/app/coverage-entrypoint.js
41+
42+
EXPOSE 1337
43+
44+
ENTRYPOINT ["dumb-init", "--"]
45+
CMD ["node", "/usr/src/app/coverage-entrypoint.js"]

0 commit comments

Comments
 (0)