Skip to content

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):

  1. Regenerates the baselines and fails if the committed ones are out of date (run scripts/update-public-api.sh).
  2. Diffs them against origin/main and derives the minimum impact: additions → minor, removals/renames → major, none → patch. Because cli-help.txt stores raw help text, a help-reword shows up as one removed + one added line and is conservatively treated as major.
  3. 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.MaxImpact in changesharp.json (default major = cap off).
  • Enforced at changesharp new (creation) and changesharp release (production), both requiring the explicit --allow-major flag to proceed.

See SemVer Rules for details, and samples/maximpact-gate/ for a runnable demo.