PR gating
PR gating is Guard's pre-merge quality gate. On every pull request, Guard selects the scenarios affected by the diff, runs them against isolated Clones, and reports results as a GitHub Check Run — the primary surface developers see in the PR header.
Unlike running your full Playwright suite on every PR, Guard's affected-scenario detection computes which user journeys touch changed files, routes, and APIs — typically in a few seconds on large repos.
Product page: guard.molar.it · Dashboard: app.molar.it/dashboard/guard
How PR gating works
pull_request webhook (opened | synchronize | reopened)
│
▼
Verify HMAC signature, dedupe delivery ID, enqueue guard-pr job
│
▼
Create Check Run: "Detecting affected scenarios…"
│
▼
Worker: compute affected set → spawn Clones → run Playwright
│
▼
Stream logs (SSE) → update Check Run incrementally
│
▼
All green → conclusion success
Any failure → conclusion failure + Mender preview in sticky comment
Two integration paths:
| Path | Best for |
|---|---|
| GitHub App | Full lifecycle: webhooks, affected selection, sticky comments, Mender previews |
| GitHub Action | Hosted PR check for a registered scenario UUID (see Quick start) |
Most production teams use the GitHub App for webhooks and the Action optionally for preview URL injection.
GitHub App permissions
Install the Molar Guard GitHub App from guard.molar.it or Onboarding in app.molar.it/dashboard/guard.
Required permissions
| Permission | Access | Why |
|---|---|---|
checks | write | Create and update Check Runs (primary PR surface) |
pull_requests | write | Sticky PR comments, request reviewers on Mender PRs |
contents | read (+ write for Mender) | Affected detection; Mender fix branches |
metadata | read | Required for any GitHub App |
statuses | write | Fallback commit status when Check Run unavailable |
actions | read | Resolve last successful workflow on default branch for diff base |
members | read | Tag reviewers on Mender fix PRs |
Webhook subscriptions
Guard listens for:
pull_request— opened, synchronize, reopened, closed, ready_for_reviewpush— refresh scenario graph on default branchcheck_run.rerequested— re-run from GitHub UIinstallation,installation_repositories— org onboardingpull_request_review— Mender accept/reject signals
Webhooks are validated with HMAC-SHA256. Deliveries are deduplicated in Redis (24-hour replay window).
Install the GitHub App
- Go to guard.molar.it or Onboarding in the dashboard
- Click Connect GitHub and choose your organization
- Select repositories (all repos or specific subset)
- Complete the install — Guard receives an
installationwebhook - Open a test PR to verify a Check Run appears
Make the check required
By default, new installations start in advisory mode (see below). When you are ready to block merges:
- In the dashboard, go to Onboarding or Settings → Repos
- Click Make
molar/guardrequired — Guard guides branch protection setup - In GitHub: Settings → Branches → Branch protection → add
molar/guardas a required status check
Alternatively, set mode: required in your GitHub Action workflow.
Required vs advisory mode
| Mode | Check Run on failure | Merge blocked? | Recommended |
|---|---|---|---|
| Advisory | neutral or informational failure | No | First 30 days; building trust |
| Required | failure | Yes | After advisory trial shows accurate gating |
Advisory mode posts results and a banner: "Advisory mode — Molar would have blocked this." Engineers see what would have failed without blocking the team during onboarding.
Required mode enforces the Check Run as a branch protection status check. Failed scenarios block merge until fixed.
Configure per repo:
// molar-guard.config.ts
export default {
required: true, // or false for advisory
};
Or via GitHub Action input:
- uses: molar/guard-action@v1
with:
api-key: $\{\{ secrets.MOLAR_API_KEY \}\}
mode: required # or advisory
When you're ready, the dashboard can help you flip from advisory to required after you've reviewed a few PR runs.
PR run lifecycle
Phase 1 — Queued
GitHub webhook arrives. Guard creates a Check Run with status: queued and title "Detecting affected scenarios…"
Phase 2 — In progress
Worker dequeues the guard-pr job (priority 10 in BullMQ):
- Compute affected scenario set (see Affected selection)
- Update Check Run →
in_progress, "Running N scenarios" - Spawn fresh Clone bundle per PR (
seed = sha256(prNumber || headSha)[:8]) - Run Playwright with parallel workers (default 4; higher limits on paid tiers)
- Stream stdout and stderr to Redis Stream → SSE in the dashboard
Phase 3 — Complete
| Outcome | Check conclusion | Sticky comment |
|---|---|---|
| All scenarios pass | success | ✓ summary table |
| Any failure | failure | Failure cards + screenshots + Mender preview |
| All non-cached green, some cached | neutral or success | Shows ⊘ cached green rows |
Check Run output example
## ✗ 2 of 12 scenarios failed
| Scenario | Status | Duration | Replay |
|----------|--------|----------|--------|
| Signup happy path | ✓ passed | 4.2s | [view](…) |
| Stripe subscription upgrade | ✗ failed | 8.1s | [view](…) |
| Password reset email | ⊘ cached green | — | [view](…) |
### Failure: Stripe subscription upgrade
**Assertion:** expected `.subscription-status` text to equal `"Pro"`, got `"Free"`
**Step:** 7/12 (click "Upgrade to Pro")
**Screenshot:** 
🤖 **Mender suggests this fix:** [Preview patch](…)
Check Run actions include Re-run failed, Re-run all, and View in Molar when the GitHub App integration is active.
Affected scenario selection
Guard selects only the scenarios affected by each PR's diff — using a scenario dependency graph and git change detection — so large repos don't run the full suite on every push.
Algorithm overview
Stage 1 — Static affected (always on):
- ScenarioGraph in Postgres — edges from scenarios to files, routes, APIs, env vars, and copy anchors. Refreshed on pushes to your default branch.
- Git diff between
baseandheadSHA. Base prefers the last green SHA on your default branch (guard_last_green_sha), falling back to the PR's base commit when no green run exists yet. - Parse diff with
parse-git-difffor file + hunk ranges. - For each changed file:
- Direct file dependency lookup
- Route handler pattern match (TypeScript/Express/Next/Nest, plus pattern-based Python/Go/Ruby extraction)
- Transitive references via import graph (depth 3, cap 200 files)
- If
.molar/scenarios/*.molar.mdchanged directly → always run that scenario.
Stage 2 — Predictive reranker (when history exists):
A heuristic reranker (LightGBM-weighted scoring) reorders priority so likely failures run first. Never drops a statically-affected scenario without a green run in the last 7 days.
Stage 3 — Fallback safety net:
| Condition | Action |
|---|---|
| >50 scenarios affected | Run full suite; emit guard.affected_fallback metric |
| 0 scenarios on non-trivial diff (>10 files or >500 LOC) | Run full suite |
Engineers see fallback events on the dashboard and tune the scenario graph.
Speed
Affected-set computation is designed to complete in a few seconds on large repos. Your first runs may be slower while the scenario graph warms up.
Full-suite insurance
Nightly cron and post-merge runs on your default branch execute the full suite. Affected-skip on PRs cannot let a regression land silently on main.
Debug affected selection
pnpm molar-guard affected --base <base-sha> --head <head-sha>
API: GET /v1/scenarios/:id/affected_by/:sha
Run caching
Guard skips scenarios already proven green on an ancestor commit.
Cache key (content + dependency tree + clone bundle + runner version):
sha256(scenario_content || dependency_tree_hash || clone_bundle_version || guard_runner_version)
If the cache key matches a previous green run on a SHA that is an ancestor of the current PR head, the scenario is marked cached_green and skipped. Check Run shows ⊘ cached green.
Disable caching per scenario
---
id: stripe-subscription-upgrade
cache: never # always re-run — critical path
---
Clones on every PR run
Every PR run uses fresh Clone instances — no shared state between PRs or scenarios.
- Deterministic seed:
sha256(prNumber || scenarioId || cloneSurface)[:8]— same PR + scenario = same Stripe customer IDs - Destructive call detection: if a scenario hits
api.stripe.cominstead of the Clone, the SDK raisesMolarSafetyErrorand the run fails safely
Clones are optional for plug-and-play mode (vanilla Playwright against preview URL). Enable in molar-guard.config.ts:
export default {
clones: { enabled: true },
};
PR comment vs Check Run
| Surface | Role |
|---|---|
| Check Run | Primary — lives in PR header, blocks merge when required |
| Sticky comment | Secondary — high-density table, screenshots, Mender buttons |
The sticky comment is posted on first run and edited in place on subsequent runs using marker <!-- molar-guard:run-id=… --> — never spammed with new comments.
Comment includes:
- Status table with per-scenario duration
- Failure screenshot thumbnails in a collapsible
<details>block - Link to the Molar dashboard run detail
GitHub Action integration
For a hosted check without the full GitHub App, pass your scenario UUID and optional preview URL:
- uses: molar/guard-action@v1
with:
api-key: $\{\{ secrets.MOLAR_API_KEY \}\}
scenario-id: "00000000-0000-0000-0000-000000000001"
base-url: $\{\{ steps.deploy.outputs.url \}\}
mode: advisory
See the full workflow in Quick start.
The Action runs one registered scenario per workflow invocation. Use the GitHub App for affected selection across your catalog.
Parallelization and plan tiers
| Tier | Workers per PR | Concurrent PRs |
|---|---|---|
| Starter | 4 | 1 |
| Team | 8 | 4 |
| Business | 32 | Unlimited |
Shards are balanced with longest-processing-time (LPT) ordering from scenario_stats timing data.
Self-healing locator drift
When Playwright throws a locator timeout or visibility failure, Guard can enter a heal sub-loop:
- Capture DOM + screenshot
- Propose a new role-based locator (vision LLM when configured)
- Retry the step; if it passes → mark
self_healed - Open a PR against
.molar/scenarios/{scenario}.molar.mdwith the locator update
Guard always opens a PR for scenario file changes — never silent healing. See Mender for application-code fixes.
PR gating dashboard
At app.molar.it/dashboard/guard:
| Page | What you see |
|---|---|
Runs (run_type=pr) | PR runs with status, duration, and cache hits |
| PR gating | Recent PR gate activity and check status |
| Run detail | Live SSE log tail, failure artifacts, Mender panel |
Filter runs by run_type=pr to see PR-only history.
Security notes
- Webhook HMAC validation + Redis deduplication
- Per-installation BullMQ queue partitions (no noisy-neighbor starvation)
- Octokit throttling with retry; secondary GitHub rate limits are never auto-retried
- GitHub App private key in your secrets manager — not in repo env vars
See Security for full hardening guide.
Next
- Quick start — local setup and Action workflow
- Production monitoring — after merge, watch prod
- Mender auto-fix — fix PRs from PR failures
- Configuration —
molar-guard.config.tsand frontmatter - Troubleshooting — stuck checks, fallback storms, rate limits