diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 173eedf..65d824b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -30,6 +30,9 @@ jobs:
self-test-advisory:
name: Self-test (advisory mode, breaking fixtures)
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
steps:
- uses: actions/checkout@v4
@@ -56,6 +59,9 @@ jobs:
self-test-enforce:
name: Self-test (enforce mode, safe fixtures)
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
steps:
- uses: actions/checkout@v4
diff --git a/README.md b/README.md
index c7a66af..19c2503 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,635 @@
-# `>` Delimit GitHub Action\n\n**Catch breaking API changes before merge** — semver classification, migration guides, and policy enforcement for OpenAPI specs.\n\n
+# `>` Delimit GitHub Action
+
+**Catch breaking API changes before merge** — semver classification, migration guides, and policy enforcement for OpenAPI specs.
+
+
---
## Think and Build
Delimit is more than just an action — it is a governance layer for your AI coding assistants. When you use the Delimit Swarm, you can say **"Think and Build"** to your agents, and they will automatically use this action to verify their own code changes against your API policies before requesting a merge.
-\n[](https://github.com/marketplace/actions/delimit-api-governance)\n[](https://opensource.org/licenses/MIT)\n[](https://github.com/marketplace/actions/delimit-api-governance)\n\nDelimit runs on every pull request, compares your OpenAPI spec against the base branch, and posts a detailed comment with breaking changes, semver classification, policy violations, and migration guidance. No API keys, no external services, no config required to get started.\n\n## What it looks like\n\n
\n
\n
\n\n---\n\n## Features\n\n- **Breaking change detection** — catches 27 types of changes (17 breaking, 10 non-breaking) across endpoints, parameters, response schemas, types, enums, security, and constraints\n- **Semver classification** — deterministic `major` / `minor` / `patch` / `none` bump recommendation with computed next version\n- **Migration guides** — auto-generated step-by-step migration instructions for every breaking change\n- **PR comments** — rich Markdown summary posted directly on your pull request, updated on each push\n- **Advisory and enforce modes** — start with non-blocking warnings, promote to CI-gating when ready\n- **Custom policies** — define your own governance rules in `.delimit/policies.yml` with path patterns, severity levels, and custom messages\n- **7 explainer templates** — developer, team lead, product, migration, changelog, PR comment, and Slack formats\n\n---\n\n## Quick Start\n\nAdd this file to `.github/workflows/api-check.yml`:\n\n```yaml\nname: API Contract Check\non: pull_request\n\njobs:\n delimit:\n runs-on: ubuntu-latest\n permissions:\n pull-requests: write\n steps:\n - uses: actions/checkout@v4\n - uses: delimit-ai/delimit-action@v1\n with:\n spec: api/openapi.yaml\n```\n\nThat is it. Delimit auto-fetches the base branch version of your spec and diffs it against the PR changes. Runs in **advisory mode** by default — posts a PR comment but never fails your build.\n\n### What the PR comment looks like\n\nWhen Delimit detects breaking changes, it posts a comment like this:\n\n> **Delimit API Governance** | Breaking Changes Detected\n>\n> | Change | Path | Severity |\n> |--------|------|----------|\n> | endpoint_removed | `DELETE /pets/{petId}` | error |\n> | type_changed | `/pets:GET:200[].id` (string → integer) | warning |\n> | enum_value_removed | `/pets:GET:200[].status` | warning |\n>\n> **Semver**: MAJOR (1.0.0 → 2.0.0)\n>\n> Migration Guide (3 steps)
\n>\n> **Step 1**: `DELETE /pets/{petId}` was removed. Update clients to use an alternative endpoint or remove calls to this path.\n>\n> **Step 2**: `id` changed from `string` to `integer`. Update serialization logic, type assertions, and database column types.\n>\n> **Step 3**: `status` enum value `"pending"` was removed. Update clients to stop sending this value.\n>\n> \n\nSee the [live demo](https://github.com/delimit-ai/delimit-action-demo/pull/2) — a Users API migration with 23 breaking changes detected across 27 change types, severity badges, and a migration guide.\n\n### Advanced: explicit base and head specs\n\nIf you need to compare specific files (e.g., pre-checked-out base branch), use `old_spec` and `new_spec` instead:\n\n```yaml\n - uses: delimit-ai/delimit-action@v1\n with:\n old_spec: base/api/openapi.yaml\n new_spec: api/openapi.yaml\n```\n\n---\n\n## Full Usage\n\n```yaml\nname: API Governance\non: pull_request\n\njobs:\n api-check:\n runs-on: ubuntu-latest\n permissions:\n pull-requests: write\n steps:\n - uses: actions/checkout@v4\n\n - name: Checkout base spec\n uses: actions/checkout@v4\n with:\n ref: ${{ github.event.pull_request.base.sha }}\n path: base\n\n - uses: delimit-ai/delimit-action@v1\n id: delimit\n with:\n old_spec: base/api/openapi.yaml\n new_spec: api/openapi.yaml\n mode: enforce\n policy_file: .delimit/policies.yml\n github_token: ${{ secrets.GITHUB_TOKEN }}\n\n - name: Use outputs\n if: always()\n run: |\n echo "Breaking changes: ${{ steps.delimit.outputs.breaking_changes_detected }}"\n echo "Violations: ${{ steps.delimit.outputs.violations_count }}"\n echo "Semver bump: ${{ steps.delimit.outputs.semver_bump }}"\n echo "Next version: ${{ steps.delimit.outputs.next_version }}"\n```\n\n---\n\n## Inputs\n\n| Input | Required | Default | Description |\n|-------|----------|---------|-------------|\n| `spec` | No | `''` | Path to the changed OpenAPI spec. On pull requests, Delimit auto-fetches the base branch version for comparison. |\n| `old_spec` | No | `''` | Path to the old/base API specification file. |\n| `new_spec` | No | `''` | Path to the new/changed API specification file. |\n| `mode` | No | `advisory` | `advisory` (comments only) or `enforce` (fails CI on breaking changes). |\n| `github_token` | No | `${{ github.token }}` | GitHub token used to post PR comments. |\n| `policy_file` | No | `''` | Path to a custom policy file (e.g., `.delimit/policies.yml`). |\n\n> **Note**: Provide either `spec` for pull request workflows, or both `old_spec` and `new_spec` for explicit comparisons. If neither form is provided, the action exits with an error.\n\n---\n\n## Outputs\n\n| Output | Type | Description |\n|--------|------|-------------|\n| `breaking_changes_detected` | `string` | `"true"` if any breaking change was found, `"false"` otherwise. |\n| `violations_count` | `string` | Number of policy violations (errors + warnings). |\n| `semver_bump` | `string` | Recommended version bump: `major`, `minor`, `patch`, or `none`. |\n| `next_version` | `string` | Computed next version string (e.g., `2.0.0`). |\n| `report` | `string` | Full JSON report of all detected changes, violations, and semver data. |\n\n### Using outputs in subsequent steps\n\n```yaml\n- uses: delimit-ai/delimit-action@v1\n id: delimit\n with:\n old_spec: base/api/openapi.yaml\n new_spec: api/openapi.yaml\n\n- name: Block release on breaking changes\n if: steps.delimit.outputs.breaking_changes_detected == 'true'\n run: |\n echo "Breaking changes detected — semver bump: ${{ steps.delimit.outputs.semver_bump }}"\n echo "Next version should be: ${{ steps.delimit.outputs.next_version }}"\n exit 1\n\n- name: Auto-tag on minor bump\n if: steps.delimit.outputs.semver_bump == 'minor'\n run: |\n git tag "v${{ steps.delimit.outputs.next_version }}"\n```\n\n---\n\n## Custom Policies\n\nCreate `.delimit/policies.yml` in your repository root to define governance rules beyond the defaults.\n\n```yaml\n# .delimit/policies.yml\n\n# Set to true to replace all default rules with only your custom rules.\n# Default: false (custom rules merge with defaults).\noverride_defaults: false\n\nrules:\n # Forbid removing endpoints without deprecation\n - id: no_endpoint_removal\n name: Forbid Endpoint Removal\n change_types:\n - endpoint_removed\n severity: error # error | warning | info\n action: forbid # forbid | allow | warn\n message: "Endpoint {path} cannot be removed. Use deprecation headers instead."\n\n # Protect V1 API — no breaking changes allowed\n - id: protect_v1_api\n name: Protect V1 API\n description: V1 endpoints are frozen\n change_types:\n - endpoint_removed\n - method_removed\n - field_removed\n severity: error\n action: forbid\n conditions:\n path_pattern: "^/v1/.*"\n message: "V1 API is frozen. Changes must be made in V2."\n\n # Warn on type changes in 2xx responses\n - id: warn_response_type_change\n name: Warn Response Type Changes\n change_types:\n - type_changed\n severity: warning\n action: warn\n conditions:\n path_pattern: ".*:2\\d\\d.*"\n message: "Type changed at {path} — verify client compatibility."\n\n # Allow adding enum values (informational)\n - id: allow_enum_expansion\n name: Allow Enum Expansion\n change_types:\n - enum_value_added\n severity: info\n action: allow\n message: "Enum value added (non-breaking)."\n```\n\n### Available change types for rules\n\n| Change type | Breaking | Description |\n|-------------|----------|-------------|\n| `endpoint_removed` | Yes | An API endpoint path was removed |\n| `method_removed` | Yes | An HTTP method was removed from an endpoint |\n| `required_param_added` | Yes | A new required parameter was added |\n| `param_removed` | Yes | A parameter was removed |\n| `response_removed` | Yes | A response status code was removed |\n| `required_field_added` | Yes | A new required field was added to a request body |\n| `field_removed` | Yes | A field was removed from a response |\n| `type_changed` | Yes | A field's type was changed (e.g., string to integer) |\n| `format_changed` | Yes | A field's format was changed (e.g., date to date-time) |\n| `enum_value_removed` | Yes | An allowed enum value was removed |\n| `endpoint_added` | No | A new endpoint was added |\n| `method_added` | No | A new HTTP method was added to an endpoint |\n| `optional_param_added` | No | A new optional parameter was added |\n| `response_added` | No | A new response status code was added |\n| `optional_field_added` | No | A new optional field was added |\n| `enum_value_added` | No | A new enum value was added |\n| `description_changed` | No | A description was modified |\n\n### Default rules\n\nDelimit ships with 6 built-in rules that are always active unless you set `override_defaults: true`:\n\n1. **Forbid Endpoint Removal** — endpoints cannot be removed (error)\n2. **Forbid Method Removal** — HTTP methods cannot be removed (error)\n3. **Forbid Required Parameter Addition** — new required params break clients (error)\n4. **Forbid Response Field Removal** — removing fields from 2xx responses (error)\n5. **Warn on Type Changes** — type changes flagged as warnings\n6. **Allow Enum Expansion** — adding enum values is always safe (info)\n\n---\n\n## Slack / Discord Notifications\n\nGet notified in Slack or Discord when breaking API changes are detected. Add a `webhook_url` input pointing to your channel's incoming webhook:\n\n```yaml\n- uses: delimit-ai/delimit-action@v1\n with:\n spec: api/openapi.yaml\n webhook_url: ${{ secrets.SLACK_WEBHOOK }}\n```\n\nThe notification fires only when breaking changes are found. If the webhook URL is not set, this step is silently skipped.\n\n### Supported platforms\n\n| Platform | URL pattern | Payload format |\n|----------|------------|----------------|\n| **Slack** | `hooks.slack.com` | Block Kit with mrkdwn |\n| **Discord** | `discord.com/api/webhooks` | Rich embed with color and fields |\n| **Generic** | Anything else | Plain JSON event payload |\n\nDelimit auto-detects the platform from the URL and formats the message accordingly. Webhook failures are logged as warnings but never fail your CI run.\n\n### Discord example\n\n```yaml\n- uses: delimit-ai/delimit-action@v1\n with:\n spec: api/openapi.yaml\n webhook_url: ${{ secrets.DISCORD_WEBHOOK }}\n```\n\n### Generic webhook\n\nAny URL that is not Slack or Discord receives a JSON payload:\n\n```json\n{\n "event": "breaking_changes_detected",\n "repo": "org/repo",\n "pr_number": 123,\n "pr_title": "Update user endpoints",\n "breaking_changes": 3,\n "additive_changes": 1,\n "semver": "MAJOR",\n "pr_url": "https://github.com/org/repo/pull/123"\n}\n```\n\n---\n\n## Advisory vs Enforce Mode\n\n| Behavior | `advisory` (default) | `enforce` |\n|----------|---------------------|-----------|\n| PR comment | Yes | Yes |\n| GitHub annotations | Yes | Yes |\n| Fails CI on breaking changes | **No** | **Yes** |\n| Exit code on violations | `0` | `1` |\n\n**Start with advisory mode.** It gives your team visibility into API changes without blocking merges. Once your team is comfortable, switch to `enforce` to gate deployments.\n\n```yaml\n# Advisory — non-blocking (default)\n- uses: delimit-ai/delimit-action@v1\n with:\n old_spec: base/api/openapi.yaml\n new_spec: api/openapi.yaml\n mode: advisory\n\n# Enforce — blocks merge on breaking changes\n- uses: delimit-ai/delimit-action@v1\n with:\n old_spec: base/api/openapi.yaml\n new_spec: api/openapi.yaml\n mode: enforce\n```\n\n---\n\n## Examples\n\n### Advisory mode (recommended starting point)\n\n```yaml\nname: API Check\non: pull_request\n\njobs:\n api-check:\n runs-on: ubuntu-latest\n permissions:\n pull-requests: write\n steps:\n - uses: actions/checkout@v4\n - uses: actions/checkout@v4\n with:\n ref: ${{ github.event.pull_request.base.sha }}\n path: base\n - uses: delimit-ai/delimit-action@v1\n with:\n old_spec: base/api/openapi.yaml\n new_spec: api/openapi.yaml\n```\n\n### Enforce mode\n\n```yaml\nname: API Governance\non: pull_request\n\njobs:\n api-check:\n runs-on: ubuntu-latest\n permissions:\n pull-requests: write\n steps:\n - uses: actions/checkout@v4\n - uses: actions/checkout@v4\n with:\n ref: ${{ github.event.pull_request.base.sha }}\n path: base\n - uses: delimit-ai/delimit-action@v1\n with:\n old_spec: base/api/openapi.yaml\n new_spec: api/openapi.yaml\n mode: enforce\n```\n\n### Custom policy file\n\n```yaml\n- uses: delimit-ai/delimit-action@v1\n with:\n old_spec: base/api/openapi.yaml\n new_spec: api/openapi.yaml\n mode: enforce\n policy_file: .delimit/policies.yml\n```\n\n### Using outputs to control downstream jobs\n\n```yaml\njobs:\n api-check:\n runs-on: ubuntu-latest\n outputs:\n breaking: ${{ steps.delimit.outputs.breaking_changes_detected }}\n bump: ${{ steps.delimit.outputs.semver_bump }}\n next_version: ${{ steps.delimit.outputs.next_version }}\n steps:\n - uses: actions/checkout@v4\n - uses: actions/checkout@v4\n with:\n ref: ${{ github.event.pull_request.base.sha }}\n path: base\n - uses: delimit-ai/delimit-action@v1\n id: delimit\n with:\n old_spec: base/api/openapi.yaml\n new_spec: api/openapi.yaml\n\n deploy:\n needs: api-check\n if: needs.api-check.outputs.breaking != 'true'\n runs-on: ubuntu-latest\n steps:\n - run: echo "Safe to deploy — next version ${{ needs.api-check.outputs.next_version }}"\n```\n\n---\n\n## Supported Formats\n\n- OpenAPI 3.0 and 3.1\n- Swagger 2.0\n- YAML and JSON spec files\n\n---\n\n## What the PR Comment Looks Like\n\nWhen Delimit detects changes, it posts (or updates) a comment on your pull request:\n\n```\n## Delimit: Breaking Changes `MAJOR`\n\n| Metric | Value |\n|--------|-------|\n| Semver bump | `major` |\n| Next version | `2.0.0` |\n| Total changes | 5 |\n| Breaking | 2 |\n| Violations | 2 |\n\n### Violations\n\n| Severity | Rule | Description | Location |\n|----------|------|-------------|----------|\n| Error | Forbid Endpoint Removal | Endpoint /users/{id} cannot be removed | `/users/{id}` |\n| Warning | Warn on Type Changes | Type changed from string to integer | `/users:200.age` |\n\n\nMigration guide
\n...step-by-step instructions for each breaking change...\n \n```\n\nThe comment is automatically updated on each push to the PR branch. No duplicate comments.\n\n---\n\n## FAQ / Troubleshooting\n\n### Delimit skipped validation and did nothing\n\nBoth `old_spec` and `new_spec` must be provided. If either is empty, Delimit exits cleanly with no output. Make sure both paths point to valid spec files.\n\n### How do I get the base branch spec?\n\nUse a second `actions/checkout` step to check out the base branch into a subdirectory:\n\n```yaml\n- uses: actions/checkout@v4\n with:\n ref: ${{ github.event.pull_request.base.sha }}\n path: base\n```\n\nThen reference `base/path/to/openapi.yaml` as `old_spec`.\n\n### My spec file is not found\n\nVerify the path relative to the repository root. Common locations:\n- `api/openapi.yaml`\n- `docs/openapi.yaml`\n- `openapi.yaml`\n- `swagger.json`\n\n### The action posts duplicate PR comments\n\nDelimit searches for an existing comment containing "Delimit" from a bot user and updates it in place. If you see duplicates, ensure `github_token` has `pull-requests: write` permission.\n\n### Can I use this with JSON specs?\n\nYes. Delimit supports both YAML (`.yaml`, `.yml`) and JSON (`.json`) spec files. Set the input paths accordingly.\n\n### Can I use this in a monorepo with multiple specs?\n\nYes. Add multiple Delimit steps, each with different `old_spec` / `new_spec` pairs:\n\n```yaml\n- uses: delimit-ai/delimit-action@v1\n with:\n old_spec: base/services/users/openapi.yaml\n new_spec: services/users/openapi.yaml\n\n- uses: delimit-ai/delimit-action@v1\n with:\n old_spec: base/services/billing/openapi.yaml\n new_spec: services/billing/openapi.yaml\n```\n\n### Advisory mode still shows errors in the PR comment — is that expected?\n\nYes. Advisory mode reports everything (including errors) in the PR comment and GitHub annotations, but it always exits with code `0` so your CI stays green. Switch to `enforce` mode when you want breaking changes to block the merge.\n\n### How is the semver bump calculated?\n\nThe classification is deterministic:\n- **major** — any breaking change detected (endpoint removed, required param added, field removed, type changed, etc.)\n- **minor** — additive changes only (new endpoints, optional fields, enum values added)\n- **patch** — non-functional changes only (description updates)\n- **none** — no changes detected\n\n---\n\n## CLI\n\nFor local development, pre-commit checks, and CI/CD pipelines outside GitHub Actions, use the [Delimit CLI](https://www.npmjs.com/package/delimit-cli):\n\n```bash\nnpm install -g delimit-cli\ndelimit lint api/openapi.yaml\ndelimit diff old-api.yaml new-api.yaml\ndelimit explain old-api.yaml new-api.yaml --template migration\n```\n\n---\n\n## Links\n\n- [Delimit CLI on npm](https://www.npmjs.com/package/delimit-cli) — Local development tool\n- [GitHub Repository](https://github.com/delimit-ai/delimit) — Source code and issues\n- [GitHub Action Marketplace](https://github.com/marketplace/actions/delimit-api-governance) — Install from Marketplace\n\n---\n\n## License\n\nMIT
\ No newline at end of file
+
+[](https://github.com/marketplace/actions/delimit-api-governance)
+[](https://opensource.org/licenses/MIT)
+[](https://github.com/marketplace/actions/delimit-api-governance)
+
+Delimit runs on every pull request, compares your OpenAPI spec against the base branch, and posts a detailed comment with breaking changes, semver classification, policy violations, and migration guidance. No API keys, no external services, no config required to get started.
+
+## What it looks like
+
+
+
+
+
+---
+
+## Features
+
+- **Breaking change detection** — catches 27 types of changes (17 breaking, 10 non-breaking) across endpoints, parameters, response schemas, types, enums, security, and constraints
+- **Semver classification** — deterministic `major` / `minor` / `patch` / `none` bump recommendation with computed next version
+- **Migration guides** — auto-generated step-by-step migration instructions for every breaking change
+- **PR comments** — rich Markdown summary posted directly on your pull request, updated on each push
+- **Advisory and enforce modes** — start with non-blocking warnings, promote to CI-gating when ready
+- **Custom policies** — define your own governance rules in `.delimit/policies.yml` with path patterns, severity levels, and custom messages
+- **7 explainer templates** — developer, team lead, product, migration, changelog, PR comment, and Slack formats
+
+---
+
+## Quick Start
+
+Add this file to `.github/workflows/api-check.yml`:
+
+```yaml
+name: API Contract Check
+on: pull_request
+
+jobs:
+ delimit:
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: delimit-ai/delimit-action@v1
+ with:
+ spec: api/openapi.yaml
+```
+
+That is it. Delimit auto-fetches the base branch version of your spec and diffs it against the PR changes. Runs in **advisory mode** by default — posts a PR comment but never fails your build.
+
+### What the PR comment looks like
+
+When Delimit detects breaking changes, it posts a comment like this:
+
+> **Delimit API Governance** | Breaking Changes Detected
+>
+> | Change | Path | Severity |
+> |--------|------|----------|
+> | endpoint_removed | `DELETE /pets/{petId}` | error |
+> | type_changed | `/pets:GET:200[].id` (string → integer) | warning |
+> | enum_value_removed | `/pets:GET:200[].status` | warning |
+>
+> **Semver**: MAJOR (1.0.0 → 2.0.0)
+>
+> Migration Guide (3 steps)
+>
+> **Step 1**: `DELETE /pets/{petId}` was removed. Update clients to use an alternative endpoint or remove calls to this path.
+>
+> **Step 2**: `id` changed from `string` to `integer`. Update serialization logic, type assertions, and database column types.
+>
+> **Step 3**: `status` enum value `"pending"` was removed. Update clients to stop sending this value.
+>
+>
+
+See the [live demo](https://github.com/delimit-ai/delimit-action-demo/pull/2) — a Users API migration with 23 breaking changes detected across 27 change types, severity badges, and a migration guide.
+
+### Advanced: explicit base and head specs
+
+If you need to compare specific files (e.g., pre-checked-out base branch), use `old_spec` and `new_spec` instead:
+
+```yaml
+ - uses: delimit-ai/delimit-action@v1
+ with:
+ old_spec: base/api/openapi.yaml
+ new_spec: api/openapi.yaml
+```
+
+---
+
+## Full Usage
+
+```yaml
+name: API Governance
+on: pull_request
+
+jobs:
+ api-check:
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Checkout base spec
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.base.sha }}
+ path: base
+
+ - uses: delimit-ai/delimit-action@v1
+ id: delimit
+ with:
+ old_spec: base/api/openapi.yaml
+ new_spec: api/openapi.yaml
+ mode: enforce
+ policy_file: .delimit/policies.yml
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Use outputs
+ if: always()
+ run: |
+ echo "Breaking changes: ${{ steps.delimit.outputs.breaking_changes_detected }}"
+ echo "Violations: ${{ steps.delimit.outputs.violations_count }}"
+ echo "Semver bump: ${{ steps.delimit.outputs.semver_bump }}"
+ echo "Next version: ${{ steps.delimit.outputs.next_version }}"
+```
+
+---
+
+## Inputs
+
+| Input | Required | Default | Description |
+|-------|----------|---------|-------------|
+| `spec` | No | `''` | Path to the changed OpenAPI or JSON Schema spec. On pull requests, Delimit auto-fetches the base branch version for comparison. |
+| `old_spec` | No | `''` | Path to the old/base API specification file. |
+| `new_spec` | No | `''` | Path to the new/changed API specification file. |
+| `mode` | No | `advisory` | `advisory` (comments only) or `enforce` (fails CI on breaking changes). |
+| `fail_on_breaking` | No | `false` | Boolean alias for `mode: enforce`. When `true`, fails CI on breaking changes regardless of `mode`. |
+| `github_token` | No | `${{ github.token }}` | GitHub token used to post PR comments. |
+| `policy_file` | No | `''` | Path to a custom policy file (e.g., `.delimit/policies.yml`). |
+| `webhook_url` | No | `''` | Slack or Discord webhook URL. Delimit posts a notification when breaking changes are detected. Auto-detects the platform from the URL. |
+| `generator_command` | No | `''` | Optional shell command that regenerates a generated artifact (e.g. `pnpm run schema:export`). When set, Delimit runs this command in a sandbox and diffs the regenerated output against the committed artifact to detect drift between source-of-truth and committed file. Pair with `generator_artifact`. See [Generator drift detection](#generator-drift-detection). |
+| `generator_artifact` | No | `''` | Path to the generated artifact that `generator_command` produces (e.g. `schemas/v1/agent.schema.json`). Required when `generator_command` is set. |
+
+> **Note**: Provide either `spec` for pull request workflows, or both `old_spec` and `new_spec` for explicit comparisons. If neither form is provided, the action exits with an error.
+
+---
+
+## Generator drift detection
+
+Many repos commit a JSON Schema (or similar artifact) that is generated from a source-of-truth file — for example a Zod schema in TypeScript compiled to JSON Schema via `zodToJsonSchema`, or a Protobuf file compiled to OpenAPI. A common class of bug is that someone updates the source and forgets to regenerate the committed artifact, so the two drift apart silently.
+
+Delimit can catch this on every PR by running the generator in a sandbox and diffing its output against the committed file.
+
+```yaml
+- uses: delimit-ai/delimit-action@v1
+ with:
+ spec: schemas/v1/agent.schema.json
+ generator_command: pnpm run schema:export
+ generator_artifact: schemas/v1/agent.schema.json
+```
+
+On every PR that touches the schema:
+
+1. Delimit runs `generator_command` in a sandboxed copy of the working tree.
+2. It reads the regenerated artifact and diffs it against the committed file.
+3. Any drift is reported in the PR comment, classified using the same JSON Schema semantics as normal schema changes (property add/remove, required, type widen/narrow, enum, `const`, `additionalProperties`, pattern, length and numeric bounds).
+4. The committed file is restored before the workflow exits — the working tree is never modified.
+
+If the regenerated output matches the committed artifact exactly, no drift is reported and the check passes silently. If the generator fails (non-zero exit or missing output file), Delimit reports the failure as an advisory warning and continues with the normal schema diff.
+
+This is separate from the base-branch schema diff. Both run on the same PR and are reported in the same comment:
+
+- **Schema classification** — committed JSON Schema vs base branch (what changed in this PR)
+- **Generator drift** — regenerated artifact vs committed file in this PR (is the committed file stale)
+
+You can use either independently, or both together. `generator_command` is opt-in — leave it empty to skip the drift check entirely.
+
+### Supported generators
+
+Anything that produces a JSON Schema or OpenAPI file at a known path and exits with code `0`. Common examples:
+
+```yaml
+# Zod → JSON Schema via zodToJsonSchema
+generator_command: pnpm run schema:export
+generator_artifact: schemas/v1/agent.schema.json
+
+# Protobuf → OpenAPI via buf or protoc-gen-openapi
+generator_command: buf generate
+generator_artifact: gen/openapi/api.yaml
+
+# TypeBox → JSON Schema
+generator_command: npm run build:schema
+generator_artifact: dist/schema.json
+```
+
+The action needs whatever toolchain the generator depends on to already be installed in the workflow — add `actions/setup-node`, `pnpm/action-setup`, or equivalent steps before the Delimit step.
+
+---
+
+## Outputs
+
+| Output | Type | Description |
+|--------|------|-------------|
+| `breaking_changes_detected` | `string` | `"true"` if any breaking change was found, `"false"` otherwise. |
+| `violations_count` | `string` | Number of policy violations (errors + warnings). |
+| `semver_bump` | `string` | Recommended version bump: `major`, `minor`, `patch`, or `none`. |
+| `next_version` | `string` | Computed next version string (e.g., `2.0.0`). |
+| `report` | `string` | Full JSON report of all detected changes, violations, and semver data. |
+
+### Using outputs in subsequent steps
+
+```yaml
+- uses: delimit-ai/delimit-action@v1
+ id: delimit
+ with:
+ old_spec: base/api/openapi.yaml
+ new_spec: api/openapi.yaml
+
+- name: Block release on breaking changes
+ if: steps.delimit.outputs.breaking_changes_detected == 'true'
+ run: |
+ echo "Breaking changes detected — semver bump: ${{ steps.delimit.outputs.semver_bump }}"
+ echo "Next version should be: ${{ steps.delimit.outputs.next_version }}"
+ exit 1
+
+- name: Auto-tag on minor bump
+ if: steps.delimit.outputs.semver_bump == 'minor'
+ run: |
+ git tag "v${{ steps.delimit.outputs.next_version }}"
+```
+
+---
+
+## Custom Policies
+
+Create `.delimit/policies.yml` in your repository root to define governance rules beyond the defaults.
+
+```yaml
+# .delimit/policies.yml
+
+# Set to true to replace all default rules with only your custom rules.
+# Default: false (custom rules merge with defaults).
+override_defaults: false
+
+rules:
+ # Forbid removing endpoints without deprecation
+ - id: no_endpoint_removal
+ name: Forbid Endpoint Removal
+ change_types:
+ - endpoint_removed
+ severity: error # error | warning | info
+ action: forbid # forbid | allow | warn
+ message: "Endpoint {path} cannot be removed. Use deprecation headers instead."
+
+ # Protect V1 API — no breaking changes allowed
+ - id: protect_v1_api
+ name: Protect V1 API
+ description: V1 endpoints are frozen
+ change_types:
+ - endpoint_removed
+ - method_removed
+ - field_removed
+ severity: error
+ action: forbid
+ conditions:
+ path_pattern: "^/v1/.*"
+ message: "V1 API is frozen. Changes must be made in V2."
+
+ # Warn on type changes in 2xx responses
+ - id: warn_response_type_change
+ name: Warn Response Type Changes
+ change_types:
+ - type_changed
+ severity: warning
+ action: warn
+ conditions:
+ path_pattern: ".*:2\\d\\d.*"
+ message: "Type changed at {path} — verify client compatibility."
+
+ # Allow adding enum values (informational)
+ - id: allow_enum_expansion
+ name: Allow Enum Expansion
+ change_types:
+ - enum_value_added
+ severity: info
+ action: allow
+ message: "Enum value added (non-breaking)."
+```
+
+### Available change types for rules
+
+| Change type | Breaking | Description |
+|-------------|----------|-------------|
+| `endpoint_removed` | Yes | An API endpoint path was removed |
+| `method_removed` | Yes | An HTTP method was removed from an endpoint |
+| `required_param_added` | Yes | A new required parameter was added |
+| `param_removed` | Yes | A parameter was removed |
+| `response_removed` | Yes | A response status code was removed |
+| `required_field_added` | Yes | A new required field was added to a request body |
+| `field_removed` | Yes | A field was removed from a response |
+| `type_changed` | Yes | A field's type was changed (e.g., string to integer) |
+| `format_changed` | Yes | A field's format was changed (e.g., date to date-time) |
+| `enum_value_removed` | Yes | An allowed enum value was removed |
+| `endpoint_added` | No | A new endpoint was added |
+| `method_added` | No | A new HTTP method was added to an endpoint |
+| `optional_param_added` | No | A new optional parameter was added |
+| `response_added` | No | A new response status code was added |
+| `optional_field_added` | No | A new optional field was added |
+| `enum_value_added` | No | A new enum value was added |
+| `description_changed` | No | A description was modified |
+
+### Default rules
+
+Delimit ships with 6 built-in rules that are always active unless you set `override_defaults: true`:
+
+1. **Forbid Endpoint Removal** — endpoints cannot be removed (error)
+2. **Forbid Method Removal** — HTTP methods cannot be removed (error)
+3. **Forbid Required Parameter Addition** — new required params break clients (error)
+4. **Forbid Response Field Removal** — removing fields from 2xx responses (error)
+5. **Warn on Type Changes** — type changes flagged as warnings
+6. **Allow Enum Expansion** — adding enum values is always safe (info)
+
+---
+
+## Slack / Discord Notifications
+
+Get notified in Slack or Discord when breaking API changes are detected. Add a `webhook_url` input pointing to your channel's incoming webhook:
+
+```yaml
+- uses: delimit-ai/delimit-action@v1
+ with:
+ spec: api/openapi.yaml
+ webhook_url: ${{ secrets.SLACK_WEBHOOK }}
+```
+
+The notification fires only when breaking changes are found. If the webhook URL is not set, this step is silently skipped.
+
+### Supported platforms
+
+| Platform | URL pattern | Payload format |
+|----------|------------|----------------|
+| **Slack** | `hooks.slack.com` | Block Kit with mrkdwn |
+| **Discord** | `discord.com/api/webhooks` | Rich embed with color and fields |
+| **Generic** | Anything else | Plain JSON event payload |
+
+Delimit auto-detects the platform from the URL and formats the message accordingly. Webhook failures are logged as warnings but never fail your CI run.
+
+### Discord example
+
+```yaml
+- uses: delimit-ai/delimit-action@v1
+ with:
+ spec: api/openapi.yaml
+ webhook_url: ${{ secrets.DISCORD_WEBHOOK }}
+```
+
+### Generic webhook
+
+Any URL that is not Slack or Discord receives a JSON payload:
+
+```json
+{
+ "event": "breaking_changes_detected",
+ "repo": "org/repo",
+ "pr_number": 123,
+ "pr_title": "Update user endpoints",
+ "breaking_changes": 3,
+ "additive_changes": 1,
+ "semver": "MAJOR",
+ "pr_url": "https://github.com/org/repo/pull/123"
+}
+```
+
+---
+
+## Advisory vs Enforce Mode
+
+| Behavior | `advisory` (default) | `enforce` |
+|----------|---------------------|-----------|
+| PR comment | Yes | Yes |
+| GitHub annotations | Yes | Yes |
+| Fails CI on breaking changes | **No** | **Yes** |
+| Exit code on violations | `0` | `1` |
+
+**Start with advisory mode.** It gives your team visibility into API changes without blocking merges. Once your team is comfortable, switch to `enforce` to gate deployments.
+
+```yaml
+# Advisory — non-blocking (default)
+- uses: delimit-ai/delimit-action@v1
+ with:
+ old_spec: base/api/openapi.yaml
+ new_spec: api/openapi.yaml
+ mode: advisory
+
+# Enforce — blocks merge on breaking changes
+- uses: delimit-ai/delimit-action@v1
+ with:
+ old_spec: base/api/openapi.yaml
+ new_spec: api/openapi.yaml
+ mode: enforce
+```
+
+---
+
+## Examples
+
+### Advisory mode (recommended starting point)
+
+```yaml
+name: API Check
+on: pull_request
+
+jobs:
+ api-check:
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.base.sha }}
+ path: base
+ - uses: delimit-ai/delimit-action@v1
+ with:
+ old_spec: base/api/openapi.yaml
+ new_spec: api/openapi.yaml
+```
+
+### Enforce mode
+
+```yaml
+name: API Governance
+on: pull_request
+
+jobs:
+ api-check:
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.base.sha }}
+ path: base
+ - uses: delimit-ai/delimit-action@v1
+ with:
+ old_spec: base/api/openapi.yaml
+ new_spec: api/openapi.yaml
+ mode: enforce
+```
+
+### Custom policy file
+
+```yaml
+- uses: delimit-ai/delimit-action@v1
+ with:
+ old_spec: base/api/openapi.yaml
+ new_spec: api/openapi.yaml
+ mode: enforce
+ policy_file: .delimit/policies.yml
+```
+
+### Using outputs to control downstream jobs
+
+```yaml
+jobs:
+ api-check:
+ runs-on: ubuntu-latest
+ outputs:
+ breaking: ${{ steps.delimit.outputs.breaking_changes_detected }}
+ bump: ${{ steps.delimit.outputs.semver_bump }}
+ next_version: ${{ steps.delimit.outputs.next_version }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.base.sha }}
+ path: base
+ - uses: delimit-ai/delimit-action@v1
+ id: delimit
+ with:
+ old_spec: base/api/openapi.yaml
+ new_spec: api/openapi.yaml
+
+ deploy:
+ needs: api-check
+ if: needs.api-check.outputs.breaking != 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo "Safe to deploy — next version ${{ needs.api-check.outputs.next_version }}"
+```
+
+---
+
+## Supported Formats
+
+- OpenAPI 3.0 and 3.1
+- Swagger 2.0
+- YAML and JSON spec files
+
+---
+
+## What the PR Comment Looks Like
+
+When Delimit detects changes, it posts (or updates) a comment on your pull request:
+
+```
+## Delimit: Breaking Changes `MAJOR`
+
+| Metric | Value |
+|--------|-------|
+| Semver bump | `major` |
+| Next version | `2.0.0` |
+| Total changes | 5 |
+| Breaking | 2 |
+| Violations | 2 |
+
+### Violations
+
+| Severity | Rule | Description | Location |
+|----------|------|-------------|----------|
+| Error | Forbid Endpoint Removal | Endpoint /users/{id} cannot be removed | `/users/{id}` |
+| Warning | Warn on Type Changes | Type changed from string to integer | `/users:200.age` |
+
+
+Migration guide
+...step-by-step instructions for each breaking change...
+
+```
+
+The comment is automatically updated on each push to the PR branch. No duplicate comments.
+
+---
+
+## FAQ / Troubleshooting
+
+### Delimit skipped validation and did nothing
+
+Both `old_spec` and `new_spec` must be provided. If either is empty, Delimit exits cleanly with no output. Make sure both paths point to valid spec files.
+
+### How do I get the base branch spec?
+
+Use a second `actions/checkout` step to check out the base branch into a subdirectory:
+
+```yaml
+- uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.base.sha }}
+ path: base
+```
+
+Then reference `base/path/to/openapi.yaml` as `old_spec`.
+
+### My spec file is not found
+
+Verify the path relative to the repository root. Common locations:
+- `api/openapi.yaml`
+- `docs/openapi.yaml`
+- `openapi.yaml`
+- `swagger.json`
+
+### The action posts duplicate PR comments
+
+Delimit searches for an existing comment containing "Delimit" from a bot user and updates it in place. If you see duplicates, ensure `github_token` has `pull-requests: write` permission.
+
+### Can I use this with JSON specs?
+
+Yes. Delimit supports both YAML (`.yaml`, `.yml`) and JSON (`.json`) spec files. Set the input paths accordingly.
+
+### Can I use this in a monorepo with multiple specs?
+
+Yes. Add multiple Delimit steps, each with different `old_spec` / `new_spec` pairs:
+
+```yaml
+- uses: delimit-ai/delimit-action@v1
+ with:
+ old_spec: base/services/users/openapi.yaml
+ new_spec: services/users/openapi.yaml
+
+- uses: delimit-ai/delimit-action@v1
+ with:
+ old_spec: base/services/billing/openapi.yaml
+ new_spec: services/billing/openapi.yaml
+```
+
+### Advisory mode still shows errors in the PR comment — is that expected?
+
+Yes. Advisory mode reports everything (including errors) in the PR comment and GitHub annotations, but it always exits with code `0` so your CI stays green. Switch to `enforce` mode when you want breaking changes to block the merge.
+
+### How is the semver bump calculated?
+
+The classification is deterministic:
+- **major** — any breaking change detected (endpoint removed, required param added, field removed, type changed, etc.)
+- **minor** — additive changes only (new endpoints, optional fields, enum values added)
+- **patch** — non-functional changes only (description updates)
+- **none** — no changes detected
+
+---
+
+## CLI
+
+For local development, pre-commit checks, and CI/CD pipelines outside GitHub Actions, use the [Delimit CLI](https://www.npmjs.com/package/delimit-cli):
+
+```bash
+npm install -g delimit-cli
+delimit lint api/openapi.yaml
+delimit diff old-api.yaml new-api.yaml
+delimit explain old-api.yaml new-api.yaml --template migration
+```
+
+---
+
+## Links
+
+- [Delimit CLI on npm](https://www.npmjs.com/package/delimit-cli) — Local development tool
+- [GitHub Repository](https://github.com/delimit-ai/delimit) — Source code and issues
+- [GitHub Action Marketplace](https://github.com/marketplace/actions/delimit-api-governance) — Install from Marketplace
+
+---
+
+## License
+
+MIT
\ No newline at end of file