Skip to content

Commit aa16aa2

Browse files
feat(restheart-mongo): keploy compat lane sample + Java line coverage gate (#134)
* feat(restheart-mongo): keploy compat lane sample (scaffold) Mirrors the doccano-django sample shape: the sample owns orchestration (compose / bootstrap / traffic / coverage), keploy CI lanes consume it as a thin wrapper. This is a SCAFFOLD — the full traffic loop driven by the existing keploy/enterprise lane (`compat_trigger_record_traffic` in .ci/scripts/restheart-linux.sh, ~600 lines covering CRUD on /<db>/<coll> + GraphQL + files + ACL + users + bulk + aggregations) needs to be ported into flow.sh::restheart_record_traffic in a follow-up. The current loop is deliberately minimal (CRUD on a seed collection) which is enough to prove the sample boots end-to-end without keploy. Layout: Dockerfile — pin to softinstigate/restheart:9.2.1 docker-compose.yml — mongo:7 + restheart:9.2.1, env-driven flow.sh — bootstrap | record-traffic | coverage | list-routes keploy.yml.template — globalNoise for _etag/_oid/lastModified/Date README.md — handoff + status notes Signed-off-by: Akash Kumar <meakash7902@gmail.com> * feat(restheart-mongo): port full RESTHeart REST surface Replace the minimal record-traffic stub with the complete loop that the keploy compat lane needs to gate. flow.sh::restheart_record_traffic now drives the full RESTHeart 9.x surface end-to-end against bare RESTHeart, and restheart_list_routes enumerates every (method, route) tuple it fires so coverage stays in lockstep. Covered surfaces: - CRUD on /<db>/<coll> + /<db>/<coll>/<docid> (HAL, _size, _meta, _indexes, ETag conditional flow, writeMode insert/update/upsert, $-operator PATCH variety) - Aggregations via _meta.aggrs with avars variable interpolation (scalars / arrays / nested / missing / malformed) - Bulk writes (POST array body, filter PATCH, filter DELETE, larger 25-doc batches, mixed valid/invalid) - GraphQL apps (gql-apps registration, query / mutation / fragment / alias / multi-op, BSON scalar coercion on outputs and inputs, introspection, error paths) - Files / GridFS (.files buckets, multipart upload, binary download with Range requests, metadata fetch, delete) - ACL rules (predicate evaluator across method / path-prefix / qparams-* / bson-request-* / equals[%U,...] / in[%h,...]) plus the mongo permission interceptors (readFilter, writeFilter, projectResponse, mergeRequest, filterOperatorsBlacklist, propertiesBlacklist, allowBulk*) - Users (/users) with the userPwdHasher bcrypt interceptor; reader / writer roles authenticating via Basic + Bearer; wrong-password deny - Sessions / multi-doc transactions (/_sessions/<id>/_txns/<txnid>) with commit and abort branches - Auth services (/token form grants, JWT, Auth-Token, Digest, OAuth metadata under /.well-known/oauth-*) - Diagnostics (/ping, /metrics in json/prometheus/openmetrics, per-db and per-coll, /health/db, OPTIONS preflight, gzip request encoding, Accept-Encoding negotiation) - MongoMountResolver (multiple databases, encoded collection names, root /_size and /_meta, trailing-slash and double-slash variants) restheart_bootstrap now PUTs every collection record-traffic touches. README.md describes the sample as a complete keploy compat lane sample and lists every surface it exercises. Signed-off-by: Akash Kumar <meakash7902@gmail.com> * ci(restheart-mongo): add per-sample coverage gate workflow Adds .github/workflows/restheart-mongo.yml plus the helper .github/workflows/scripts/run-and-measure.sh, modeled on the doccano-django sample's coverage gate. * paths-scoped trigger: pull_request and push-to-main both filter on `restheart-mongo/**` and `.github/workflows/restheart-mongo.yml`, so changes to other samples in this repo do not trigger this workflow (and vice versa). * Three jobs: build-coverage (PR HEAD), release-coverage (PR base with first-PR bootstrap escape hatch), and coverage-gate that fails the PR if coverage drops more than COVERAGE_THRESHOLD percentage points (default 1.0pp, override via repo variable RESTHEART_COVERAGE_THRESHOLD). * Helper script brings the sample up via its own docker-compose.yml, waits for the RESTHeart listener (treating both 200 and 401 as ready since `/` requires auth), runs flow.sh bootstrap → record-traffic → coverage, and emits the parsed percentage onto $GITHUB_OUTPUT for the gate job. * Isolated from the enterprise lane: the enterprise PR pipeline (.woodpecker/restheart-linux.yml) calls `flow.sh coverage` only informationally and does not gate on it. The gate lives only here, on the sample repo, so coverage regressions surface on PRs that touch this sample without coupling enterprise CI to the route table. Signed-off-by: Akash Kumar <meakash7902@gmail.com> * ci(restheart-mongo): dump container logs on app wait timeout build-coverage on PR #134 hung 8 min when restheart never bound on port 8080 (last_code=000). The helper script silently looped through both the wait and flow.sh bootstrap timers, then the gate job aborted without surfacing why restheart didn't start. Adding an explicit fail-fast + docker logs dump after the 240s wait so a future failure surfaces the restheart Java traceback (or the mongo connection error, or whatever else). Signed-off-by: Akash Kumar <meakash7902@gmail.com> * feat(restheart-mongo): real Java line coverage via JaCoCo overlay Replaces the prior API-route-surface "coverage" (counting fired routes / curated route table) with actual JaCoCo line coverage of the RESTHeart 9.x JVM under traffic. Architecture: - `Dockerfile.coverage` is a multi-stage build: stage 1 (alpine) fetches JaCoCo 0.8.13 (jacocoagent.jar + jacococli.jar), stage 2 layers them into the upstream restheart image (which is distroless — no shell, no curl, so jars must be pulled in a builder stage and COPY'd over). - `docker-compose.coverage.yml` is an OVERLAY: applied via `-f docker-compose.yml -f docker-compose.coverage.yml`. It sets JAVA_TOOL_OPTIONS=-javaagent:.../jacocoagent.jar=output=tcpserver,... so JaCoCo attaches at JVM start and listens on port 6300. The base `Dockerfile` and `docker-compose.yml` are untouched, so keploy/integrations and keploy/enterprise CI lanes consume the base compose and pay zero JaCoCo cost (the agent rewrites bytecode at class-load, adding ~5-10% per-call overhead that would slow record/replay). - `flow.sh::restheart_report_coverage` shells into a one-off coverage container to dump execution data via JaCoCo TCP and render an XML report against /opt/restheart/restheart.jar. 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. Also fixes a pre-existing config bug in the base docker-compose.yml's RHO env var: the override syntax uses ';' as a key->value separator (the upstream image's default RHO uses ';'); the prior YAML-folded version used ',' which RESTHeart parsed as part of the connection-string value, leading RESTHeart to ignore the override and bind /http-listener/host to its localhost default — making the HTTP listener unreachable from the host port mapping. The base compose now uses ';' AND explicitly overrides /http-listener/host -> "0.0.0.0". Removed: - `restheart_list_routes` (curated route table denominator). - `restheart_list_recorded_routes` (keploy-tests / fired-routes reader). - The legacy route-surface `restheart_report_coverage` body. - `list-routes` subcommand. Validated locally: helper produced `coverage=52.3` to GITHUB_OUTPUT against a clean stack (1663/3182 lines covered in restheart.jar; INSTRUCTION coverage 50.8%). Signed-off-by: Akash Kumar <meakash7902@gmail.com> * ci(restheart-mongo): drop trailing prose from sticky comment Signed-off-by: Akash Kumar <meakash7902@gmail.com> * docs(restheart-mongo): document coverage overlay; drop list-routes/FIRED_ROUTES refs Signed-off-by: Akash Kumar <meakash7902@gmail.com> * fix(restheart-mongo): pin JWT key + correct globalNoise format for deterministic replay Two record/replay determinism fixes the full keploy/enterprise compat matrix needs to go green: 1) RHO `/jwtConfigProvider/key` pinned to a fixed string. Default is `key: null` which makes RESTHeart auto-generate a random HS256 secret per container start. Recorded JWT bearers carry an HS256 signature over the payload using that secret, so a fresh-container replay phase rejects the recorded bearer with 401 even though --freezeTime keeps `exp` valid. Pinning the secret keeps the bearer signature verifiable across record→replay container restarts. 2) keploy.yml.template rewritten to use NESTED globalNoise format (`body: { field: [] }`) instead of flat dotted keys (`body.field: []`). Keploy's matcher reads `globalNoise.global` as map[section][field]regex and treats dotted keys as literal outer keys, never matching the body section. Verified by walking pkg/matcher/http/match.go and the employee-manager sample's commented example. Fields added: header.{Date, Content-Length, Auth-Token}, body.{_etag, _oid, _id, lastModified, client_ip, latencyMs, access_token}. Validated locally with all three matrix cells (record-stable-replay-pr, record-pr-replay-pr, record-pr-replay-stable) — each reaches 296/296 PASSED with these two changes plus the lane-side --port and --freezeTime flags already in keploy/enterprise#1889. * fix(restheart-mongo): bound JVM heap to prevent OOM on shared CI runners RESTHeart 9.x's default JVM uses MaxRAMPercentage=25, which on a typical Woodpecker runner cgroup (~8GB) lands ~2GB heap. With the keploy/enterprise compat matrix running 3 restheart cells concurrent alongside parse-server / umami / doccano cells (12 lane-cells total on one runner), aggregate memory pressure triggers cgroup OOM and the kernel SIGKILLs lighter processes (bash, curl) mid-bootstrap. Observed in keploy/enterprise pipeline 3721/26 (record-stable-replay-pr): RESTHeart came up cleanly (`127.0.0.1:18070 is now reachable`), then `bash flow.sh bootstrap 240` got SIGKILL'd (exit 137) before any seed PUTs landed. Same shape across all 3 restheart cells in 3721. Cap heap at -Xmx512m (start at -Xms128m). Local single-cell record/replay peaks at ~280MB heap, so 512m is comfortable headroom without bleeding into neighbours. With the bound, each cell's footprint stays under ~700MB (heap + native + mongo); 3 restheart cells fit alongside other lanes on a shared runner. Validated locally: cell record-pr-replay-pr 296/0 PASSED with the new JAVA_TOOL_OPTIONS in place. RESTHeart logs confirm "Picked up JAVA_TOOL_OPTIONS: -Xms128m -Xmx512m ..." at startup. * fix(restheart-mongo): prefix RESTHEART_ADMIN_AUTH with `Authorization:` at curl call sites flow.sh's 214 admin-authenticated curl invocations passed the credential as `-H "$RESTHEART_ADMIN_AUTH"` where RESTHEART_ADMIN_AUTH defaults to `Basic YWRtaW46c2VjcmV0` (the credential value alone, no header name). curl renders that as a header value with no name and the request arrives at RESTHeart with no Authorization header, producing 401. Effect on the lane: - bootstrap's `PUT /<db>/<coll>` seed-collection writes all 401'd silently (each is `|| true`'d), so the seed collections were NEVER created. record-traffic ran against an empty mongo. - record-traffic's 214 admin-authenticated calls all 401'd. The recorded testcases are 256 × 401 (out of 296), 25 × 404, and only 9 × 200 (the public /ping and OPTIONS preflights). - Lane still passed-by-equality because both record and replay fail identically (the 401 wire bytes match), but the lane was not actually exercising RESTHeart's admin surface. Fix: change the call site, not the env. `-H "$RESTHEART_ADMIN_AUTH"` → `-H "Authorization: $RESTHEART_ADMIN_AUTH"`. The env var stays a plain credential ("Basic YWRtaW46c2VjcmV0"), the curl line constructs the full HTTP header. 214 sites swapped via: sed -i 's|-H "\$RESTHEART_ADMIN_AUTH"|-H "Authorization: $RESTHEART_ADMIN_AUTH"|g' Bare-smoke validation (docker compose up + flow.sh bootstrap + flow.sh record-traffic against vanilla restheart 9.2.1): before: 256 × 401 25 × 404 9 × 200 after: 110 × 200 59 × 201 49 × 404 33 × 405 25 × 204 16 × 401 11 × 400 8 × 403 4 × 409 3 × 500 The 16 remaining 401s are the genuinely intentional ones — bogus JWT bearer probe, malformed Digest, no-auth liveness, and similar. The 8 × 403 are real ACL-deny coverage that was previously unreachable behind the universal 401. * fix(restheart-mongo): noise Etag/Location + disable ACL cache for time-freeze compat After fixing the Authorization-header-prefix bug in flow.sh (parent commit), 214 admin-auth calls that previously 401'd now actually mutate state and reach RESTHeart's normal write/read path. That exposed two new categories of record/replay divergence the all-401 recording had hidden: 1) Etag header: RESTHeart auto-stamps a hash on every doc / collection response. body._etag is already in globalNoise, but the same hash also appears as a response header (Etag) — needs to be noised separately. Without it, ~85 of 306 tests fail on header.Etag diff. 2) Location header: POST /_sessions returns Location: /_sessions/<uuid> where the UUID is server-generated. 2 tests fail without noising. Both added to keploy.yml.template's globalNoise.global.header. Separate fix on the compose side: disable mongoAclAuthorizer's cache (`/mongoAclAuthorizer/cache-enabled->false` via RHO). Default is cache-enabled with TTL=5_000ms, backed by Caffeine. Caffeine's internal ticker uses System.nanoTime, which keploy's --freezeTime LD_PRELOAD shim intercepts — so at replay the wall-clock-bound TTL never expires. flow.sh's `sleep 6` between `POST /acl` and the writer-permission test works at record (clock advances, cache reloads the new rule) but is ineffective at replay (cache stuck on whatever it loaded first). Result: writer gets 403 at replay despite recorded 200. Disabling the cache makes ACL rules read-through from mongo on every request — small perf cost in the test environment but eliminates the time-freeze × cache-TTL interaction. Local cell record-pr-replay-pr verified 306/306 PASSED with both fixes in place. Test count went 296 → 306 (10 additional tests captured because admin operations now mutate state and unlock paths that previously 404'd). --------- Signed-off-by: Akash Kumar <meakash7902@gmail.com>
1 parent f3fc534 commit aa16aa2

10 files changed

Lines changed: 1905 additions & 0 deletions

File tree

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# restheart-mongo sample CI — keploy-independent end-to-end smoke +
2+
# coverage gate.
3+
#
4+
# Triggers ONLY on changes under restheart-mongo/ (or this workflow
5+
# file). Other samples in this repo have their own orthogonal CI;
6+
# gating the whole repo on every restheart 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 admin db + collections, 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 `RESTHEART_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 route-based
41+
# (single percentage), so the gate is a 3-line subtraction.
42+
#
43+
# Sample is genuinely keploy-independent here: the workflow uses
44+
# flow.sh's $RESTHEART_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::restheart_list_recorded_routes`.
51+
name: restheart-mongo sample
52+
53+
on:
54+
pull_request:
55+
paths:
56+
- 'restheart-mongo/**'
57+
- '.github/workflows/restheart-mongo.yml'
58+
push:
59+
branches: [main]
60+
paths:
61+
- 'restheart-mongo/**'
62+
- '.github/workflows/restheart-mongo.yml'
63+
workflow_dispatch: {}
64+
65+
concurrency:
66+
group: restheart-mongo-${{ github.ref }}
67+
cancel-in-progress: true
68+
69+
env:
70+
COVERAGE_THRESHOLD: ${{ vars.RESTHEART_COVERAGE_THRESHOLD || '1.0' }}
71+
72+
jobs:
73+
build-coverage:
74+
name: build (current ref) coverage
75+
runs-on: ubuntu-latest
76+
timeout-minutes: 20
77+
outputs:
78+
coverage: ${{ steps.measure.outputs.coverage }}
79+
steps:
80+
- uses: actions/checkout@v4
81+
- id: measure
82+
name: Run sample end-to-end + measure coverage
83+
working-directory: restheart-mongo
84+
env:
85+
RESTHEART_FIRED_ROUTES_FILE: ${{ runner.temp }}/fired-routes-build.log
86+
RESTHEART_PHASE: ci-build
87+
run: ../.github/workflows/scripts/run-and-measure.sh
88+
89+
- name: Upload coverage report
90+
if: always()
91+
uses: actions/upload-artifact@v4
92+
with:
93+
name: coverage-build
94+
path: restheart-mongo/coverage_report.txt
95+
if-no-files-found: warn
96+
97+
release-coverage:
98+
if: github.event_name == 'pull_request'
99+
name: release (base ref) coverage
100+
runs-on: ubuntu-latest
101+
timeout-minutes: 20
102+
outputs:
103+
coverage: ${{ steps.measure.outputs.coverage || steps.empty-baseline.outputs.coverage }}
104+
sample-existed: ${{ steps.detect.outputs.sample-existed }}
105+
steps:
106+
- uses: actions/checkout@v4
107+
with:
108+
ref: ${{ github.event.pull_request.base.ref }}
109+
110+
# First-PR bootstrap escape hatch: the very PR that
111+
# introduces the restheart-mongo/ sample has no baseline
112+
# (restheart-mongo/ doesn't exist on the base ref). Detect
113+
# that and short-circuit to coverage=0; the gate then
114+
# treats build's coverage as the new baseline and trivially
115+
# passes for any percentage > 0. After the introducing PR
116+
# merges, every subsequent PR has a real baseline to diff
117+
# against.
118+
- id: detect
119+
name: Detect baseline presence
120+
run: |
121+
if [ -d restheart-mongo ] && [ -x restheart-mongo/flow.sh ]; then
122+
echo "sample-existed=true" >>"$GITHUB_OUTPUT"
123+
echo "Sample exists on base ref — running full measurement."
124+
else
125+
echo "sample-existed=false" >>"$GITHUB_OUTPUT"
126+
echo "No restheart-mongo/ on base ref — first-PR bootstrap; baseline coverage treated as 0%."
127+
fi
128+
129+
- id: measure
130+
name: Run sample end-to-end + measure coverage
131+
if: steps.detect.outputs.sample-existed == 'true'
132+
working-directory: restheart-mongo
133+
env:
134+
RESTHEART_FIRED_ROUTES_FILE: ${{ runner.temp }}/fired-routes-release.log
135+
RESTHEART_PHASE: ci-release
136+
run: ../.github/workflows/scripts/run-and-measure.sh
137+
138+
- id: empty-baseline
139+
name: Emit zero baseline (first-PR bootstrap)
140+
if: steps.detect.outputs.sample-existed != 'true'
141+
run: echo "coverage=0.0" >>"$GITHUB_OUTPUT"
142+
143+
- name: Upload coverage report
144+
if: always() && steps.detect.outputs.sample-existed == 'true'
145+
uses: actions/upload-artifact@v4
146+
with:
147+
name: coverage-release
148+
path: restheart-mongo/coverage_report.txt
149+
if-no-files-found: warn
150+
151+
coverage-gate:
152+
if: github.event_name == 'pull_request'
153+
name: coverage gate
154+
needs: [build-coverage, release-coverage]
155+
runs-on: ubuntu-latest
156+
steps:
157+
- name: Compare build vs release
158+
env:
159+
BUILD: ${{ needs.build-coverage.outputs.coverage }}
160+
RELEASE: ${{ needs.release-coverage.outputs.coverage }}
161+
THRESHOLD: ${{ env.COVERAGE_THRESHOLD }}
162+
BASE_REF: ${{ github.event.pull_request.base.ref }}
163+
run: |
164+
set -Eeuo pipefail
165+
if [ -z "${BUILD:-}" ] || [ -z "${RELEASE:-}" ]; then
166+
echo "::error::missing coverage outputs — build='${BUILD:-}' release='${RELEASE:-}'"
167+
exit 1
168+
fi
169+
drop=$(python3 -c "print(round(${RELEASE} - ${BUILD}, 2))")
170+
echo "Release (${BASE_REF}): ${RELEASE}%"
171+
echo "Build (this PR): ${BUILD}%"
172+
echo "Drop: ${drop}pp (threshold ${THRESHOLD}pp)"
173+
if python3 -c "import sys; sys.exit(0 if (${RELEASE} - ${BUILD}) > ${THRESHOLD} else 1)"; then
174+
echo "::error::restheart-mongo coverage dropped from ${RELEASE}% → ${BUILD}% (-${drop}pp), exceeding the ${THRESHOLD}pp threshold."
175+
echo "Suggested actions:"
176+
echo " * Add curl(s) to flow.sh::restheart_record_traffic that exercise the routes you changed/touched."
177+
echo " * If the route(s) was intentionally retired, drop it from restheart-mongo/flow.sh::restheart_list_routes' SCOPE_PATHS too so it's removed from the denominator."
178+
exit 1
179+
fi
180+
echo "OK — coverage delta within ${THRESHOLD}pp threshold."
181+
182+
- name: Sticky PR comment
183+
if: ${{ !cancelled() }}
184+
uses: marocchino/sticky-pull-request-comment@v2
185+
with:
186+
header: restheart-mongo-coverage
187+
message: |
188+
### restheart-mongo sample coverage
189+
190+
| ref | coverage |
191+
|---|---|
192+
| base (`${{ github.event.pull_request.base.ref }}`) | **${{ needs.release-coverage.outputs.coverage }}%** |
193+
| this PR | **${{ needs.build-coverage.outputs.coverage }}%** |
194+
195+
Threshold: PR may not drop coverage by more than **${{ env.COVERAGE_THRESHOLD }}pp**. Override per-repo via the `RESTHEART_COVERAGE_THRESHOLD` actions variable.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env bash
2+
#
3+
# run-and-measure.sh — bring restheart-mongo up under the
4+
# coverage overlay (JaCoCo agent attached via JAVA_TOOL_OPTIONS),
5+
# run flow.sh bootstrap + record-traffic, dump JaCoCo execution
6+
# data over the agent's TCP server, render a Java line-coverage
7+
# report, and emit `coverage=PCT` onto $GITHUB_OUTPUT for the
8+
# downstream coverage-gate job.
9+
#
10+
# Coverage isolation contract:
11+
# * Base `Dockerfile` and `docker-compose.yml` are untouched.
12+
# * The overlay `Dockerfile.coverage` + `docker-compose.coverage.yml`
13+
# attach JaCoCo and expose its TCP server. ONLY this script
14+
# applies the overlay; keploy/integrations and keploy/enterprise
15+
# CI lanes consume the base compose and pay zero JVM-instrument
16+
# cost (jacocoagent adds ~5-10% per-call overhead).
17+
#
18+
# Inputs (from the workflow env):
19+
# RESTHEART_PHASE — label for log diffing.
20+
# GITHUB_OUTPUT — standard GH Actions sink for step outputs.
21+
set -Eeuo pipefail
22+
23+
export RESTHEART_APP_CONTAINER="${RESTHEART_APP_CONTAINER:-restheart_app}"
24+
export RESTHEART_MONGO_CONTAINER="${RESTHEART_MONGO_CONTAINER:-restheart_mongo}"
25+
export RESTHEART_APP_PORT="${RESTHEART_APP_PORT:-8080}"
26+
export RESTHEART_MONGO_IP="${RESTHEART_MONGO_IP:-172.36.0.10}"
27+
export RESTHEART_NETWORK_SUBNET="${RESTHEART_NETWORK_SUBNET:-172.36.0.0/24}"
28+
export RESTHEART_ADMIN_AUTH="${RESTHEART_ADMIN_AUTH:-Basic YWRtaW46c2VjcmV0}"
29+
30+
mkdir -p coverage
31+
chmod 777 coverage
32+
sudo rm -rf coverage/jacoco.exec coverage/report.xml coverage/coverage_report.txt 2>/dev/null \
33+
|| rm -rf coverage/jacoco.exec coverage/report.xml coverage/coverage_report.txt 2>/dev/null \
34+
|| true
35+
36+
COMPOSE=(docker compose -f docker-compose.yml -f docker-compose.coverage.yml)
37+
38+
"${COMPOSE[@]}" up -d --build
39+
40+
# Both 200 and 401 are success signals.
41+
for i in $(seq 1 120); do
42+
code=$(curl -sS -o /dev/null -w '%{http_code}' \
43+
"http://127.0.0.1:${RESTHEART_APP_PORT}/" 2>/dev/null || echo "")
44+
if [ "$code" = "200" ] || [ "$code" = "401" ]; then break; fi
45+
sleep 2
46+
done
47+
48+
if [ "$code" != "200" ] && [ "$code" != "401" ]; then
49+
echo "::error::restheart did not bind on port ${RESTHEART_APP_PORT} within 240s (last code: ${code:-empty})"
50+
echo "----- restheart container logs -----"
51+
docker logs "${RESTHEART_APP_CONTAINER}" --tail 200 2>&1 || true
52+
echo "----- mongo container logs -----"
53+
docker logs "${RESTHEART_MONGO_CONTAINER}" --tail 100 2>&1 || true
54+
"${COMPOSE[@]}" down -v --remove-orphans || true
55+
exit 1
56+
fi
57+
58+
bash flow.sh bootstrap 240
59+
bash flow.sh record-traffic
60+
61+
# JaCoCo TCP-dump + report (no JVM stop needed).
62+
COVERAGE_REPORT_FILE="$PWD/coverage_report.txt" bash flow.sh coverage
63+
64+
if [ ! -f coverage_report.txt ]; then
65+
echo "::error::flow.sh coverage produced no coverage_report.txt"
66+
exit 1
67+
fi
68+
69+
pct=$(grep -oE '\([0-9]+\.[0-9]+%\)' coverage_report.txt | head -1 | tr -d '()%')
70+
if [ -z "$pct" ]; then
71+
echo "::error::Could not parse coverage percentage from coverage_report.txt"
72+
cat coverage_report.txt || true
73+
exit 1
74+
fi
75+
echo "coverage=${pct}" >>"$GITHUB_OUTPUT"
76+
echo "coverage: ${pct}% (Java line coverage via JaCoCo)"
77+
78+
"${COMPOSE[@]}" down -v --remove-orphans

restheart-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

restheart-mongo/Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Thin wrapper around RESTHeart's official image at the version
2+
# this sample tracks. Pin lives here so a future RESTHeart release
3+
# is a one-line retag, not a hunt across keploy CI lanes.
4+
#
5+
# Upstream: https://github.com/SoftInstigate/restheart
6+
# Image: docker.io/softinstigate/restheart:9.2.1
7+
FROM softinstigate/restheart:9.2.1
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Coverage overlay image for restheart-mongo.
2+
#
3+
# Adds the JaCoCo agent (jacocoagent.jar) and CLI (jacococli.jar)
4+
# alongside the upstream restheart 9.2.1 image. The agent is
5+
# attached at JVM start via JAVA_TOOL_OPTIONS (set in
6+
# docker-compose.coverage.yml) so we don't have to rewrite the
7+
# upstream entrypoint, which is `java -jar restheart.jar` with
8+
# specific JVM flags.
9+
#
10+
# The agent runs in `tcpserver` mode so the workflow can dump
11+
# coverage data on demand without restarting the JVM —
12+
# important for distroless-style upstream images that don't
13+
# ship a shell.
14+
#
15+
# IMPORTANT: this image is only consumed by docker-compose.coverage.yml.
16+
# The base Dockerfile and docker-compose.yml stay uninstrumented so
17+
# enterprise's keploy compat lane pays no JVM-instrumentation cost
18+
# (jacocoagent adds ~5-10% per-call overhead through bytecode
19+
# rewriting, which would slow record/replay measurably).
20+
21+
# Stage 1: pull JaCoCo zip in an alpine builder. The upstream
22+
# restheart image is distroless (no shell, no curl/unzip), so we
23+
# can't fetch JaCoCo from inside it.
24+
FROM alpine:3.19 AS jacoco-fetch
25+
ARG JACOCO_VERSION=0.8.13
26+
RUN apk add --no-cache curl ca-certificates unzip \
27+
&& curl -fsSL "https://repo1.maven.org/maven2/org/jacoco/jacoco/${JACOCO_VERSION}/jacoco-${JACOCO_VERSION}.zip" -o /tmp/jacoco.zip \
28+
&& mkdir -p /tmp/jacoco \
29+
&& unzip -j /tmp/jacoco.zip lib/jacocoagent.jar lib/jacococli.jar -d /tmp/jacoco
30+
31+
# Stage 2: layer JaCoCo into the upstream image. We can't `RUN`
32+
# anything because the base image has no shell — only COPY and
33+
# WORKDIR work. COPY --chown sets ownership at copy time so the
34+
# distroless user (uid 65532) can read the agent.
35+
FROM softinstigate/restheart:9.2.1
36+
COPY --from=jacoco-fetch --chown=65532:65532 /tmp/jacoco/jacocoagent.jar /opt/jacoco/jacocoagent.jar
37+
COPY --from=jacoco-fetch --chown=65532:65532 /tmp/jacoco/jacococli.jar /opt/jacoco/jacococli.jar
38+
39+
# Pre-create /coverage as an empty WORKDIR so docker has a
40+
# mountpoint for the bind-mount in docker-compose.coverage.yml.
41+
# WORKDIR doesn't require a shell.
42+
WORKDIR /coverage
43+
WORKDIR /opt/restheart

0 commit comments

Comments
 (0)