Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions .github/workflows/restheart-mongo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# restheart-mongo sample CI — keploy-independent end-to-end smoke +
# coverage gate.
#
# Triggers ONLY on changes under restheart-mongo/ (or this workflow
# file). Other samples in this repo have their own orthogonal CI;
# gating the whole repo on every restheart change would slow them
# all down for no benefit.
#
# What it gates:
# * `release-coverage` — checks out the PR's base branch (main)
# and runs the sample end-to-end: docker compose up, bootstrap
# the admin db + collections, drive flow.sh record-traffic with
# the per-call audit log enabled, capture the route-coverage
# percentage from `flow.sh coverage`. This is the baseline.
# * `build-coverage` — same end-to-end against the PR's HEAD ref.
# * `coverage-gate` — fails the PR if `build`'s coverage drops
# more than COVERAGE_THRESHOLD percentage points below
# `release`. Default threshold is 1.0pp; override via repo
# variable `RESTHEART_COVERAGE_THRESHOLD` for a tighter or
# looser bar.
#
# On push to main, only `build-coverage` runs (no baseline to
# compare against — main IS the baseline).
#
# Standards-aligned choices:
# * `paths:` filter on both push and pull_request triggers — the
# canonical GH Actions way to scope a workflow to one
# subdirectory.
# * Job outputs (steps.<id>.outputs.coverage → needs.<job>.outputs)
# to thread the captured percentage between jobs.
# * `concurrency:` cancel-in-progress on the same ref so a stale
# run doesn't waste runner minutes.
# * actions/upload-artifact for the human-readable
# coverage_report.txt — reviewers can inspect missing routes
# directly from the PR's "checks" tab.
# * marocchino/sticky-pull-request-comment for the PR-side diff
# comment. Pinned-by-header so successive runs update the same
# comment instead of fanning out.
# * The compare step is plain bash + python3 (no external
# coverage service). The sample's coverage is route-based
# (single percentage), so the gate is a 3-line subtraction.
#
# Sample is genuinely keploy-independent here: the workflow uses
# flow.sh's $RESTHEART_FIRED_ROUTES_FILE per-call audit log as its
# numerator source, not a keploy recording. The lane scripts in
# keploy/integrations and keploy/enterprise consume the same
# flow.sh, but use the keploy/test-set-*/tests/*.yaml tree as
# their numerator (authoritative — only calls keploy actually
# CAPTURED count). Both modes are wired into
# `flow.sh::restheart_list_recorded_routes`.
name: restheart-mongo sample

on:
pull_request:
paths:
- 'restheart-mongo/**'
- '.github/workflows/restheart-mongo.yml'
push:
branches: [main]
paths:
- 'restheart-mongo/**'
- '.github/workflows/restheart-mongo.yml'
workflow_dispatch: {}

concurrency:
group: restheart-mongo-${{ github.ref }}
cancel-in-progress: true

env:
COVERAGE_THRESHOLD: ${{ vars.RESTHEART_COVERAGE_THRESHOLD || '1.0' }}

jobs:
build-coverage:
name: build (current ref) coverage
runs-on: ubuntu-latest
timeout-minutes: 20
outputs:
coverage: ${{ steps.measure.outputs.coverage }}
steps:
- uses: actions/checkout@v4
- id: measure
name: Run sample end-to-end + measure coverage
working-directory: restheart-mongo
env:
RESTHEART_FIRED_ROUTES_FILE: ${{ runner.temp }}/fired-routes-build.log
RESTHEART_PHASE: ci-build
run: ../.github/workflows/scripts/run-and-measure.sh

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-build
path: restheart-mongo/coverage_report.txt
if-no-files-found: warn

release-coverage:
if: github.event_name == 'pull_request'
name: release (base ref) coverage
runs-on: ubuntu-latest
timeout-minutes: 20
outputs:
coverage: ${{ steps.measure.outputs.coverage || steps.empty-baseline.outputs.coverage }}
sample-existed: ${{ steps.detect.outputs.sample-existed }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref }}

