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[![GitHub Marketplace](https://img.shields.io/badge/Marketplace-Delimit-blue)](https://github.com/marketplace/actions/delimit-api-governance)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n[![API Governance](https://delimit-ai.github.io/badge/pass.svg)](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 Delimit PR comment showing breaking changes\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 + +[![GitHub Marketplace](https://img.shields.io/badge/Marketplace-Delimit-blue)](https://github.com/marketplace/actions/delimit-api-governance) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![API Governance](https://delimit-ai.github.io/badge/pass.svg)](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 + +

+ Delimit PR comment showing breaking changes +

+ +--- + +## 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