Use before declaring any Supabase C# SDK change done or opening a PR, and as the Verify step of every flow. Runs the committed scripts/quality-gate/gate.sh script — the mechanized gauntlet (build/analyzers with a warning baseline, format on changed files, tests, a line-coverage baseline, vulnerability scan and E2E/acceptance tests, plus a public-API diff signal) — and reports its verdict. This is the deterministic "is it done" check; do not report a change as done without a PASS.
يبقى الأمر في سطر واحد. مرّر أفقيًا لمراجعته كاملًا قبل النسخ.
تفضّل نسخة محلية؟ نزّل الملفات المتاحة حاليًا لدى SkillsMP.
عرض SKILL.md
SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
sdk-quality-gate
description
Use before declaring any Supabase C# SDK change done or opening a PR, and as the Verify step of every flow. Runs the committed scripts/quality-gate/gate.sh script — the mechanized gauntlet (build/analyzers with a warning baseline, format on changed files, tests, a line-coverage baseline, vulnerability scan and E2E/acceptance tests, plus a public-API diff signal) — and reports its verdict. This is the deterministic "is it done" check; do not report a change as done without a PASS.
Skill: SDK quality gate
The gauntlet is a program, not a procedure. Run it and report what it says.
The script lives at scripts/quality-gate/gate.sh — committed alongside the code
so contributors and CI run the identical command, not just agents. Paths below
assume the working directory is the workspace root (sdk-csharp); the script
itself is location-independent, so an absolute path works equally well.
scripts/quality-gate/gate.sh <package> # full gate — before a PR
scripts/quality-gate/gate.sh <package> --fast # inner loop only
There are two modes. --fast is the inner loop only (build, format, tests) — the
fast local red/green cycle; the default is the full gate and adds security, the
public-API check, E2E and a line-coverage baseline. <package> is a directory —
e.g. gotrue-csharp,
core-csharp — and defaults to the current directory. Run with bash <path> if
the executable bit is unset.
Mutation testing is not part of the gate. It is too slow for the inner/PR
loop and runs in its own scheduled GitHub Action.
CI runs the same command. The Build and Test workflow
(.github/workflows/build-and-test.yml) invokes scripts/quality-gate/gate.sh
on every push and pull request, so a local PASS and a CI PASS mean the same
thing. The blocking-stage verdict here is the one that gates the merge.
Reading the verdict
Verdict
Exit
Meaning
PASS
0
Blocking stages green. Report done; surface the signals.
PARTIAL
0
--fast only. Not sufficient to declare done.
INCOMPLETE
2
A blocking stage could not run. Unverified ≠ verified.
FAIL
1
A blocking stage failed.
Blocking stages [B] decide the verdict: build/analyzers, format, tests,
line coverage, dependency vulnerabilities, public-API declared, and
E2E/acceptance. A failing E2E test blocks the merge exactly like a failing unit
test — there is no green build with a red test.
Test execution differs by mode.--fast runs the inner loop only
(TestCategory!=E2E) — no coverage measured, verdict is PARTIAL. The full gate,
when the local/CI Supabase stack is reachable, runs every test in one
unfiltered pass — unit, contract and E2E together, under stage id 2 — so a
red unit test no longer prevents E2E from executing in the same run (they either
both ran, or neither did). That same run is also the coverage source: 2b
measures line coverage across the whole suite, not just the inner loop, so
"coverage" always means the full picture. Stage 7 doesn't appear in the
report at all when this happens — id 2's own per-package rows (labeled
"Tests (Unit + Contract + E2E)") already carry the full outcome, so a separate
E2E row would only restate it, and imprecisely at that (a package with zero E2E
tests has nothing to "fold in"). If the stack is unreachable, the gate falls back
to running the inner loop alone for local feedback, and both E2E (7) and
coverage (2b) SKIP with a reason — "full" coverage isn't measurable without the
E2E half of the suite, and a check that couldn't run must never read as passed.
Either way, a stack that can't be reached holds the gate at INCOMPLETE.
Dropped intentionally: a package with zero [TestCategory("E2E")] tests used to
get its own non-blocking signal row under 7 ("no E2E tests in this package").
Now that E2E runs folded into the unfiltered stage 2, that distinction isn't
separately observable without a second test-discovery pass, so it's gone — not a
regression to chase, just a note so its absence isn't a surprise.
The one signal stage [s] is the public-API diff: it never fails the build,
because the maintainer is the merge gate on breaking changes — a break may be
intended. The tool informs that call, it does not veto it (QUALITY_RUBRIC §4).
Stages are skipped only when an earlier failure makes them impossible or
meaningless — a failed build blocks format, tests, E2E and coverage; a stack
that can't be reached blocks E2E and coverage specifically (tests still run via
the inner-loop fallback). Nothing else suppresses a stage, and anything not run
is still reported as SKIP with its reason.
Reporting
Paste the summary table verbatim. Never paraphrase a stage result, and never
report a stage as passing when the script recorded SKIP. Detail is in
<package>/.gate/report.json and the per-stage logs it references.
Add the triage a number alone doesn't carry:
Public-API diff — additive, or a break needing sign-off + a major plan +
[Obsolete] + a MIGRATION_vN.md entry.
On failure — fix the cause, not the check
Not acceptable repairs:
#pragma warning disable, or lowering a severity in .editorconfig
re-categorising a test so a filter skips it
deleting or [Ignore]-ing a red test
retrying or quarantining a flaky E2E — flakiness is a design defect
hand-editing .gate-baseline.json the wrong way (warnings up, coverage down)
to make a stage pass
If a blocking stage fails for a reason outside the change's scope, stop and
report it. Do not work around it and continue.
Baselines
<package>/.gate-baseline.json is committed and holds the discovered project
paths, the warning count per code, and line coverage. The warning baseline only
moves down (fewer is better); coverage only up (more is better,
coverage.hermeticLine, a single percentage). Either direction, moving the number the
wrong way means editing the file by hand, so the regression appears in code review.
Writing the baseline is opt-in. A plain gate.sh run is read-only — it reads
the baseline to produce a verdict but never touches the file, so a contributor's
run can't silently rewrite a committed baseline. Pass --overwrite-baseline to
create it (a package's first run) or to update it. On CI this is a post-merge
maintainer concern: PR runs stay read-only, and only the push-to-master run
passes --overwrite-baseline and commits any improvement back to master.
Coverage is compared with a ~1% tolerance band, not an exact match. Even
hermetic (unit+contract) coverage isn't byte-reproducible on a loaded CI runner —
async continuations (retry/backoff Task.Delay, cancellation races, background
timers) get scheduled differently run to run, flipping a few covered lines. The
band absorbs that jitter symmetrically: a drop within it passes, a rise within it
doesn't move the baseline (so it never locks onto a lucky peak the next run can't
reach), and a real regression — an untested method, far more than 1% — still fails.
Coverage has no ceiling. It must always be at or above the best ever
recorded — never capped at, say, 95%, because a cap reopens exactly the
regression window the baseline exists to close: once at a cap, a large chunk of
fully untested new code could land without the aggregate dipping below it,
especially against a large existing denominator. If coverage nears 100% and the
remaining gaps are genuinely low-value to test (a guard clause, an unreachable
default: arm), the answer is [ExcludeFromCodeCoverage] on those specific
lines — not a gate-level threshold. That's a per-line, reviewed-in-diff opt-out;
a reviewer sees it and can push back, unlike a global cap that quietly loosens
the bar for everyone forever.
Optional keys: formatBaseRef (default: origin/HEAD, then main/master)
for the changed-file comparison, and e2eHealthUrl if the local stack isn't on
the default port.
When a --overwrite-baseline run reports baseline lowered or baseline raised,
commit the file. A read-only run (the default) never emits those — it won't have
written anything to commit.
What the script cannot check
These stay human judgment. Walk them; don't recall them:
Behavior asserted through the public surface; testability friction fixed by
changing the design, not by exposing internals.
Approved wire-shape snapshots are right — the contract tests only prove
the current output still matches them, not that the approved bytes are correct.
CancellationToken genuinely honored through the call path, not just
accepted at the entry point.
Orchestration reads as named intent; expected failures returned, not thrown.
Public surface minimal and documented; entry points carry an <example>.
Parity deviations noted in the PR description.
Commits follow type(scope): description. The scope is required since the
move to a monorepo and names the affected package — functions, gotrue,
postgrest, realtime, storage, core, supabase (append ! before
the colon for breaking changes, e.g. feat(functions)!:).
Every item that becomes mechanizable — an analyzer, an architecture test, a
TestConventions rule — should move out of this list and into the script. The
list is meant to shrink.