Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/core/src/runtime/file-native-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ const resources: readonly AdapterResourceConfig[] = [
createExample:
"discovery/linear/issues/{issueId}/comments/.create.example.json",
},
{
name: "merge",
path: "/github/repos/{owner}/{repo}/pulls/{pullNumber}/merge.json",
pathPattern:
/^\/github\/repos\/[^/]+\/[^/]+\/pulls\/[1-9]\d*(?:__[^/]+)?\/merge\.json$/,
idPattern: /^[1-9]\d*(?:__.*)?$/,
schema:
"discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}/merge.json/.schema.json",
createExample:
"discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}/merge.json/.create.example.json",
},
];

const issueId = "11111111-1111-1111-1111-111111111111";
Expand Down Expand Up @@ -77,6 +88,18 @@ test("classifyWrite chooses the most specific matching resource", () => {
assert.equal(route?.resource.name, "comments");
});

test("classifyWrite maps slugged exact-file resources to patch", () => {
const route = classifyWrite(
"/github/repos/acme/widgets/pulls/7__finish-feature/merge.json",
resources
);

assert.equal(route?.kind, "patch");
assert.equal(route?.canonical, true);
assert.equal(route?.id, "7__finish-feature");
assert.equal(route?.resource.name, "merge");
});

test("classifyWrite ignores temporary and partial writeback filenames", () => {
for (const path of [
"/linear/issues/.tmp.json",
Expand Down
16 changes: 15 additions & 1 deletion packages/core/src/runtime/schema-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,10 @@ export class SchemaAdapter {
const parsed = parseEndpointDescriptor(mapping.endpoint);
const placeholders = extractEndpointParams(parsed.path);
const params = Object.fromEntries(
placeholders.map((placeholder, index) => [placeholder, wildcardValues[index] ?? ""])
placeholders.map((placeholder, index) => [
placeholder,
normalizeWritebackParam(this.name, placeholder, wildcardValues[index] ?? ""),
])
);

return {
Expand Down Expand Up @@ -1356,6 +1359,17 @@ function readString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
}

function normalizeWritebackParam(
adapterName: string,
placeholder: string,
value: string
): string {
if (adapterName === "github" && (placeholder === "pull_number" || placeholder === "issue_number")) {
return value.split("__", 1)[0] ?? value;
}
return value;
}

const SYNC_OPTION_KEYS = new Set([
"connectionId",
"cursor",
Expand Down
13 changes: 13 additions & 0 deletions packages/core/tests/runtime/schema-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ test("SchemaAdapter matches writebacks and proxies them to the provider", async
match: "/github/repos/*/*/pulls/*/reviews/*.json",
endpoint: "POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews",
},
merge: {
match: "/github/repos/*/*/pulls/*/merge.json",
endpoint: "PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge",
},
},
},
defaultConnectionId: "conn_default",
Expand All @@ -107,4 +111,13 @@ test("SchemaAdapter matches writebacks and proxies them to the provider", async
assert.equal(providerCalls.length, 1);
assert.equal(providerCalls[0]?.endpoint, "/repos/acme/demo/pulls/42/reviews");
assert.equal(providerCalls[0]?.connectionId, "conn_default");

await adapter.writeBack(
"ws_123",
"/github/repos/acme/demo/pulls/43__finish-feature/merge.json",
JSON.stringify({ merge_method: "squash" })
);

assert.equal(providerCalls.length, 2);
assert.equal(providerCalls[1]?.endpoint, "/repos/acme/demo/pulls/43/merge");
});
22 changes: 21 additions & 1 deletion packages/github/discovery/github/.adapter.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# GitHub adapter

The GitHub adapter exposes repository pull requests, issues, reviews, comments, commits, files, and checks under `/github`, with writeback support for creating and updating issues, creating and updating issue comments, and submitting pull request reviews.
The GitHub adapter exposes repository pull requests, issues, reviews, comments, commits, files, and checks under `/github`, with writeback support for creating and updating issues, creating and updating issue comments, submitting pull request reviews, and merging pull requests.

Read-only mounts:
- `/github/repos/<owner>/<repo>/pulls/<pullNumber>/meta.json` - Pull request metadata.
Expand All @@ -15,6 +15,7 @@ Resources:
| `/github/repos/{owner}/{repo}/issues/<id>.json` | `/github/repos/{owner}/{repo}/issues/.schema.json` | `/github/repos/{owner}/{repo}/issues/.create.example.json` | `^[1-9]\d*$` | Creates a GitHub issue. |
| `/github/repos/{owner}/{repo}/issues/{issueNumber}/comments/<id>.json` | `/github/repos/{owner}/{repo}/issues/{issueNumber}/comments/.schema.json` | `/github/repos/{owner}/{repo}/issues/{issueNumber}/comments/.create.example.json` | `^\d+$` | Creates or updates a GitHub issue comment. |
| `/github/repos/{owner}/{repo}/pulls/{pullNumber}/reviews/<id>.json` | `/github/repos/{owner}/{repo}/pulls/{pullNumber}/reviews/.schema.json` | `/github/repos/{owner}/{repo}/pulls/{pullNumber}/reviews/.create.example.json` | `^\d+$` | Submits a pull request review with optional inline comments. |
| `/github/repos/{owner}/{repo}/pulls/{pullNumber}/merge.json` | `/github/repos/{owner}/{repo}/pulls/{pullNumber}/merge.json/.schema.json` | `/github/repos/{owner}/{repo}/pulls/{pullNumber}/merge.json/.create.example.json` | exact file path | Merges a pull request. Uses the repository default merge strategy when no merge method is supplied. |

## Operations

Expand All @@ -30,6 +31,7 @@ Resources:
- `/github/repos/{owner}/{repo}/issues/<id>.json`: `^[1-9]\d*$`. Filenames that do not match this pattern are treated as create drafts.
- `/github/repos/{owner}/{repo}/issues/{issueNumber}/comments/<id>.json`: `^\d+$`. Filenames that do not match this pattern are treated as create drafts.
- `/github/repos/{owner}/{repo}/pulls/{pullNumber}/reviews/<id>.json`: `^\d+$`. Filenames that do not match this pattern are treated as create drafts.
- `/github/repos/{owner}/{repo}/pulls/{pullNumber}/merge.json`: exact file path.

## Write field contracts

Expand Down Expand Up @@ -94,5 +96,23 @@ Fields:
- `metadata.connectionId` (optional, string) - Relayfile connection id override.
- `metadata.providerConfigKey` (optional, string) - GitHub provider config key override.

### Merge GitHub pull request

Resource: `/github/repos/{owner}/{repo}/pulls/{pullNumber}/merge.json`
Schema: `/github/repos/{owner}/{repo}/pulls/{pullNumber}/merge.json/.schema.json`
Create example: `/github/repos/{owner}/{repo}/pulls/{pullNumber}/merge.json/.create.example.json`
Required fields: none at the top level.
Optional fields: `commit_title`, `commit_message`, `sha`, `merge_method`, `metadata`.

Fields:

- `commit_title` (optional, string) - Title for the automatic commit message.
- `commit_message` (optional, string) - Extra detail to append to automatic commit message.
- `sha` (optional, string) - Expected head SHA. GitHub refuses the merge if the pull request head has moved.
- `merge_method` (optional, string) - Merge strategy. When omitted, GitHub uses the repository default. Allowed values: `merge`, `squash`, `rebase`.
- `metadata` (optional, object) - Optional submission metadata.
- `metadata.connectionId` (optional, string) - Relayfile connection id override.
- `metadata.providerConfigKey` (optional, string) - GitHub provider config key override.

## Create Examples
Read the resource `.schema.json` first, then use the sibling `.create.example.json` as a minimal create document. The example intentionally omits read-only fields.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"merge_method": "squash"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Merge GitHub pull request",
"description": "Full resource record schema. Fields marked readOnly are synced from the provider and cannot be written by agents.",
"type": "object",
"required": [],
"properties": {
"commit_title": {
"type": "string",
"description": "Title for the automatic commit message."
},
"commit_message": {
"type": "string",
"description": "Extra detail to append to automatic commit message."
},
"sha": {
"type": "string",
"description": "Expected head SHA. GitHub refuses the merge if the pull request head has moved."
},
"merge_method": {
"type": "string",
"description": "Merge strategy. When omitted, GitHub uses the repository default.",
"enum": [
"merge",
"squash",
"rebase"
]
},
"metadata": {
"type": "object",
"description": "Optional submission metadata.",
"properties": {
"connectionId": {
"type": "string",
"description": "Relayfile connection id override."
},
"providerConfigKey": {
"type": "string",
"description": "GitHub provider config key override."
}
},
"additionalProperties": true
},
"id": {
"type": "string",
"description": "Provider canonical record id.",
"readOnly": true
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "Provider creation timestamp.",
"readOnly": true
},
"updatedAt": {
"type": "string",
"format": "date-time",
"description": "Provider last update timestamp.",
"readOnly": true
},
"url": {
"type": "string",
"format": "uri",
"description": "Provider URL for the record.",
"readOnly": true
},
"identifier": {
"type": "string",
"description": "Provider human-readable identifier or key.",
"readOnly": true
},
"provider": {
"type": "string",
"description": "Relayfile provider name.",
"readOnly": true
},
"objectType": {
"type": "string",
"description": "Relayfile object type.",
"readOnly": true
},
"objectId": {
"type": "string",
"description": "Relayfile object id.",
"readOnly": true
},
"workspaceId": {
"type": "string",
"description": "Relayfile workspace id.",
"readOnly": true
},
"connectionId": {
"type": "string",
"description": "Relayfile connection id.",
"readOnly": true
},
"_webhook": {
"type": "object",
"description": "Provider webhook metadata captured during sync.",
"readOnly": true,
"additionalProperties": true
},
"_connection": {
"type": "object",
"description": "Relayfile connection metadata captured during sync.",
"readOnly": true,
"additionalProperties": true
}
},
"additionalProperties": false,
"x-relayfile-source": {
"operationId": "pulls/merge",
"sourceKind": "openapi",
"sourcePath": "scripts/integration-contracts/github/source/openapi.yaml",
"sourceUrl": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml"
}
}
3 changes: 3 additions & 0 deletions packages/github/github.mapping.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ writebacks:
review:
match: /github/repos/*/*/pulls/*/reviews/*.json
endpoint: POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews
merge:
match: /github/repos/*/*/pulls/*/merge.json
endpoint: PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge
comment:
match: /github/repos/*/*/pulls/*/comments/*.json
endpoint: POST /repos/{owner}/{repo}/pulls/{pull_number}/comments
2 changes: 1 addition & 1 deletion packages/github/src/layout-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Always run \`ls\` before constructing a path. PR 0 standardizes issue and pull r
\`pulls/<number>__<slug>/diff.patch\`, \`pulls/<number>__<slug>/files/**\`, and \`pulls/<number>__<slug>/base/**\` are nested artifacts and should not be treated as canonical records.
Issue and pull request aliases are materialized under \`/github/repos/<owner>__<repo>/<issues|pulls>/...\`, distinct from the canonical \`/github/repos/<owner>/<repo>/...\` tree. Alias views include \`by-id/<number>.json\`, \`by-title/<slug>__<number>.json\`, \`by-state/<state>/<number>.json\`, \`by-assignee/<assignee>/<number>.json\`, \`by-creator/<creator>/<number>.json\`, \`by-priority/<priority>/<number>.json\`, and \`by-edited/YYYY-MM-DD/<number>.json\`. The edited-date bucket uses the provider update timestamp, or a merge/close timestamp when that is the most recent activity-summary fallback date.

Writable resources advertise sibling schemas and create examples at \`discovery/github/repos/{owner}/{repo}/issues/.schema.json\`, \`discovery/github/repos/{owner}/{repo}/issues/.create.example.json\`, \`discovery/github/repos/{owner}/{repo}/issues/{issueNumber}/comments/.schema.json\`, \`discovery/github/repos/{owner}/{repo}/issues/{issueNumber}/comments/.create.example.json\`, \`discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}/reviews/.schema.json\`, and \`discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}/reviews/.create.example.json\`.
Writable resources advertise sibling schemas and create examples at \`discovery/github/repos/{owner}/{repo}/issues/.schema.json\`, \`discovery/github/repos/{owner}/{repo}/issues/.create.example.json\`, \`discovery/github/repos/{owner}/{repo}/issues/{issueNumber}/comments/.schema.json\`, \`discovery/github/repos/{owner}/{repo}/issues/{issueNumber}/comments/.create.example.json\`, \`discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}/reviews/.schema.json\`, and \`discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}/reviews/.create.example.json\`. To merge a pull request, write \`{"method":"squash"}\` or another valid merge payload to \`/github/repos/<owner>/<repo>/pulls/<number>/merge.json\`.

## Indexes

Expand Down
1 change: 1 addition & 0 deletions packages/github/src/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const layoutManifest: CoreLayoutManifestProvider = () => ({
aliasSegments: ['by-id', 'by-title', 'by-state', 'by-assignee', 'by-creator', 'by-priority', 'by-edited'],
writebackResources: [
{ path: 'github/repos/*/*/pulls/reviews', schemaId: 'github/pull-request-review' },
{ path: 'github/repos/*/*/pulls/*/merge.json', schemaId: 'github/pull-request-merge' },
],
},
],
Expand Down
8 changes: 8 additions & 0 deletions packages/github/src/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export const resources = [
schema: "discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}/reviews/.schema.json",
createExample: "discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}/reviews/.create.example.json",
},
{
name: "merge",
path: "/github/repos/{owner}/{repo}/pulls/{pullNumber}/merge.json",
pathPattern: /^\/github\/repos\/[^\/]+\/[^\/]+\/pulls\/[1-9]\d*(?:__[^\/]+)?\/merge\.json$/,
idPattern: /^[1-9]\d*(?:__.*)?$/,
schema: "discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}/merge.json/.schema.json",
createExample: "discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}/merge.json/.create.example.json",
},
] as const satisfies readonly AdapterResourceConfig[];

export function findResourceByPath(path: string): AdapterResourceConfig | undefined {
Expand Down
10 changes: 10 additions & 0 deletions packages/github/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ export interface GitHubIssueCommentWritebackInput {
body: string;
}

export type GitHubMergeMethod = 'merge' | 'squash' | 'rebase';

export interface GitHubMergePullRequestWritebackInput {
method?: GitHubMergeMethod;
commitTitle?: string;
commitMessage?: string;
sha?: string;
metadata?: AgentReviewMetadata;
}

export type ProxyRequest = SharedProxyRequest;
export type ProxyResponse<T = JsonValue | null> = SharedProxyResponse<T>;

Expand Down
Loading
Loading