From baf3ecbfd48ce5f598ab8f7a353843340eff69fb Mon Sep 17 00:00:00 2001 From: VedranZoricic Date: Mon, 9 Mar 2026 13:47:06 +0000 Subject: [PATCH 1/6] Add CLAUDE.md and generate-openapi-from-pr skill Add repo-level CLAUDE.md documenting the OpenAPI spec structure, CI/CD workflows, editing conventions, and SDK generation safety rules. Add a Claude skill that automates generating OpenAPI spec changes from intercom monolith PRs. The skill analyzes PR diffs (controllers, presenters, version changes, routes) and produces the corresponding YAML updates. Includes reference guides for Ruby-to-OpenAPI mapping, YAML patterns, and version propagation. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/generate-openapi-from-pr/SKILL.md | 305 ++++++++ .../openapi-patterns.md | 707 ++++++++++++++++++ .../ruby-to-openapi-mapping.md | 480 ++++++++++++ .../version-propagation.md | 135 ++++ CLAUDE.md | 133 ++++ 5 files changed, 1760 insertions(+) create mode 100644 .claude/skills/generate-openapi-from-pr/SKILL.md create mode 100644 .claude/skills/generate-openapi-from-pr/openapi-patterns.md create mode 100644 .claude/skills/generate-openapi-from-pr/ruby-to-openapi-mapping.md create mode 100644 .claude/skills/generate-openapi-from-pr/version-propagation.md create mode 100644 CLAUDE.md diff --git a/.claude/skills/generate-openapi-from-pr/SKILL.md b/.claude/skills/generate-openapi-from-pr/SKILL.md new file mode 100644 index 0000000..c22ab7f --- /dev/null +++ b/.claude/skills/generate-openapi-from-pr/SKILL.md @@ -0,0 +1,305 @@ +--- +name: generate-openapi-from-pr +description: Generate OpenAPI spec changes from an intercom monolith PR. Use when a user provides an intercom/intercom PR URL or number and wants to create the corresponding OpenAPI documentation changes in this repo. +allowed-tools: Task, Read, Glob, Grep, Write, Edit, Bash, AskUserQuestion +--- + +# Generate OpenAPI Spec from Intercom PR + +This skill takes an intercom monolith PR (from `intercom/intercom`) and generates the corresponding OpenAPI spec changes in this repo (`Intercom-OpenAPI`). + +## When to Activate + +- User provides an `intercom/intercom` PR URL or number +- User asks to "generate docs from PR", "create OpenAPI from PR", "document this API change" + +## Workflow + +### Step 1: Parse Input + +Extract the PR number from the user's input. Accept: +- Full URL: `https://github.com/intercom/intercom/pull/12345` +- Short reference: `intercom/intercom#12345` +- Just a number: `12345` (assume intercom/intercom) + +### Step 2: Fetch PR Details + +```bash +# Get PR description, metadata, and state +gh pr view --repo intercom/intercom --json title,body,files,labels,state + +# Get the full diff (works for open and merged PRs) +gh pr diff --repo intercom/intercom +``` + +If the diff is too large, fetch individual changed files instead: +```bash +gh pr view --repo intercom/intercom --json files --jq '.files[].path' +``` + +Then fetch specific files of interest (controllers, models, version changes, routes). + +For merged PRs where you need the full file (not just diff), fetch from the default branch: +```bash +gh api repos/intercom/intercom/contents/ --jq '.content' | base64 -d +``` + +### Step 3: Analyze the Diff + +Scan the diff for these file patterns and extract API-relevant information: + +#### 3a. Controllers (`app/controllers/api/v3/`) + +Look for: +- **New controller files** → new API resource with endpoints +- **New actions** (`def index`, `def show`, `def create`, `def update`, `def destroy`) → new operations +- **`requires_version_change`** → which version change gates this endpoint +- **`render_json Api::V3::Models::XxxResponse`** → identifies the response model/presenter +- **`params.slice(...).permit(...)`** or **request parser classes** (`RequestParser`, `StrongParams`) → request body fields +- **Error handling** (`raise Api::V3::Errors::ApiCodedError`) → error responses +- **`before_action :check_api_version!`** → version-gated endpoint + +#### 3b. Models/Presenters (`app/presenters/api/v3/` or `app/lib/api/v3/models/`) + +Look for: +- **`serialized_attributes do`** blocks → response schema properties +- **`attribute :field_name`** → schema field definition +- **`stringify: true`** → field is string type (even if integer in DB) +- **`from_model` method** → how the model maps from internal objects +- **Conditional attributes** based on version → version-specific fields + +#### 3c. Version Changes (`app/lib/api/versioning/changes/`) + +Look for: +- **`define_description`** → description of the API change (use in PR/commit message) +- **`define_is_breaking`** → whether this is a breaking change +- **`define_is_ready_for_release`** → usually `false` for new changes +- **`define_transformation ... data.except(:field1, :field2)`** → these fields are NEW (removed for old versions) +- **`define_transformation` with data modification** → field format/value changed between versions + +#### 3d. Version Registration (`app/lib/api/versioning/service.rb`) + +Look for which version block the new change is added to: +- `UnstableVersion.new(changes: [...])` → goes in Unstable (version `0/`) +- `Version.new(id: "2.15", changes: [...])` → goes in that specific version + +#### 3e. Routes (`config/routes/api_v3.rb`) + +Look for: +- `resources :things` → standard CRUD: index, show, create, update, destroy +- `resources :things, only: [:index, :show]` → limited operations +- `member do ... end` → actions on specific resource (e.g., PUT `/things/{id}/action`) +- `collection do ... end` → actions on resource collection (e.g., POST `/things/search`) +- Nested resources → parent/child paths (e.g., `/contacts/{id}/tags`) + +#### 3f. OAuth Scopes (`app/lib/policy/api_controller_routes_oauth_scope_policy.rb`) + +Look for scope mappings to understand required auth scope for the endpoint. + +### Step 4: Ask User for Version Targeting + +Present the findings and ask: + +``` +I found the following API changes in PR #XXXXX: +- [list of changes found] + +Which API versions should I update? +- Unstable only (default for new features) +- Specific versions (for bug fixes/backports) +``` + +Default to Unstable (`descriptions/0/api.intercom.io.yaml`) unless the PR clearly targets specific versions. + +### Step 5: Read Target Spec File(s) + +Read the target spec file(s) to understand: +- Existing endpoints in the same resource group (for consistent naming/style) +- Existing schemas that can be reused or extended +- The `intercom_version` enum (to verify version values) +- Where to insert new paths/schemas (maintain alphabetical or logical grouping) +- **All inline examples that reference the affected schema** — when adding a field, you must update every response example that returns that schema. Search with: `grep -n 'schemas/' ` + +### Step 6: Generate OpenAPI Changes + +Use the patterns from the companion guide files: +- **[./ruby-to-openapi-mapping.md](./ruby-to-openapi-mapping.md)** — Ruby pattern → OpenAPI mapping rules +- **[./openapi-patterns.md](./openapi-patterns.md)** — Concrete YAML templates +- **[./version-propagation.md](./version-propagation.md)** — Cross-version propagation rules + +#### Adding a new field to an existing schema (most common change) + +This is the most frequent PR type. You must update TWO places in the spec: + +1. **Schema property** — add the new field to `components/schemas//properties` +2. **Inline response examples** — search for ALL endpoints that return this schema and update their inline example `value` objects to include the new field + +Example from a real PR (adding `previous_ticket_state_id` to ticket): +```yaml +# 1. In components/schemas/ticket/properties — add the field: +previous_ticket_state_id: + type: string + nullable: true + description: The ID of the previous ticket state. + example: '7493' + +# 2. In EVERY endpoint response example that returns a ticket — add the value: +examples: + Successful response: + value: + type: ticket + id: '494' + # ... existing fields ... + previous_ticket_state_id: '7490' # ADD THIS +``` + +**To find all examples that need updating**, search the spec file for the schema name: +```bash +grep -n 'schemas/ticket' descriptions/0/api.intercom.io.yaml +``` +Then check each endpoint that references that schema and update its inline examples. + +#### Required elements for every new endpoint: + +1. **`summary`** — short description used as page title in docs (required) + +2. **`description`** — longer explanation of what the endpoint does + +3. **`Intercom-Version` header parameter** — always reference the schema: + ```yaml + parameters: + - name: Intercom-Version + in: header + schema: + "$ref": "#/components/schemas/intercom_version" + ``` + +4. **`tags`** — group name matching existing tags or new tag for new resource + +5. **`operationId`** — unique, camelCase identifier (e.g., `listTags`, `createContact`, `retrieveTicket`) + +6. **Response with inline examples + schema ref**: + ```yaml + responses: + '200': + description: Successful response + content: + application/json: + examples: + Successful response: + value: + type: resource + id: '123' + schema: + "$ref": "#/components/schemas/resource_schema" + ``` + +7. **Standard error responses** — at minimum `401 Unauthorized`: + ```yaml + '401': + description: Unauthorized + content: + application/json: + examples: + Unauthorized: + value: + type: error.list + request_id: + errors: + - code: unauthorized + message: Access Token Invalid + schema: + "$ref": "#/components/schemas/error" + ``` + +8. **Request body** (for POST/PUT) with schema and examples + +#### Required elements for new schemas: + +1. **`title`** — Title Case +2. **`type: object`** +3. **`x-tags`** — tag group linkage (must match a top-level tag name) +4. **`description`** +5. **`properties`** — each with `type`, `description`, `example` +6. **`nullable: true`** — on fields that can be null +7. **Timestamps** — `type: integer` + `format: date-time` + +#### Top-level `tags` section (for new resources) + +If adding a completely new API resource (not just new endpoints on an existing resource), you MUST add a tag entry to the **top-level `tags` array** at the bottom of the spec file (after `security`): + +```yaml +tags: +# ... existing tags ... +- name: Your Resource + description: Everything about your Resource +``` + +This tag name must match: +- The `tags` array on each endpoint (e.g., `tags: [Your Resource]`) +- The `x-tags` on related schemas (e.g., `x-tags: [Your Resource]`) + +Existing top-level tags include: Admins, AI Content, Articles, Away Status Reasons, Brands, Calls, Companies, Contacts, Conversations, Custom Channel Events, Custom Object Instances, Data Attributes, Data Events, Data Export, Emails, Help Center, Internal Articles, Jobs, Macros, Messages, News, Notes, Segments, Subscription Types, Switch, Tags, Teams, Ticket States, Ticket Type Attributes, Ticket Types, Tickets, Visitors, Workflows. + +#### Reusable `components/responses` (optional) + +The spec defines reusable error responses in `components/responses`: +- `Unauthorized` — 401 with access token invalid +- `TypeNotFound` — 404 for custom object types +- `ObjectNotFound` — 404 for objects/custom objects/integrations +- `ValidationError` — validation errors +- `BadRequest` — bad request errors +- `CustomChannelNotificationSuccess` — custom channel success + +You can reference these with `"$ref": "#/components/responses/Unauthorized"` instead of inlining the error response, but most existing endpoints inline their errors. **Match the style of nearby endpoints** in the same resource group. + +### Step 7: Apply Changes + +Use the Edit tool to insert changes into the spec file(s). Be careful about: +- YAML indentation (2-space indent throughout) +- Inserting paths in logical order (group related endpoints together) +- Inserting schemas alphabetically in `components/schemas` +- Adding new top-level tags in alphabetical order in the `tags` array +- Not breaking existing content + +### Step 8: Validate + +Run Fern validation: +```bash +fern check +``` + +If validation fails, read the error output and fix the issues. + +### Step 9: Summarize + +Report to the user: +- What was added/changed (new endpoints, new schemas, new fields, new top-level tags) +- Which files were modified +- Which versions were updated +- Any manual follow-up needed + +#### Follow-up Checklist + +Always remind the user of remaining manual steps: + +1. **Review generated changes** for accuracy against the actual API behavior +2. **Fern overrides** — if new endpoints were added to Unstable, check if `fern/unstable-openapi-overrides.yml` needs SDK method name entries +3. **Developer-docs PR** — copy the updated spec to the `developer-docs` repo: + - Copy `descriptions/0/api.intercom.io.yaml` → `docs/references/@Unstable/rest-api/api.intercom.io.yaml` + - For stable versions: `descriptions/2.15/api.intercom.io.yaml` → `docs/references/@2.15/rest-api/api.intercom.io.yaml` +4. **Changelog** — if the change should appear in the public changelog, update `docs/references/@/changelog.md` in the developer-docs repo (newest entries at top) +5. **Cross-version changes** — if this is an unversioned change (affects all versions), also update `docs/build-an-integration/learn-more/rest-apis/unversioned-changes.md` in developer-docs +6. **Run `fern check`** to validate before committing + +## Important Notes + +- **Do NOT run `fern generate` without `--preview`** — this would auto-submit PRs to SDK repos +- **Examples must be realistic** — use plausible IDs, emails, timestamps +- **Match existing style** — look at nearby endpoints for naming and formatting conventions +- **Cross-reference with existing schemas** — reuse `$ref` to existing schemas wherever possible +- **Nullable fields** — always explicitly mark with `nullable: true` +- **The `error` schema** is already defined — always reference it with `"$ref": "#/components/schemas/error"` +- **Top-level tags** — new resources need a tag in the `tags` array at the bottom of the spec +- **Servers** — the spec already defines 3 regional servers (US, EU, AU) — do not modify +- **Security** — global `bearerAuth` is already configured — do not modify diff --git a/.claude/skills/generate-openapi-from-pr/openapi-patterns.md b/.claude/skills/generate-openapi-from-pr/openapi-patterns.md new file mode 100644 index 0000000..2e99749 --- /dev/null +++ b/.claude/skills/generate-openapi-from-pr/openapi-patterns.md @@ -0,0 +1,707 @@ +# OpenAPI YAML Patterns + +Concrete templates extracted from the actual Intercom OpenAPI spec. Use these as the basis for generating new endpoints and schemas. + +## Endpoint Patterns + +### GET List Endpoint + +```yaml +"/tags": + get: + summary: List all tags + parameters: + - name: Intercom-Version + in: header + schema: + "$ref": "#/components/schemas/intercom_version" + tags: + - Tags + operationId: listTags + description: "You can fetch a list of all tags for a given workspace.\n\n" + responses: + '200': + description: successful + content: + application/json: + examples: + successful: + value: + type: list + data: + - type: tag + id: '102' + name: Manual tag 1 + schema: + "$ref": "#/components/schemas/tag_list" + '401': + description: Unauthorized + content: + application/json: + examples: + Unauthorized: + value: + type: error.list + request_id: 2859da57-c83f-405c-8166-240a312442a3 + errors: + - code: unauthorized + message: Access Token Invalid + schema: + "$ref": "#/components/schemas/error" +``` + +### GET List with Pagination + +```yaml +"/contacts": + get: + summary: List all contacts + parameters: + - name: Intercom-Version + in: header + schema: + "$ref": "#/components/schemas/intercom_version" + - name: page + in: query + description: The page of results to fetch. + schema: + type: integer + example: 1 + - name: per_page + in: query + description: The number of results per page. + schema: + type: integer + example: 50 + tags: + - Contacts + operationId: ListContacts + description: | + You can fetch a list of all contacts (ie. users or leads) in your workspace. + {% admonition type="warning" name="Pagination" %} + You can use pagination to limit the number of results returned. + {% /admonition %} + responses: + '200': + description: successful + content: + application/json: + examples: + successful: + value: + type: list + data: [] + total_count: 0 + pages: + type: pages + page: 1 + per_page: 50 + total_pages: 0 + schema: + "$ref": "#/components/schemas/contact_list" + '401': + description: Unauthorized + content: + application/json: + examples: + Unauthorized: + value: + type: error.list + request_id: e097e446-9ae6-44a8-8e13-2bf3008b87ef + errors: + - code: unauthorized + message: Access Token Invalid + schema: + "$ref": "#/components/schemas/error" +``` + +### GET by ID Endpoint + +```yaml +"/tags/{id}": + get: + summary: Find a specific tag + parameters: + - name: Intercom-Version + in: header + schema: + "$ref": "#/components/schemas/intercom_version" + - name: id + in: path + description: The unique identifier of a given tag + example: '123' + required: true + schema: + type: string + tags: + - Tags + operationId: findTag + description: | + You can fetch the details of tags that are on the workspace by their id. + This will return a tag object. + responses: + '200': + description: Tag found + content: + application/json: + examples: + Tag found: + value: + type: tag + id: '113' + name: Manual tag + schema: + "$ref": "#/components/schemas/tag_basic" + '404': + description: Tag not found + content: + application/json: + examples: + Tag not found: + value: + type: error.list + request_id: e20c89d2-29c6-4abb-aa3d-c860e1cec1ca + errors: + - code: not_found + message: Resource Not Found + schema: + "$ref": "#/components/schemas/error" + '401': + description: Unauthorized + content: + application/json: + examples: + Unauthorized: + value: + type: error.list + request_id: f230e3a7-00a9-456b-bf1c-2ad4b7dc49f6 + errors: + - code: unauthorized + message: Access Token Invalid + schema: + "$ref": "#/components/schemas/error" +``` + +### POST Create Endpoint + +```yaml +"/contacts": + post: + summary: Create contact + parameters: + - name: Intercom-Version + in: header + schema: + "$ref": "#/components/schemas/intercom_version" + tags: + - Contacts + operationId: CreateContact + description: You can create a new contact (ie. user or lead). + responses: + '200': + description: successful + content: + application/json: + examples: + successful: + value: + type: contact + id: 6762f0dd1bb69f9f2193bb83 + email: joebloggs@intercom.io + created_at: 1734537437 + schema: + "$ref": "#/components/schemas/contact" + '401': + description: Unauthorized + content: + application/json: + examples: + Unauthorized: + value: + type: error.list + request_id: ff2353d3-d3d6-4f20-8268-847869d01e73 + errors: + - code: unauthorized + message: Access Token Invalid + schema: + "$ref": "#/components/schemas/error" + requestBody: + content: + application/json: + schema: + "$ref": "#/components/schemas/create_contact_request" + examples: + successful: + summary: successful + value: + email: joebloggs@intercom.io +``` + +### PUT Update Endpoint + +```yaml +"/admins/{id}/away": + put: + summary: Set an admin to away + parameters: + - name: Intercom-Version + in: header + schema: + "$ref": "#/components/schemas/intercom_version" + - name: id + in: path + required: true + description: The unique identifier of a given admin + schema: + type: integer + tags: + - Admins + operationId: setAwayAdmin + description: You can set an Admin as away for the Inbox. + responses: + '200': + description: Successful response + content: + application/json: + examples: + Successful response: + value: + type: admin + id: '991267460' + name: Ciaran2 Lee + email: admin2@email.com + away_mode_enabled: true + away_mode_reassign: true + has_inbox_seat: true + schema: + "$ref": "#/components/schemas/admin" + '404': + description: Admin not found + content: + application/json: + examples: + Admin not found: + value: + type: error.list + request_id: efcd0531-798b-4c22-bccd-68877ed7faa4 + errors: + - code: admin_not_found + message: Admin for admin_id not found + schema: + "$ref": "#/components/schemas/error" + requestBody: + content: + application/json: + schema: + type: object + required: + - away_mode_enabled + - away_mode_reassign + properties: + away_mode_enabled: + type: boolean + description: Set to "true" to change the status of the admin to away. + example: true + away_mode_reassign: + type: boolean + description: Set to "true" to assign new replies to default inbox. + example: false + examples: + successful_response: + summary: Successful response + value: + away_mode_enabled: true + away_mode_reassign: true +``` + +### DELETE Endpoint + +```yaml +"/tags/{id}": + delete: + summary: Delete tag + parameters: + - name: Intercom-Version + in: header + schema: + "$ref": "#/components/schemas/intercom_version" + - name: id + in: path + description: The unique identifier of a given tag + example: '123' + required: true + schema: + type: string + tags: + - Tags + operationId: deleteTag + description: You can delete tags by passing in the id. + responses: + '200': + description: Successful + '404': + description: Resource not found + content: + application/json: + examples: + Resource not found: + value: + type: error.list + request_id: 49536975-bbc5-4a2f-ab8b-7928275cb4d3 + errors: + - code: not_found + message: Resource Not Found + schema: + "$ref": "#/components/schemas/error" + '401': + description: Unauthorized + content: + application/json: + examples: + Unauthorized: + value: + type: error.list + request_id: a3e5b8e2-1234-5678-9abc-def012345678 + errors: + - code: unauthorized + message: Access Token Invalid + schema: + "$ref": "#/components/schemas/error" +``` + +### POST Search Endpoint + +```yaml +"/conversations/search": + post: + summary: Search conversations + parameters: + - name: Intercom-Version + in: header + schema: + "$ref": "#/components/schemas/intercom_version" + tags: + - Conversations + operationId: searchConversations + description: | + You can search for multiple conversations by the value of their attributes. + responses: + '200': + description: successful + content: + application/json: + examples: + successful: + value: + type: conversation.list + conversations: [] + total_count: 0 + pages: + type: pages + page: 1 + per_page: 20 + total_pages: 0 + schema: + "$ref": "#/components/schemas/conversation_list" + requestBody: + content: + application/json: + schema: + "$ref": "#/components/schemas/search_request" + examples: + successful: + summary: successful + value: + query: + operator: AND + value: + - field: created_at + operator: ">" + value: '1306054154' +``` + +--- + +## Schema Patterns + +### Simple Object Schema + +```yaml +tag_basic: + title: Tag + type: object + x-tags: + - Tags + description: A tag allows you to label your contacts, companies, and conversations. + properties: + type: + type: string + description: value is "tag" + example: tag + id: + type: string + description: The id of the tag. + example: '123' + name: + type: string + description: The name of the tag. + example: Test tag +``` + +### Schema with Nullable Fields + +```yaml +tag: + title: Tag + type: object + x-tags: + - Tags + description: A tag allows you to label your contacts, companies, and conversations. + properties: + type: + type: string + description: value is "tag" + example: tag + id: + type: string + description: The id of the tag. + example: '123' + name: + type: string + description: The name of the tag. + example: Test tag + applied_at: + type: integer + format: date-time + nullable: true + description: The time when the tag was applied to the object. + example: 1663597223 + applied_by: + type: object + nullable: true + description: The admin who applied the tag. + allOf: + - "$ref": "#/components/schemas/reference" +``` + +### List/Collection Schema + +```yaml +tag_list: + title: Tags + type: object + description: A list of tags objects in the workspace. + properties: + type: + type: string + description: The type of the object + enum: + - list + example: list + data: + type: array + description: A list of tags objects + items: + "$ref": "#/components/schemas/tag" +``` + +### Request Body Schema + +```yaml +create_or_update_tag_request: + title: Create or Update Tag Request Payload + type: object + description: You can create or update an existing tag. + required: + - name + properties: + name: + type: string + description: The name of the tag, which will be created if not found. + example: Independent + id: + type: string + description: The id of tag to updates. + example: '656452352' +``` + +### Schema with Enum + +```yaml +activity_log: + title: Activity Log + type: object + description: Activities performed by Admins. + nullable: true + properties: + activity_type: + type: string + enum: + - admin_away_mode_change + - admin_deletion + - admin_login_success + - app_name_change + example: app_name_change +``` + +### Nested Reference (allOf pattern) + +Use `allOf` when referencing another schema as a nullable property: + +```yaml +properties: + applied_by: + type: object + nullable: true + description: The admin who applied the tag. + allOf: + - "$ref": "#/components/schemas/reference" +``` + +Direct `$ref` when the field is not nullable: +```yaml +properties: + assignee: + "$ref": "#/components/schemas/admin" +``` + +### Paginated List Schema + +```yaml +contact_list: + title: Contact List + type: object + description: A list of contacts. + properties: + type: + type: string + enum: + - list + example: list + data: + type: array + items: + "$ref": "#/components/schemas/contact" + total_count: + type: integer + description: Total number of contacts. + example: 100 + pages: + "$ref": "#/components/schemas/cursor_pages" +``` + +--- + +## Common Reusable Schemas + +These schemas already exist in the spec. Always use `$ref` instead of redefining: + +| Schema | Use for | +|---|---| +| `error` | All error responses | +| `cursor_pages` | Pagination metadata in list responses | +| `reference` | Simple `{type, id}` reference to another object | +| `intercom_version` | The `Intercom-Version` header enum | +| `admin` | Admin/teammate objects | +| `contact` | Contact (user/lead) objects | +| `tag` | Tag with applied_at/applied_by | +| `tag_basic` | Tag without applied_at/applied_by | +| `tag_list` | List of tags | + +--- + +## Top-Level Spec Sections + +These sections appear at the bottom of each spec file (after `components/schemas`). Only modify when adding entirely new API resources. + +### Top-Level Tags (add for new resources) + +Located after `security:` at the end of the file. Tags define resource groups in the API docs sidebar. + +```yaml +tags: +- name: Admins + description: Everything about your Admins +- name: Articles + description: Everything about your Articles +# ... alphabetically ordered ... +- name: Your New Resource + description: Everything about your New Resource +``` + +Some tags have extended descriptions or `externalDocs`: +```yaml +- name: Conversations + description: Everything about your Conversations + externalDocs: + description: What is a conversation? + url: https://www.intercom.com/help/en/articles/4323904-what-is-a-conversation +``` + +Some tags use admonitions in descriptions: +```yaml +- name: Custom Object Instances + description: | + Everything about your Custom Object instances. + {% admonition type="warning" name="Permission Requirements" %} + From now on, to access this endpoint, you need additional permissions. + {% /admonition %} +``` + +### Servers (do NOT modify) + +```yaml +servers: +- url: https://api.intercom.io + description: The production API server +- url: https://api.eu.intercom.io + description: The european API server +- url: https://api.au.intercom.io + description: The australian API server +``` + +### Security (do NOT modify) + +```yaml +security: +- bearerAuth: [] +``` + +### Reusable Responses (`components/responses`) + +The spec defines reusable error responses. Most endpoints inline their errors, but these are available: + +```yaml +components: + responses: + Unauthorized: + description: Unauthorized + content: + application/json: + examples: + Unauthorized: + value: + type: error.list + request_id: 12a938a3-314e-4939-b773-5cd45738bd21 + errors: + - code: unauthorized + message: Access Token Invalid + schema: + "$ref": "#/components/schemas/error" +``` + +--- + +## Style Rules + +1. **Indentation**: 2 spaces throughout +2. **Strings**: Quote strings that could be ambiguous YAML (`'200'`, `'123'`, `'true'`) +3. **Descriptions**: Use `|` for multi-line descriptions, inline for single-line +4. **Examples**: Use realistic but fake data (not real customer data) +5. **Request IDs**: Use UUID format for `request_id` in error examples +6. **Timestamps**: Integer UNIX timestamps (e.g., `1734537437`), not ISO strings +7. **IDs**: String-quoted integers (e.g., `'123'`) for most resource IDs +8. **Refs**: Use `"$ref"` (quoted, with dollar sign) for JSON references +9. **Enum values**: Always include an `example` that matches one of the enum values +10. **operationId**: Must be unique across the entire spec +11. **Top-level tags**: Alphabetical order, required for new resources diff --git a/.claude/skills/generate-openapi-from-pr/ruby-to-openapi-mapping.md b/.claude/skills/generate-openapi-from-pr/ruby-to-openapi-mapping.md new file mode 100644 index 0000000..da21ed5 --- /dev/null +++ b/.claude/skills/generate-openapi-from-pr/ruby-to-openapi-mapping.md @@ -0,0 +1,480 @@ +# Ruby-to-OpenAPI Mapping Reference + +This guide maps common Ruby API patterns in the intercom monolith to their corresponding OpenAPI YAML output. + +## Routes → Paths + +### Standard RESTful Resources + +```ruby +# Ruby route +resources :tags, only: [:index, :show, :create, :destroy] +``` + +Maps to: +```yaml +# OpenAPI paths +"/tags": + get: # index + operationId: listTags + post: # create + operationId: createTag +"/tags/{id}": + get: # show + operationId: findTag + delete: # destroy + operationId: deleteTag +``` + +### Member Routes (actions on a specific resource) + +```ruby +# Ruby route +resources :admins, only: [:index, :show] do + member do + put :away + end +end +``` + +Maps to: +```yaml +"/admins": + get: + operationId: listAdmins +"/admins/{id}": + get: + operationId: retrieveAdmin +"/admins/{id}/away": + put: + operationId: setAwayAdmin +``` + +### Collection Routes (actions on the resource collection) + +```ruby +# Ruby route +resources :conversations do + collection do + post :search + end +end +``` + +Maps to: +```yaml +"/conversations/search": + post: + operationId: searchConversations +``` + +### Nested Resources + +```ruby +# Ruby route +resources :contacts do + resources :tags, only: [:index, :create, :destroy] +end +``` + +Maps to: +```yaml +"/contacts/{contact_id}/tags": + get: + operationId: listTagsForAContact + post: + operationId: attachTagToContact +"/contacts/{contact_id}/tags/{id}": + delete: + operationId: detachTagFromContact +``` + +## Controller Actions → Operations + +### Action Name Conventions + +| Rails Action | HTTP Method | operationId Pattern | Example | +|---|---|---|---| +| `index` | GET | `list{Resources}` | `listTags` | +| `show` | GET | `retrieve{Resource}` or `find{Resource}` or `Show{Resource}` | `findTag` | +| `create` | POST | `create{Resource}` or `Create{Resource}` | `createTag` | +| `update` | PUT | `update{Resource}` or `Update{Resource}` | `UpdateContact` | +| `destroy` | DELETE | `delete{Resource}` | `deleteTag` | +| `search` | POST | `search{Resources}` | `searchConversations` | +| custom | varies | descriptive camelCase | `setAwayAdmin`, `convertVisitor` | + +Note: operationId naming is inconsistent in the existing spec (some camelCase, some PascalCase). Match the style of the resource group you're adding to. + +### Request Parameters + +```ruby +# Controller code +def update + tag = Tags::Update.run!( + params.slice(:name, :id).permit(:name, :id), + { app: current_app, admin: current_admin } + ) +end +``` + +Maps to request body: +```yaml +requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: The name of the tag. + example: "Updated tag name" + id: + type: string + description: The unique identifier of the tag. + example: "123" + examples: + successful: + summary: Successful update + value: + name: "Updated tag name" + id: "123" +``` + +### Path Parameters + +```ruby +# Controller with path param +def show + tag = Tag.find(params[:id]) +end +``` + +Maps to: +```yaml +parameters: +- name: id + in: path + description: The unique identifier of the tag. + example: '123' + required: true + schema: + type: string +``` + +### Query Parameters + +```ruby +# Controller with query params +def index + per_page = params[:per_page] || 50 + page = params[:page] || 1 +end +``` + +Maps to: +```yaml +parameters: +- name: per_page + in: query + description: Number of results per page. + schema: + type: integer + default: 50 +- name: page + in: query + description: Page number. + schema: + type: integer + default: 1 +``` + +## Presenters/Models → Schemas + +### Simple Presenter + +```ruby +# Ruby presenter +class TagResponse < ::Api::Versioning::Presenter + serialized_attributes do + attribute :type + attribute :id, stringify: true + attribute :name + end + + def type + "tag" + end +end +``` + +Maps to schema: +```yaml +components: + schemas: + tag: + title: Tag + type: object + x-tags: + - Tags + description: A tag allows you to label your contacts, companies, and conversations. + properties: + type: + type: string + description: value is "tag" + example: tag + id: + type: string + description: The id of the tag. + example: '123' + name: + type: string + description: The name of the tag. + example: Test tag +``` + +### Attribute Type Mapping + +| Ruby/Presenter Pattern | OpenAPI Type | +|---|---| +| `attribute :name` (string field) | `type: string` | +| `attribute :id, stringify: true` | `type: string` (even though integer in DB) | +| `attribute :count` (integer field) | `type: integer` | +| `attribute :enabled` (boolean field) | `type: boolean` | +| `attribute :created_at` (timestamp) | `type: integer` + `format: date-time` | +| `attribute :items` (array) | `type: array` + `items:` | +| `attribute :metadata` (hash/object) | `type: object` | +| Field can be nil | Add `nullable: true` | + +### Nested Object References + +```ruby +# Presenter with nested object +class ConversationResponse + serialized_attributes do + attribute :assignee # returns an Admin object + attribute :tags # returns a TagList object + end +end +``` + +Maps to: +```yaml +properties: + assignee: + "$ref": "#/components/schemas/admin" + tags: + "$ref": "#/components/schemas/tag_list" +``` + +For nullable nested objects, use `allOf`: +```yaml +properties: + applied_by: + type: object + nullable: true + description: The admin who applied the tag. + allOf: + - "$ref": "#/components/schemas/reference" +``` + +### List/Collection Schema + +```ruby +# List presenter +class TagListResponse + def type; "list"; end + attribute :data # array of TagResponse +end +``` + +Maps to: +```yaml +tag_list: + title: Tags + type: object + description: A list of tags. + properties: + type: + type: string + enum: + - list + example: list + data: + type: array + description: A list of tags + items: + "$ref": "#/components/schemas/tag" +``` + +## Version Changes → What's New + +### Field Addition (via transformation) + +```ruby +# Version change that removes field for old versions +class AddTagsToConversationPart < ::Api::Versioning::Change + define_transformation ::Api::V3::Models::VersionedConversationPartResponse do |target_model:, data:| + data.except(:tags) + end +end +``` + +This means: **`tags` is a NEW field** added in this version. It's removed from the response for older API versions. + +In OpenAPI: Add the `tags` property to the response schema, and include it in response examples. + +### Field Modification (via transformation) + +```ruby +# Version change that modifies field format +class ChangeTimestampFormat < ::Api::Versioning::Change + define_transformation ::Api::V3::Models::SomeResponse do |target_model:, data:| + data.merge(created_at: data[:created_at].to_s) + end +end +``` + +This means: the field format changed between versions. Document the new format. + +### New Endpoint (via version gating) + +```ruby +# Controller with version check +class TicketsController < OauthAuthenticatedController + requires_version_change ::Api::Versioning::Changes::AddTicketsApi + + def show + # ... + end +end +``` + +This means: the entire endpoint only exists in versions that include `AddTicketsApi`. Add the endpoint to the Unstable spec (or whichever version includes the change). + +### Version Registration Location + +```ruby +# In app/lib/api/versioning/service.rb +UnstableVersion.new(changes: [ + Changes::AddNewFeature, # → add to descriptions/0/ +]) + +Version.new(id: "2.15", changes: [ + Changes::SomeOtherChange, # → add to descriptions/2.15/ +]) +``` + +## Response Rendering → Response Schema + +### Simple Response + +```ruby +render_json Api::V3::Models::TagResponse.from_model(model: tag, api_version: current_api_version) +``` + +Maps to: +```yaml +responses: + '200': + description: Successful response + content: + application/json: + schema: + "$ref": "#/components/schemas/tag" +``` + +### Error Responses + +```ruby +raise Api::V3::Errors::ApiCodedError.new( + status: 404, + type: Api::V3::Errors::Code::NOT_FOUND, + message: "Resource Not Found" +) +``` + +Maps to: +```yaml +'404': + description: Resource not found + content: + application/json: + examples: + Resource not found: + value: + type: error.list + request_id: + errors: + - code: not_found + message: Resource Not Found + schema: + "$ref": "#/components/schemas/error" +``` + +### Common Error Codes + +| Ruby Error Code | OpenAPI `code` value | HTTP Status | +|---|---|---| +| `NOT_FOUND` | `not_found` | 404 | +| `UNAUTHORIZED` | `unauthorized` | 401 | +| `PARAMETER_INVALID` | `parameter_invalid` | 400 | +| `PARAMETER_NOT_FOUND` | `parameter_not_found` | 400 | +| `INTERCOM_VERSION_INVALID` | `intercom_version_invalid` | 400 | +| `FORBIDDEN` | `forbidden` | 403 | +| `UNPROCESSABLE_ENTITY` | `unprocessable_entity` | 422 | + +## Multiple Request Body Shapes (oneOf) + +When a single endpoint accepts different request body formats (e.g., POST `/tags` can create a tag, tag companies, untag companies, or tag users), use `oneOf`: + +```ruby +# Controller accepts different params depending on the operation +def create + if params[:companies] + # tag/untag companies + elsif params[:users] + # tag users + else + # create/update tag + end +end +``` + +Maps to: +```yaml +requestBody: + content: + application/json: + schema: + oneOf: + - "$ref": "#/components/schemas/create_or_update_tag_request" + - "$ref": "#/components/schemas/tag_company_request" + - "$ref": "#/components/schemas/untag_company_request" + - "$ref": "#/components/schemas/tag_multiple_users_request" + examples: + create_tag: + summary: Create a tag + value: + name: test + tag_company: + summary: Tag a company + value: + name: test + companies: + - company_id: '123' +``` + +## Adding a Field to an Existing Schema + +The most common change type. Requires updates in TWO places: + +1. **Add the property to `components/schemas//properties`** +2. **Update ALL inline response examples** that return this schema + +To find all affected examples, search for `schemas/` in the spec: +```bash +grep -n 'schemas/ticket' descriptions/0/api.intercom.io.yaml +``` + +Then check each endpoint that references that schema `$ref` and add the new field value to its inline examples. diff --git a/.claude/skills/generate-openapi-from-pr/version-propagation.md b/.claude/skills/generate-openapi-from-pr/version-propagation.md new file mode 100644 index 0000000..b7965a3 --- /dev/null +++ b/.claude/skills/generate-openapi-from-pr/version-propagation.md @@ -0,0 +1,135 @@ +# Version Propagation Guide + +How to decide which API version spec files to update and how to propagate changes across versions. + +## Version Directory Mapping + +| Version | Directory | intercom_version default | Notes | +|---|---|---|---| +| Unstable | `descriptions/0/` | `Unstable` | All new features land here first | +| 2.15 | `descriptions/2.15/` | `2.14` | Current latest stable | +| 2.14 | `descriptions/2.14/` | `2.14` | | +| 2.13 | `descriptions/2.13/` | `2.11` | | +| 2.12 | `descriptions/2.12/` | `2.11` | | +| 2.11 | `descriptions/2.11/` | `2.11` | | +| 2.10 | `descriptions/2.10/` | `2.10` | | +| 2.9 | `descriptions/2.9/` | `2.9` | | +| 2.8 | `descriptions/2.8/` | `2.8` | | +| 2.7 | `descriptions/2.7/` | `2.7` | | + +## Decision Tree: Which Versions to Update + +### 1. New Feature (most common) + +**Update: Unstable only** (`descriptions/0/api.intercom.io.yaml`) + +If the PR adds the version change to `UnstableVersion` in the versioning service, only update the Unstable spec. This is the case for ~95% of API changes. + +### 2. Bug Fix to Existing Documentation + +**Update: All affected versions** + +If the PR fixes a bug in how an existing field/endpoint is documented (wrong type, missing example, incorrect description), propagate the fix to all versions that have the endpoint. + +### 3. Feature Promoted to Stable Version + +**Update: Target version + all later versions + Unstable** + +If the PR adds a version change to a specific numbered version (e.g., `Version.new(id: "2.15")`), update that version and all later versions. Features in a numbered version are also in all subsequent versions. + +Example: Change added to 2.14 → update 2.14, 2.15, and Unstable. + +### 4. Backport to Older Versions + +**Update: All specified versions** + +Rare. If the PR explicitly mentions backporting, update all specified versions. Check the PR description and version change registration for which versions are affected. + +## Key Differences Between Version Files + +### intercom_version Enum + +Each version's spec has a different enum for `intercom_version`: + +**Unstable** includes all versions plus `Unstable`: +```yaml +intercom_version: + example: Unstable + default: Unstable + enum: + - '1.0' + - '1.1' + # ... all versions ... + - '2.14' + - Unstable +``` + +**Stable versions** include all versions up to and including themselves: +```yaml +# v2.15 +intercom_version: + example: '2.14' + default: '2.14' + enum: + - '1.0' + - '1.1' + # ... all versions up to ... + - '2.14' +``` + +Note: The v2.15 spec has `default: '2.14'` and does NOT include `'2.15'` in its own enum. This is an existing pattern — follow it. + +### info.version + +Each spec has a different `info.version`: +```yaml +# Unstable +info: + version: Unstable + +# v2.15 +info: + version: '2.15' +``` + +### Available Endpoints + +Unstable has endpoints that don't exist in stable versions. When adding an endpoint to Unstable, do NOT add it to stable versions unless the corresponding version change is registered in that version's change list. + +### Schema Differences + +Some schemas have additional fields in Unstable that don't exist in stable versions. When propagating a fix, be careful not to add Unstable-only fields to stable versions. + +## Propagation Checklist + +When updating multiple versions: + +1. **Start with Unstable** — make the change in `descriptions/0/api.intercom.io.yaml` +2. **Copy to each target version** — replicate the same change in each version file +3. **Verify consistency** — ensure the change makes sense in context of each version (don't add references to schemas that don't exist in older versions) +4. **Check intercom_version** — do NOT modify the `intercom_version` enum unless explicitly adding a new API version +5. **Run validation** — `fern check` validates the spec used by Fern (currently v2.14 + Unstable), but manually review other versions + +## Fern Validation Scope + +`fern check` only validates: +- `descriptions/2.14/api.intercom.io.yaml` (stable SDK source) +- `descriptions/0/api.intercom.io.yaml` (unstable SDK source) + +Changes to other version files (2.7-2.13, 2.15) are NOT validated by Fern. Be extra careful with YAML syntax in those files. + +## Fern Overrides + +When adding new endpoints to Unstable, you may need to add entries to `fern/unstable-openapi-overrides.yml` for SDK method naming: + +```yaml +paths: + '/your_new_endpoint': + get: + x-fern-sdk-group-name: + - yourResource + x-fern-sdk-method-name: list + x-fern-request-name: ListYourResourceRequest +``` + +Check existing entries in the overrides file for the pattern. Not all endpoints need overrides — Fern can often infer good names from the spec. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..88ac830 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,133 @@ +# Intercom OpenAPI Specifications + +Source of truth for Intercom's public REST API [OpenAPI 3.0.1](https://www.openapis.org/) specs. Changes here drive SDK generation (TypeScript, Java, Python, PHP), Postman collections, and are manually synced to [developer-docs](https://github.com/intercom/developer-docs). + +Owner: `team-data-foundations` (see `REPO_OWNER`) + +## Structure + +``` +descriptions/ + 2.7/ ... 2.15/ # Stable versioned API specs (one YAML each) + 0/ # Unstable version (version "0" internally) +fern/ + generators.yml # SDK generation config (TS, Java, Python, PHP) + openapi-overrides.yml # Fern overrides for stable spec (v2.14) + unstable-openapi-overrides.yml # Fern overrides for Unstable + fern.config.json # Fern org config +scripts/ + run-sync.js # Entry point for spec upload (CI only) + upload-api-specification.js # Upload logic for ReadMe API + postman/ # Postman collection generation +postman/ # Postman collection outputs +``` + +## Key Files + +- **API specs**: `descriptions//api.intercom.io.yaml` — one self-contained OpenAPI 3.0.1 YAML per version (~23K lines for v2.15) +- **SDK config**: `fern/generators.yml` — defines 4 SDK groups: `ts-sdk`, `java-sdk`, `python-sdk`, `php-sdk` +- **Fern overrides**: `fern/openapi-overrides.yml` — patches applied on top of the spec for SDK generation + +## Development + +```bash +npm install +``` + +No dev server, no test suite, no linting commands. Validation is done via Fern: + +```bash +npm install -g fern-api +fern check # Validate spec +fern generate --preview --group ts-sdk # Preview SDK generation +``` + +## CI/CD (GitHub Actions) + +| Workflow | Trigger | What it does | +|---|---|---| +| `fern_check.yml` | PR + push to main | Validates spec with `fern check` | +| `preview_sdks.yml` | PR (changes in `fern/`) | Preview-generates all 4 SDKs and compiles them | +| `ts-sdk.yml` | Manual dispatch | Releases TypeScript SDK to npm | +| `java-sdk.yml` | Manual dispatch | Releases Java SDK to Maven | +| `python-sdk.yml` | Manual dispatch | Releases Python SDK to PyPI | +| `php-sdk.yml` | Manual dispatch | Releases PHP SDK to Packagist | +| `generate_postman_collection.yml` | Push to main | Deploys updated Postman collections | + +## Editing API Specs + +### Adding/Modifying Fields + +1. Edit `descriptions//api.intercom.io.yaml` +2. Add property definition with `type`, `description`, `example`, and `nullable` if applicable +3. Update inline response examples in the same file (examples are inline, schemas use `$ref`) +4. Apply change to ALL affected versions (specs are independent copies, not inherited) + +### OpenAPI Conventions + +```yaml +# Every property needs an example +field_name: + type: string + description: The field description. + example: 'some-value' + nullable: true # Explicit when field can be null + +# Response pattern: schema via $ref, examples inline +responses: + '200': + content: + application/json: + examples: + Successful response: + value: + type: resource + id: '123' + schema: + "$ref": "#/components/schemas/resource_schema" +``` + +### Cross-Version Changes + +When a change applies to multiple API versions, you must edit each version's YAML independently. There is no inheritance between versions — each `descriptions//api.intercom.io.yaml` is a standalone file. + +### Version Numbering + +- Stable versions: `2.7`, `2.8`, ..., `2.15` (current latest) +- Unstable: stored as `descriptions/0/` (mapped from "Unstable" in upload scripts) +- Every endpoint requires an `Intercom-Version` header parameter + +## SDK Generation (Fern) + +SDKs are generated from the spec using [Fern](https://buildwithfern.com/). The `fern/generators.yml` configures: +- **Stable SDK source**: `descriptions/2.14/api.intercom.io.yaml` (note: v2.14, not v2.15) +- **Unstable SDK source**: `descriptions/0/api.intercom.io.yaml` (namespace: `unstable`) + +### DANGER: Never run `fern generate` without `--preview` locally + +Running `fern generate --group ` (without `--preview`) auto-submits PRs to the SDK GitHub repos. This is CI-only. + +```bash +# SAFE — local preview only +fern generate --preview --group ts-sdk + +# DANGEROUS — opens PRs on SDK repos! +fern generate --group ts-sdk +``` + +## PR Workflow + +- PRs trigger `fern check` validation and SDK preview builds (if `fern/` changed) +- PR template asks for: what changed + which API versions are affected +- Merges to main auto-deploy Postman collections +- SDK releases are manual (workflow_dispatch) + +## Skills + +- **generate-openapi-from-pr** — Takes an intercom monolith PR and generates OpenAPI spec changes. Provide a PR URL/number from `intercom/intercom` and the skill analyzes the diff (controllers, presenters, version changes, routes) to produce the corresponding YAML updates in the target spec file(s). See `.claude/skills/generate-openapi-from-pr/SKILL.md`. + +## Downstream Consumers + +Changes in this repo must be manually synced to: +- [intercom/developer-docs](https://github.com/intercom/developer-docs) — the spec YAMLs are copied into `docs/references/@/rest-api/` +- SDK repos receive changes via Fern-generated PRs (automated on release) From b94bf0ed2750799473e290a6081d0c406dcf9a5e Mon Sep 17 00:00:00 2001 From: VedranZoricic Date: Mon, 9 Mar 2026 14:00:19 +0000 Subject: [PATCH 2/6] Add auto-inject hook, argument hint, and CLAUDE.md skill directive Add a UserPromptSubmit hook that detects intercom PR URLs and injects context telling Claude to use the generate-openapi-from-pr skill. Add user-invocable metadata and argument-hint to SKILL.md frontmatter. Update CLAUDE.md to explicitly direct Claude to use the skill. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/hooks/openapi-skill-auto-inject.sh | 24 +++++++++++++++++++ .claude/settings.json | 14 +++++++++++ .../skills/generate-openapi-from-pr/SKILL.md | 7 ++++++ CLAUDE.md | 2 ++ 4 files changed, 47 insertions(+) create mode 100755 .claude/hooks/openapi-skill-auto-inject.sh create mode 100644 .claude/settings.json diff --git a/.claude/hooks/openapi-skill-auto-inject.sh b/.claude/hooks/openapi-skill-auto-inject.sh new file mode 100755 index 0000000..c234268 --- /dev/null +++ b/.claude/hooks/openapi-skill-auto-inject.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# UserPromptSubmit hook that auto-injects generate-openapi-from-pr skill instruction +# when user provides an intercom/intercom PR URL or asks to generate OpenAPI docs from a PR + +# Read hook event data from stdin +hook_data=$(cat) + +# Extract user message from the JSON +user_message=$(echo "$hook_data" | jq -r '.prompt // empty' 2>/dev/null) + +# Check if user message contains intercom PR references or OpenAPI generation keywords +if echo "$user_message" | grep -qiE 'intercom/intercom(/pull/|#)[0-9]+|generate.*(openapi|open-api|spec).*(from|pr)|openapi.*(from|pr)|document.*(this|api).*(pr|change)'; then + cat <<'EOF' +{ + "hookSpecificOutput": { + "hookEventName": "UserPromptSubmit", + "additionalContext": "OPENAPI: The user wants to generate OpenAPI spec changes from an intercom PR. Use the generate-openapi-from-pr skill to handle this request." + } +} +EOF +else + # No match - no additional context + exit 0 +fi diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..0fbab6e --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/openapi-skill-auto-inject.sh" + } + ] + } + ] + } +} diff --git a/.claude/skills/generate-openapi-from-pr/SKILL.md b/.claude/skills/generate-openapi-from-pr/SKILL.md index c22ab7f..3095ce9 100644 --- a/.claude/skills/generate-openapi-from-pr/SKILL.md +++ b/.claude/skills/generate-openapi-from-pr/SKILL.md @@ -1,6 +1,13 @@ --- name: generate-openapi-from-pr description: Generate OpenAPI spec changes from an intercom monolith PR. Use when a user provides an intercom/intercom PR URL or number and wants to create the corresponding OpenAPI documentation changes in this repo. + +metadata: + author: team-data-foundations + version: "1.0" + user-invocable: true + argument-hint: "" + allowed-tools: Task, Read, Glob, Grep, Write, Edit, Bash, AskUserQuestion --- diff --git a/CLAUDE.md b/CLAUDE.md index 88ac830..7e286e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -126,6 +126,8 @@ fern generate --group ts-sdk - **generate-openapi-from-pr** — Takes an intercom monolith PR and generates OpenAPI spec changes. Provide a PR URL/number from `intercom/intercom` and the skill analyzes the diff (controllers, presenters, version changes, routes) to produce the corresponding YAML updates in the target spec file(s). See `.claude/skills/generate-openapi-from-pr/SKILL.md`. +When a user provides an `intercom/intercom` PR URL or asks to generate OpenAPI docs from a PR, always use the `generate-openapi-from-pr` skill. Invoke it with: `/generate-openapi-from-pr ` + ## Downstream Consumers Changes in this repo must be manually synced to: From 125406413130a02454ecc023677f235fb1847997 Mon Sep 17 00:00:00 2001 From: VedranZoricic Date: Mon, 9 Mar 2026 14:05:10 +0000 Subject: [PATCH 3/6] Improve skill based on skill-creator review - Make description pushier for better trigger accuracy (catches PR URLs, "update the spec", "document API change", etc.) - Remove redundant "When to Activate" section (triggering is from description) - Slim SKILL.md from 313 to 205 lines by moving inline YAML templates to reference files and using conditional pointers instead - Add clear "read X when doing Y" guidance for reference files - Condense endpoint/schema checklists into scannable summaries Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/generate-openapi-from-pr/SKILL.md | 149 +++--------------- 1 file changed, 21 insertions(+), 128 deletions(-) diff --git a/.claude/skills/generate-openapi-from-pr/SKILL.md b/.claude/skills/generate-openapi-from-pr/SKILL.md index 3095ce9..ddc7e32 100644 --- a/.claude/skills/generate-openapi-from-pr/SKILL.md +++ b/.claude/skills/generate-openapi-from-pr/SKILL.md @@ -1,6 +1,12 @@ --- name: generate-openapi-from-pr -description: Generate OpenAPI spec changes from an intercom monolith PR. Use when a user provides an intercom/intercom PR URL or number and wants to create the corresponding OpenAPI documentation changes in this repo. +description: > + Generate OpenAPI spec changes from an intercom monolith PR. Use this skill whenever a user + provides an intercom/intercom PR URL or number, asks to generate or update OpenAPI docs, + update the spec, document an API change, or mentions shipping API documentation from a PR. + Also trigger when the user pastes a github.com/intercom/intercom/pull/ URL even without + explicit instructions — they almost certainly want spec changes generated. This is the + primary workflow for this repository. metadata: author: team-data-foundations @@ -15,11 +21,6 @@ allowed-tools: Task, Read, Glob, Grep, Write, Edit, Bash, AskUserQuestion This skill takes an intercom monolith PR (from `intercom/intercom`) and generates the corresponding OpenAPI spec changes in this repo (`Intercom-OpenAPI`). -## When to Activate - -- User provides an `intercom/intercom` PR URL or number -- User asks to "generate docs from PR", "create OpenAPI from PR", "document this API change" - ## Workflow ### Step 1: Parse Input @@ -129,136 +130,28 @@ Read the target spec file(s) to understand: ### Step 6: Generate OpenAPI Changes -Use the patterns from the companion guide files: -- **[./ruby-to-openapi-mapping.md](./ruby-to-openapi-mapping.md)** — Ruby pattern → OpenAPI mapping rules -- **[./openapi-patterns.md](./openapi-patterns.md)** — Concrete YAML templates -- **[./version-propagation.md](./version-propagation.md)** — Cross-version propagation rules - -#### Adding a new field to an existing schema (most common change) - -This is the most frequent PR type. You must update TWO places in the spec: - -1. **Schema property** — add the new field to `components/schemas//properties` -2. **Inline response examples** — search for ALL endpoints that return this schema and update their inline example `value` objects to include the new field - -Example from a real PR (adding `previous_ticket_state_id` to ticket): -```yaml -# 1. In components/schemas/ticket/properties — add the field: -previous_ticket_state_id: - type: string - nullable: true - description: The ID of the previous ticket state. - example: '7493' - -# 2. In EVERY endpoint response example that returns a ticket — add the value: -examples: - Successful response: - value: - type: ticket - id: '494' - # ... existing fields ... - previous_ticket_state_id: '7490' # ADD THIS -``` +Read the appropriate reference file based on what the PR changes: -**To find all examples that need updating**, search the spec file for the schema name: +- **Adding/modifying fields or schemas?** → Read [./ruby-to-openapi-mapping.md](./ruby-to-openapi-mapping.md) for how Ruby presenter attributes map to OpenAPI types +- **Adding new endpoints?** → Read [./openapi-patterns.md](./openapi-patterns.md) for concrete YAML templates (GET, POST, PUT, DELETE, search) +- **Updating multiple versions?** → Read [./version-propagation.md](./version-propagation.md) for the decision tree on which files to update + +#### The two most important rules + +**Rule 1: Field additions require updates in TWO places.** When adding a field to a schema, you must update both the schema definition in `components/schemas` AND every inline response example that returns that schema. Find all affected examples with: ```bash -grep -n 'schemas/ticket' descriptions/0/api.intercom.io.yaml -``` -Then check each endpoint that references that schema and update its inline examples. - -#### Required elements for every new endpoint: - -1. **`summary`** — short description used as page title in docs (required) - -2. **`description`** — longer explanation of what the endpoint does - -3. **`Intercom-Version` header parameter** — always reference the schema: - ```yaml - parameters: - - name: Intercom-Version - in: header - schema: - "$ref": "#/components/schemas/intercom_version" - ``` - -4. **`tags`** — group name matching existing tags or new tag for new resource - -5. **`operationId`** — unique, camelCase identifier (e.g., `listTags`, `createContact`, `retrieveTicket`) - -6. **Response with inline examples + schema ref**: - ```yaml - responses: - '200': - description: Successful response - content: - application/json: - examples: - Successful response: - value: - type: resource - id: '123' - schema: - "$ref": "#/components/schemas/resource_schema" - ``` - -7. **Standard error responses** — at minimum `401 Unauthorized`: - ```yaml - '401': - description: Unauthorized - content: - application/json: - examples: - Unauthorized: - value: - type: error.list - request_id: - errors: - - code: unauthorized - message: Access Token Invalid - schema: - "$ref": "#/components/schemas/error" - ``` - -8. **Request body** (for POST/PUT) with schema and examples - -#### Required elements for new schemas: - -1. **`title`** — Title Case -2. **`type: object`** -3. **`x-tags`** — tag group linkage (must match a top-level tag name) -4. **`description`** -5. **`properties`** — each with `type`, `description`, `example` -6. **`nullable: true`** — on fields that can be null -7. **Timestamps** — `type: integer` + `format: date-time` - -#### Top-level `tags` section (for new resources) - -If adding a completely new API resource (not just new endpoints on an existing resource), you MUST add a tag entry to the **top-level `tags` array** at the bottom of the spec file (after `security`): - -```yaml -tags: -# ... existing tags ... -- name: Your Resource - description: Everything about your Resource +grep -n 'schemas/' descriptions/0/api.intercom.io.yaml ``` -This tag name must match: -- The `tags` array on each endpoint (e.g., `tags: [Your Resource]`) -- The `x-tags` on related schemas (e.g., `x-tags: [Your Resource]`) +**Rule 2: New resources need a top-level tag.** If adding an entirely new API resource, add an entry to the `tags` array at the bottom of the spec (alphabetical order). The tag name must match the `tags` on endpoints and `x-tags` on schemas. See existing tags in [./openapi-patterns.md](./openapi-patterns.md) under "Top-Level Tags". -Existing top-level tags include: Admins, AI Content, Articles, Away Status Reasons, Brands, Calls, Companies, Contacts, Conversations, Custom Channel Events, Custom Object Instances, Data Attributes, Data Events, Data Export, Emails, Help Center, Internal Articles, Jobs, Macros, Messages, News, Notes, Segments, Subscription Types, Switch, Tags, Teams, Ticket States, Ticket Type Attributes, Ticket Types, Tickets, Visitors, Workflows. +#### Quick checklist for new endpoints -#### Reusable `components/responses` (optional) +Every endpoint needs: `summary`, `description`, `operationId` (unique, camelCase), `tags`, `Intercom-Version` header parameter (`"$ref": "#/components/schemas/intercom_version"`), response with inline examples + schema `$ref`, and at minimum a `401 Unauthorized` error response. POST/PUT endpoints also need a `requestBody` with schema and examples. See [./openapi-patterns.md](./openapi-patterns.md) for complete templates. -The spec defines reusable error responses in `components/responses`: -- `Unauthorized` — 401 with access token invalid -- `TypeNotFound` — 404 for custom object types -- `ObjectNotFound` — 404 for objects/custom objects/integrations -- `ValidationError` — validation errors -- `BadRequest` — bad request errors -- `CustomChannelNotificationSuccess` — custom channel success +#### Quick checklist for new schemas -You can reference these with `"$ref": "#/components/responses/Unauthorized"` instead of inlining the error response, but most existing endpoints inline their errors. **Match the style of nearby endpoints** in the same resource group. +Every schema needs: `title` (Title Case), `type: object`, `x-tags`, `description`, and `properties` where each property has `type`, `description`, and `example`. Mark nullable fields explicitly with `nullable: true`. Timestamps use `type: integer` + `format: date-time`. ### Step 7: Apply Changes From da4b98311af03f6c97579ce82d0f62c1afa2091f Mon Sep 17 00:00:00 2001 From: VedranZoricic Date: Mon, 9 Mar 2026 14:19:26 +0000 Subject: [PATCH 4/6] Refine skill based on test results against real PRs Tested against intercom/intercom#474982 (field addition) and #477688 (new endpoint). Both produced correct output matching human-written PRs. Improvements based on test observations: - Add fern check fallback (python YAML validation) for environments without fern installed - Add guidance to extract descriptions from version change define_description - Add guidance to match example verbosity level of sibling endpoints - Add guidance to reuse existing example value styles (IDs, workspace IDs) - Note common YAML validation pitfalls Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/generate-openapi-from-pr/SKILL.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.claude/skills/generate-openapi-from-pr/SKILL.md b/.claude/skills/generate-openapi-from-pr/SKILL.md index ddc7e32..e72d919 100644 --- a/.claude/skills/generate-openapi-from-pr/SKILL.md +++ b/.claude/skills/generate-openapi-from-pr/SKILL.md @@ -127,6 +127,7 @@ Read the target spec file(s) to understand: - The `intercom_version` enum (to verify version values) - Where to insert new paths/schemas (maintain alphabetical or logical grouping) - **All inline examples that reference the affected schema** — when adding a field, you must update every response example that returns that schema. Search with: `grep -n 'schemas/' ` +- **Existing example values** for the same resource — reuse the same style of IDs, workspace IDs, timestamps, and names that nearby endpoints use. Consistency matters more than novelty. ### Step 6: Generate OpenAPI Changes @@ -149,6 +150,10 @@ grep -n 'schemas/' descriptions/0/api.intercom.io.yaml Every endpoint needs: `summary`, `description`, `operationId` (unique, camelCase), `tags`, `Intercom-Version` header parameter (`"$ref": "#/components/schemas/intercom_version"`), response with inline examples + schema `$ref`, and at minimum a `401 Unauthorized` error response. POST/PUT endpoints also need a `requestBody` with schema and examples. See [./openapi-patterns.md](./openapi-patterns.md) for complete templates. +**Writing good descriptions:** Extract the description from the PR's version change `define_description` if available — it's usually well-written for the changelog. Supplement with details from the controller (constraints, validations, edge cases). A good description explains what the endpoint does AND when you'd use it, not just "You can do X." + +**Response example detail level:** Match the verbosity of existing examples for the same schema. If other ticket endpoints show a full ticket object with nested `ticket_parts`, `contacts`, and `linked_objects`, your example should too. If they're minimal (just `type` and `id`), keep yours minimal. Look at the nearest sibling endpoint for the right level of detail. + #### Quick checklist for new schemas Every schema needs: `title` (Title Case), `type: object`, `x-tags`, `description`, and `properties` where each property has `type`, `description`, and `example`. Mark nullable fields explicitly with `nullable: true`. Timestamps use `type: integer` + `format: date-time`. @@ -169,7 +174,12 @@ Run Fern validation: fern check ``` -If validation fails, read the error output and fix the issues. +If `fern` is not installed, fall back to YAML syntax validation: +```bash +python3 -c "import yaml; yaml.safe_load(open('descriptions/0/api.intercom.io.yaml'))" && echo "YAML valid" +``` + +If validation fails, read the error output and fix the issues. Common problems: indentation errors, missing quotes on string values that look like numbers, and duplicate keys. ### Step 9: Summarize @@ -195,8 +205,9 @@ Always remind the user of remaining manual steps: ## Important Notes - **Do NOT run `fern generate` without `--preview`** — this would auto-submit PRs to SDK repos -- **Examples must be realistic** — use plausible IDs, emails, timestamps -- **Match existing style** — look at nearby endpoints for naming and formatting conventions +- **Match existing examples** — before writing new example values, look at how nearby endpoints for the same resource format their examples. Reuse the same style of IDs (`'494'` not `'1'`), workspace IDs (`this_is_an_id664_that_should_be_at_least_`), timestamps (recent UNIX timestamps like `1719493065`), and names. Consistency across the spec is more important than creativity. +- **Match existing style** — look at nearby endpoints for naming, formatting, and level of detail in response examples. If sibling endpoints show full nested objects, yours should too. +- **Extract descriptions from the PR** — the version change's `define_description` is usually well-written. Use it as the basis for your endpoint description, then enrich with constraints and edge cases from the controller code. - **Cross-reference with existing schemas** — reuse `$ref` to existing schemas wherever possible - **Nullable fields** — always explicitly mark with `nullable: true` - **The `error` schema** is already defined — always reference it with `"$ref": "#/components/schemas/error"` From d491ac9b3191bd02672bf258828e0011c27e0b3e Mon Sep 17 00:00:00 2001 From: VedranZoricic Date: Mon, 9 Mar 2026 15:53:26 +0000 Subject: [PATCH 5/6] Address PR review findings - Replace placeholder with actual UUID format in error template to prevent literal output - Make version-propagation note future-proof by removing hardcoded v2.15 reference and adding "check the actual file" guidance - Consolidate CLAUDE.md skill directive into single paragraph Co-Authored-By: Claude Opus 4.6 (1M context) --- .../generate-openapi-from-pr/ruby-to-openapi-mapping.md | 2 +- .../skills/generate-openapi-from-pr/version-propagation.md | 2 +- CLAUDE.md | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.claude/skills/generate-openapi-from-pr/ruby-to-openapi-mapping.md b/.claude/skills/generate-openapi-from-pr/ruby-to-openapi-mapping.md index da21ed5..6f6252d 100644 --- a/.claude/skills/generate-openapi-from-pr/ruby-to-openapi-mapping.md +++ b/.claude/skills/generate-openapi-from-pr/ruby-to-openapi-mapping.md @@ -404,7 +404,7 @@ Maps to: Resource not found: value: type: error.list - request_id: + request_id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 errors: - code: not_found message: Resource Not Found diff --git a/.claude/skills/generate-openapi-from-pr/version-propagation.md b/.claude/skills/generate-openapi-from-pr/version-propagation.md index b7965a3..1d9603e 100644 --- a/.claude/skills/generate-openapi-from-pr/version-propagation.md +++ b/.claude/skills/generate-openapi-from-pr/version-propagation.md @@ -77,7 +77,7 @@ intercom_version: - '2.14' ``` -Note: The v2.15 spec has `default: '2.14'` and does NOT include `'2.15'` in its own enum. This is an existing pattern — follow it. +Note: Stable specs set `default` to the previous stable version and do NOT include their own version number in the enum. For example, v2.15 has `default: '2.14'` and does not list `'2.15'`. Always check the actual file to confirm the pattern before editing — it may evolve with future releases. ### info.version diff --git a/CLAUDE.md b/CLAUDE.md index 7e286e7..d7f7ddf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -124,9 +124,7 @@ fern generate --group ts-sdk ## Skills -- **generate-openapi-from-pr** — Takes an intercom monolith PR and generates OpenAPI spec changes. Provide a PR URL/number from `intercom/intercom` and the skill analyzes the diff (controllers, presenters, version changes, routes) to produce the corresponding YAML updates in the target spec file(s). See `.claude/skills/generate-openapi-from-pr/SKILL.md`. - -When a user provides an `intercom/intercom` PR URL or asks to generate OpenAPI docs from a PR, always use the `generate-openapi-from-pr` skill. Invoke it with: `/generate-openapi-from-pr ` +- **generate-openapi-from-pr** — Takes an intercom monolith PR and generates OpenAPI spec changes. Provide a PR URL/number from `intercom/intercom` and the skill analyzes the diff (controllers, presenters, version changes, routes) to produce the corresponding YAML updates in the target spec file(s). See `.claude/skills/generate-openapi-from-pr/SKILL.md`. This is the primary workflow for this repo — always use it when a user provides an intercom PR URL or asks to update the spec from a PR. ## Downstream Consumers From 141d301ace64bd473e613208dfaa4864476932b0 Mon Sep 17 00:00:00 2001 From: VedranZoricic Date: Mon, 9 Mar 2026 16:14:49 +0000 Subject: [PATCH 6/6] Fix misleading version defaults table in version-propagation.md Remove the intercom_version default column that mixed two different patterns without explanation, making v2.15 look like an anomaly. Instead, point readers to check the actual file. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../version-propagation.md | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/.claude/skills/generate-openapi-from-pr/version-propagation.md b/.claude/skills/generate-openapi-from-pr/version-propagation.md index 1d9603e..7c20611 100644 --- a/.claude/skills/generate-openapi-from-pr/version-propagation.md +++ b/.claude/skills/generate-openapi-from-pr/version-propagation.md @@ -4,18 +4,20 @@ How to decide which API version spec files to update and how to propagate change ## Version Directory Mapping -| Version | Directory | intercom_version default | Notes | -|---|---|---|---| -| Unstable | `descriptions/0/` | `Unstable` | All new features land here first | -| 2.15 | `descriptions/2.15/` | `2.14` | Current latest stable | -| 2.14 | `descriptions/2.14/` | `2.14` | | -| 2.13 | `descriptions/2.13/` | `2.11` | | -| 2.12 | `descriptions/2.12/` | `2.11` | | -| 2.11 | `descriptions/2.11/` | `2.11` | | -| 2.10 | `descriptions/2.10/` | `2.10` | | -| 2.9 | `descriptions/2.9/` | `2.9` | | -| 2.8 | `descriptions/2.8/` | `2.8` | | -| 2.7 | `descriptions/2.7/` | `2.7` | | +| Version | Directory | Notes | +|---|---|---| +| Unstable | `descriptions/0/` | All new features land here first | +| 2.15 | `descriptions/2.15/` | Current latest stable | +| 2.14 | `descriptions/2.14/` | Stable SDK source (used by Fern) | +| 2.13 | `descriptions/2.13/` | | +| 2.12 | `descriptions/2.12/` | | +| 2.11 | `descriptions/2.11/` | | +| 2.10 | `descriptions/2.10/` | | +| 2.9 | `descriptions/2.9/` | | +| 2.8 | `descriptions/2.8/` | | +| 2.7 | `descriptions/2.7/` | | + +Each version file is independent — always check the actual `intercom_version` schema in the file for its `default` and `enum` values before editing. ## Decision Tree: Which Versions to Update