API Surface Gate¶
Concept¶
A single rule: a fragment must not be lower than the actual impact on the public API surface.
Fragment says "### Fixed" (Patch)
CI detects: new public method added → minimum impact is Minor
→ ❌ "The API surface changed (Minor). Your Fixed fragment is too low."
This is not an absolute validation — it's a safety net that says "you touched the API, your fragment must reflect it."
Approach: CI does the diff, ChangeSharp enforces the policy¶
┌──────────────────────────────────────────┐
│ CI (GitHub Actions, GitLab CI, etc.) │
│ │
│ 1. Pick the right api diff tool │
│ 2. Compare API before/after │
│ 3. Deduce the minimum impact level │
│ (patch | minor | major) │
│ 4. Pass the info to ChangeSharp │
│ │
│ changesharp validate --api-min-level minor
│ └─ Exit 0 if fragments comply │
│ └─ Exit 3 if a fragment is too low │
└──────────────────────────────────────────┘
ChangeSharp does not perform the diff. It only receives a minimum impact level and checks that the fragments are consistent.
CLI Interface¶
# On validate (PR)
changesharp validate --api-min-level minor
# → Ensures no fragment is below Minor
# On release
changesharp release --api-min-level major
# → Checks before releasing
Mapping table¶
--api-min-level |
Allowed fragments | Rejected fragments |
|---|---|---|
patch |
All | None (gate disabled) |
minor |
Added, Changed, Deprecated, Breaking Changes, Removed |
Fixed, Security |
major |
Breaking Changes, Removed |
Fixed, Security, Added, Changed, Deprecated |
CI Example (GitHub Actions)¶
- name: Run API diff
id: apidiff
run: |
# Team chooses the tool for their stack
dotnet tool run faithlife.apidifftool --base HEAD~1 --current . --format json > apidiff.json
LEVEL=$(jq -r '.impact' apidiff.json) # "patch", "minor", or "major"
echo "impact=$LEVEL" >> $GITHUB_OUTPUT
- name: Validate fragments against API impact
run: changesharp validate --api-min-level ${{ steps.apidiff.outputs.impact }}
The team chooses its diff tool. ChangeSharp only consumes the result.
Why this approach?¶
- Full decoupling — ChangeSharp doesn't need to know about every diff tool in the world
- Multi-language — CI can use the right tool for each ecosystem
- Simple to implement — one CLI flag, one lookup table
- Unix philosophy — each tool does one thing well
- No snapshot management — CI handles before/after as it sees fit
Implementation¶
In validate and release:
if (parseResult.GetValue(apiMinLevelOption) is string minLevel)
{
var fragments = LoadFragments();
int maxFragmentImpact = fragments.Max(f => GetSemVerImpact(f.Category));
int requiredImpact = ParseLevel(minLevel); // patch=0, minor=1, major=2
if (maxFragmentImpact < requiredImpact)
{
Console.Error.WriteLine($"API surface requires a {minLevel} bump, but fragments only declare {maxFragmentImpact}.");
return ExitCodeValidationError;
}
}
~20 lines of code. No provider, no JSON parsing, no external integration.
ChangeSharp dogfoods it¶
ChangeSharp applies the API Surface Gate to its own public surfaces. Committed baselines in tests/public-api/ snapshot:
| Baseline | Covers | Generated by |
|---|---|---|
cli-help.txt |
CLI commands and options (root + subcommands) | scripts/update-public-api.sh |
mcp-tools.json |
MCP tools/list (names + input schema) |
scripts/update-public-api.sh |
public-api.txt |
Library public API | PublicApiGenerator (test PublicApiBaselineTests) |
CI job api-surface (.github/workflows/dotnet.yml):
- Regenerates the baselines and fails if the committed ones are out of date (
run scripts/update-public-api.sh). - Diffs them against
origin/mainand derives the minimum impact: additions →minor, removals/renames →major, none →patch. Becausecli-help.txtstores raw help text, a help-reword shows up as one removed + one added line and is conservatively treated asmajor. - Runs
changesharp validate --api-min-level <impact>so the PR's fragments cannot be lower than the real surface change.
The library baseline is also enforced at build time by PublicApiBaselineTests (part of dotnet test).
scripts/update-public-api.sh requires dotnet and jq (the MCP snapshot is pretty-printed with jq; jq is pre-installed on the CI ubuntu-latest runner).
When you change a public surface, update the baselines and add a fragment that matches the impact:
scripts/update-public-api.sh
changesharp new --added "Add the new --foo option to status"
Symmetric guard: the max-impact cap¶
The API Surface Gate is a floor (fragments must not be lower than the real impact). ChangeSharp also provides the symmetric cap so a fragment can never silently force a Major bump the team does not want:
SemverPolicy.MaxImpactinchangesharp.json(defaultmajor= cap off).- Enforced at
changesharp new(creation) andchangesharp release(production), both requiring the explicit--allow-majorflag to proceed.
See SemVer Rules for details, and
samples/maximpact-gate/ for a runnable demo.