# First-PR bootstrap escape hatch: the very PR that
# introduces the restheart-mongo/ sample has no baseline
# (restheart-mongo/ doesn't exist on the base ref). Detect
# that and short-circuit to coverage=0; the gate then
# treats build's coverage as the new baseline and trivially
# passes for any percentage > 0. After the introducing PR
# merges, every subsequent PR has a real baseline to diff
# against.
- id: detect
name: Detect baseline presence
run: |
if [ -d restheart-mongo ] && [ -x restheart-mongo/flow.sh ]; then
echo "sample-existed=true" >>"$GITHUB_OUTPUT"
echo "Sample exists on base ref — running full measurement."
else
echo "sample-existed=false" >>"$GITHUB_OUTPUT"
echo "No restheart-mongo/ on base ref — first-PR bootstrap; baseline coverage treated as 0%."
fi

- id: measure
name: Run sample end-to-end + measure coverage
if: steps.detect.outputs.sample-existed == 'true'
working-directory: restheart-mongo
env:
RESTHEART_FIRED_ROUTES_FILE: ${{ runner.temp }}/fired-routes-release.log
RESTHEART_PHASE: ci-release
run: ../.github/workflows/scripts/run-and-measure.sh

- id: empty-baseline
name: Emit zero baseline (first-PR bootstrap)
if: steps.detect.outputs.sample-existed != 'true'
run: echo "coverage=0.0" >>"$GITHUB_OUTPUT"

- name: Upload coverage report
if: always() && steps.detect.outputs.sample-existed == 'true'
uses: actions/upload-artifact@v4
with:
name: coverage-release
path: restheart-mongo/coverage_report.txt
if-no-files-found: warn

coverage-gate:
if: github.event_name == 'pull_request'
name: coverage gate
needs: [build-coverage, release-coverage]
runs-on: ubuntu-latest
steps:
- name: Compare build vs release
env:
BUILD: ${{ needs.build-coverage.outputs.coverage }}
RELEASE: ${{ needs.release-coverage.outputs.coverage }}
THRESHOLD: ${{ env.COVERAGE_THRESHOLD }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
set -Eeuo pipefail
if [ -z "${BUILD:-}" ] || [ -z "${RELEASE:-}" ]; then
echo "::error::missing coverage outputs — build='${BUILD:-}' release='${RELEASE:-}'"
exit 1
fi
drop=$(python3 -c "print(round(${RELEASE} - ${BUILD}, 2))")
echo "Release (${BASE_REF}): ${RELEASE}%"
echo "Build (this PR): ${BUILD}%"
echo "Drop: ${drop}pp (threshold ${THRESHOLD}pp)"
if python3 -c "import sys; sys.exit(0 if (${RELEASE} - ${BUILD}) > ${THRESHOLD} else 1)"; then
echo "::error::restheart-mongo coverage dropped from ${RELEASE}% → ${BUILD}% (-${drop}pp), exceeding the ${THRESHOLD}pp threshold."
echo "Suggested actions:"
echo " * Add curl(s) to flow.sh::restheart_record_traffic that exercise the routes you changed/touched."
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."
exit 1
fi
echo "OK — coverage delta within ${THRESHOLD}pp threshold."

- name: Sticky PR comment
if: ${{ !cancelled() }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: restheart-mongo-coverage
message: |
### restheart-mongo sample coverage

| ref | coverage |
|---|---|
| base (`${{ github.event.pull_request.base.ref }}`) | **${{ needs.release-coverage.outputs.coverage }}%** |
| this PR | **${{ needs.build-coverage.outputs.coverage }}%** |

Threshold: PR may not drop coverage by more than **${{ env.COVERAGE_THRESHOLD }}pp**. Override per-repo via the `RESTHEART_COVERAGE_THRESHOLD` actions variable.

Coverage measures the RESTHeart 9.x REST surface (`/{db}/{coll}` CRUD + `_aggrs/{name}` + `_size` + `_meta` + `_indexes` + `_streams/{name}` + `/graphql` + `/graphql/{appname}` + `/{db}/{coll}.files` + `/acl` + `/users` + `/tokens` + sessions/transactions + `/ic` + `/csv` + metrics + OAuth) that `flow.sh::restheart_record_traffic` exercises against the running backend. Reports are attached as artifacts on each job ("coverage-build" / "coverage-release").
112 changes: 112 additions & 0 deletions .github/workflows/scripts/run-and-measure.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
#
# run-and-measure.sh — bring restheart-mongo up via the sample's
# compose, run flow.sh bootstrap + record-traffic with the
# per-call audit log enabled, run flow.sh coverage, and emit
# `coverage=PCT` onto $GITHUB_OUTPUT for the downstream
# coverage-gate job.
#
# Called from .github/workflows/restheart-mongo.yml's
# build-coverage and release-coverage jobs (one per ref under
# comparison). Both jobs source the same script so the
# measurement is identical across refs — any drift in the
# numerator definition would otherwise produce a misleading
# delta.
#
# Inputs (all from the workflow env):
# RESTHEART_FIRED_ROUTES_FILE — per-call audit log path; passed
# through to flow.sh so its
# record-traffic loop logs each
# (METHOD, URL) pair, and so its
# coverage subcommand uses that
# file as the standalone
# numerator.
# RESTHEART_PHASE — label spliced into the project
# name so build vs. release runs
# don't collide on volume names
# (compose project naming inside
# the GH runner is per-job
# anyway, but RESTHEART_PHASE
# shows up in the test fixtures
# and is useful for diffing logs).
# GITHUB_OUTPUT — standard GH Actions sink for
# step outputs.
set -Eeuo pipefail

# Compose-substituted variables. Defaults match the sample's
# docker-compose.yml so a local invocation of this script (no
# overrides) reproduces what CI runs.
export RESTHEART_APP_CONTAINER="${RESTHEART_APP_CONTAINER:-restheart_app}"
export RESTHEART_MONGO_CONTAINER="${RESTHEART_MONGO_CONTAINER:-restheart_mongo}"
export RESTHEART_APP_PORT="${RESTHEART_APP_PORT:-8080}"
export RESTHEART_MONGO_IP="${RESTHEART_MONGO_IP:-172.36.0.10}"
export RESTHEART_NETWORK_SUBNET="${RESTHEART_NETWORK_SUBNET:-172.36.0.0/24}"

# RESTHeart 9.x ships with admin/secret as the default
# bootstrapped principal. flow.sh reads this header for every
# call, so exporting it here keeps the standalone CI run aligned
# with the keploy lanes (which pass the same value through).
export RESTHEART_ADMIN_AUTH="${RESTHEART_ADMIN_AUTH:-Basic YWRtaW46c2VjcmV0}"

: "${RESTHEART_FIRED_ROUTES_FILE:?RESTHEART_FIRED_ROUTES_FILE must be set by the workflow}"

# Reset audit log for this run; otherwise a prior run's entries
# would inflate the numerator on a re-trigger.
: >"$RESTHEART_FIRED_ROUTES_FILE"

# Single-phase bootstrap: RESTHeart embeds its own admin
# principal at first boot, so there's no separate "seed admin
# user" stage the way doccano needs. compose up → wait for app
# port → flow.sh bootstrap (PUTs the db + record-traffic's
# collections) → flow.sh record-traffic → flow.sh coverage.
docker compose up -d

# Wait for the backend to start serving. Per the sample's
# restheart_wait_for_app, both 200 AND 401 are success signals
# — RESTHeart returns 401 on `/` until you authenticate, but
# 401 still proves the HTTP listener and the auth filter are
# both up. Anything before that (000 / connection refused) is
# pre-listen.
for i in $(seq 1 120); do
code=$(curl -sS -o /dev/null -w '%{http_code}' \
"http://127.0.0.1:${RESTHEART_APP_PORT}/" 2>/dev/null || echo "")
if [ "$code" = "200" ] || [ "$code" = "401" ]; then break; fi
sleep 2
done

if [ "$code" != "200" ] && [ "$code" != "401" ]; then
echo "::error::restheart did not bind on port ${RESTHEART_APP_PORT} within 240s (last code: ${code:-empty})"
echo "----- restheart container logs -----"
docker logs "${RESTHEART_APP_CONTAINER}" --tail 200 2>&1 || true
echo "----- mongo container logs -----"
docker logs "${RESTHEART_MONGO_CONTAINER}" --tail 100 2>&1 || true
echo "----- docker compose ps -----"
docker compose ps || true
docker compose down -v --remove-orphans || true
exit 1
fi

bash flow.sh bootstrap 240

# Drive traffic. flow.sh::restheart_record_traffic gates on
# restheart_wait_for_app internally, so this won't fire curls
# at a half-booted backend.
bash flow.sh record-traffic

# Coverage report — uses RESTHEART_FIRED_ROUTES_FILE as numerator
# since no keploy/test-set-* tree exists in the standalone case.
COVERAGE_REPORT_FILE="$PWD/coverage_report.txt" bash flow.sh coverage

# Pull the percentage out of the report's `Covered N/M (XX.X%)`
# line. Anchored on the parenthesised form so a future change to
# the report's prose doesn't break the parse.
pct=$(grep -oE '\([0-9]+\.[0-9]+%\)' coverage_report.txt | head -1 | tr -d '()%')
if [ -z "$pct" ]; then
echo "::error::Could not parse coverage percentage from coverage_report.txt"
cat coverage_report.txt || true
exit 1
fi
echo "coverage=${pct}" >>"$GITHUB_OUTPUT"
echo "coverage: ${pct}% (audit log: $RESTHEART_FIRED_ROUTES_FILE)"

docker compose down -v --remove-orphans
7 changes: 7 additions & 0 deletions restheart-mongo/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Thin wrapper around RESTHeart's official image at the version
# this sample tracks. Pin lives here so a future RESTHeart release
# is a one-line retag, not a hunt across keploy CI lanes.
#
# Upstream: https://github.com/SoftInstigate/restheart
# Image: docker.io/softinstigate/restheart:9.2.1
FROM softinstigate/restheart:9.2.1
52 changes: 52 additions & 0 deletions restheart-mongo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# restheart-mongo — keploy compat lane sample

A complete, self-contained sample that drives the RESTHeart 9.x REST surface keploy needs to gate on its compat lanes. Mirrors the architectural pattern of the [doccano-django sample in `samples-python`](https://github.com/keploy/samples-python/tree/main/doccano-django): the sample owns orchestration (compose / bootstrap / traffic / noise filter / coverage), and keploy CI lanes consume it as a thin wrapper.

The traffic loop exercises the surfaces that keploy parsers and matchers have to handle correctly across record + replay:

* **CRUD** on `/<db>/<coll>` and `/<db>/<coll>/<docid>` — including `_size`, `_meta`, `_indexes`, ETag conditional requests, `writeMode=insert/update/upsert`, and `$inc / $push / $addToSet / $pull / $unset / $rename / $currentDate` PATCH operators.
* **HAL** representations via `Accept: application/hal+json` and `?rep=hal&hal=full` on documents, collections, indexes, and bulk responses.
* **Aggregations** via `_meta.aggrs` — group / count / sort / project / facet / lookup / unwind plus `avars` variable interpolation (scalars, arrays, nested objects, missing / malformed inputs).
* **Bulk writes** — array-body POST, filter-bound PATCH and DELETE, larger 25-doc batches, mixed valid / invalid documents.
* **GraphQL** apps — `gql-apps` registration, query / mutation / fragment / alias / multi-op forms, BSON scalar coercion (`BsonObjectId`, `BsonDecimal128`, `BsonLong`, `BsonDate`, `BsonBinary`) on outputs and inputs, introspection.
* **Files / GridFS** — buckets (`<coll>.files`), multipart upload, binary download with `Range` requests, metadata fetch, delete.
* **ACL** rules (`/acl`) — predicate evaluation (`method`, `path-prefix`, `qparams-whitelist`, `qparams-blacklist`, `qparams-contain`, `qparams-size`, `bson-request-whitelist/blacklist/contains`, `equals[%U,...]`, `in[%h, ...]`), `mongo` permission interceptors (`readFilter`, `writeFilter`, `projectResponse`, `mergeRequest`, `filterOperatorsBlacklist`, `propertiesBlacklist`, `allowBulk*`).
* **Users** (`/users`) — non-admin user creation with the bcrypt password hasher; reader / writer roles authenticating via Basic + Bearer; wrong-password denial.
* **Sessions / transactions** (`/_sessions`, `/_sessions/<id>/_txns/<txnid>`) — open, write inside, commit (PATCH), abort (DELETE), and re-read.
* **Auth services** — `/token` form grants (password, client_credentials, refresh_token, unsupported), JWT bearer (valid + invalid signature), Auth-Token, Digest, OAuth metadata under `/.well-known/oauth-*`.
* **Diagnostics** — `/ping`, `/metrics` (json / prometheus / openmetrics, per-db, per-coll), `/health/db`, OPTIONS preflight, gzip request encoding, Accept-Encoding negotiation.
* **MongoMountResolver** — multiple databases, collections with dashes / dots / encoded slashes, root `/_size` and `/_meta`, trailing-slash and double-slash variants.

## Layout

```
restheart-mongo/
├── Dockerfile # FROM softinstigate/restheart:9.2.1
├── docker-compose.yml # mongo:7 + restheart:9.2.1, fixed subnet, env-driven
├── flow.sh # bootstrap | record-traffic | coverage | list-routes
├── keploy.yml.template # globalNoise for _etag/_oid/lastModified/Date
└── README.md # this file
```

## Contract

The sample is keploy-independent: `docker compose up && bash flow.sh bootstrap && bash flow.sh record-traffic` runs end-to-end against bare RESTHeart. Lane scripts wrap that exact same path inside `keploy record` / `keploy test`.

* `bootstrap` — wait for RESTHeart to start serving and PUT the seed collections (`items`, `people`, `places`, `halpeople`, `relpeople`, `gql-apps`, `acl`, `_schemas`, `avatars.files`, `range_files.files`, `imported_csv`) so subsequent record-traffic calls have something to find.
* `record-traffic` — drive the full RESTHeart REST surface listed above. Every call is logged to `${RESTHEART_FIRED_ROUTES_FILE}` (when set) so `coverage` has a numerator without a keploy recording, and every call is fault-tolerant (`|| true`) so a single transient 4xx never aborts the run. keploy is the assertion layer.
* `coverage` — emits `(method, path)` coverage. The denominator is curated from RESTHeart's pattern-based mount table (see `restheart_list_routes` in `flow.sh`); RESTHeart routes are not file-system-derivable like Next.js, so the list lives in source and stays in lockstep with `record-traffic`.
* `list-routes` — diagnostic; prints the route table the coverage report uses as its denominator.

## Local run

```sh
docker compose up -d
bash flow.sh bootstrap 240
RESTHEART_FIRED_ROUTES_FILE=/tmp/fired.log bash flow.sh record-traffic
RESTHEART_FIRED_ROUTES_FILE=/tmp/fired.log bash flow.sh coverage
docker compose down -v
```

## Consumers

* `keploy/enterprise` `.woodpecker/restheart-linux.yml` — the RESTHeart compat lane delegates compose + traffic + coverage to this sample and wraps them in `keploy record` / `keploy test`.
Loading
Loading