experimental-ash 0.56.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/docs/internals/compiler-and-artifacts.md +1 -1
- package/dist/docs/internals/context.md +3 -3
- package/dist/docs/public/advanced/typescript-api.md +19 -0
- package/dist/docs/public/advanced/vercel-deployment.md +2 -1
- package/dist/docs/public/channels/github.md +145 -0
- package/dist/docs/public/channels/index.md +23 -2
- package/dist/docs/public/sandbox.md +18 -3
- package/dist/docs/public/subagents.mdx +7 -2
- package/dist/src/compiler/manifest.d.ts +8 -2
- package/dist/src/compiler/manifest.js +1 -1
- package/dist/src/compiler/normalize-sandbox.js +1 -1
- package/dist/src/execution/remote-agent-dispatch.js +1 -1
- package/dist/src/execution/subagent-tool.js +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/authored-definition/sandbox.d.ts +2 -1
- package/dist/src/internal/authored-definition/sandbox.js +1 -1
- package/dist/src/packages/ash-scaffold/src/channels.js +1 -1
- package/dist/src/public/channels/github/api.d.ts +166 -0
- package/dist/src/public/channels/github/api.js +1 -0
- package/dist/src/public/channels/github/auth.d.ts +83 -0
- package/dist/src/public/channels/github/auth.js +2 -0
- package/dist/src/public/channels/github/binding.d.ts +49 -0
- package/dist/src/public/channels/github/binding.js +1 -0
- package/dist/src/public/channels/github/checkout.d.ts +48 -0
- package/dist/src/public/channels/github/checkout.js +1 -0
- package/dist/src/public/channels/github/constants.d.ts +2 -0
- package/dist/src/public/channels/github/constants.js +1 -0
- package/dist/src/public/channels/github/defaults.d.ts +21 -0
- package/dist/src/public/channels/github/defaults.js +3 -0
- package/dist/src/public/channels/github/dispatch.d.ts +34 -0
- package/dist/src/public/channels/github/dispatch.js +1 -0
- package/dist/src/public/channels/github/githubChannel.d.ts +109 -0
- package/dist/src/public/channels/github/githubChannel.js +1 -0
- package/dist/src/public/channels/github/inbound.d.ts +183 -0
- package/dist/src/public/channels/github/inbound.js +2 -0
- package/dist/src/public/channels/github/index.d.ts +9 -0
- package/dist/src/public/channels/github/index.js +1 -0
- package/dist/src/public/channels/github/limits.d.ts +4 -0
- package/dist/src/public/channels/github/limits.js +2 -0
- package/dist/src/public/channels/github/pr-context.d.ts +45 -0
- package/dist/src/public/channels/github/pr-context.js +5 -0
- package/dist/src/public/channels/github/state.d.ts +48 -0
- package/dist/src/public/channels/github/state.js +1 -0
- package/dist/src/public/channels/github/verify.d.ts +35 -0
- package/dist/src/public/channels/github/verify.js +1 -0
- package/dist/src/public/definitions/sandbox.d.ts +3 -3
- package/dist/src/public/sandbox/index.d.ts +1 -1
- package/dist/src/runtime/resolve-sandbox.js +1 -1
- package/dist/src/runtime/sandbox/keys.js +1 -1
- package/dist/src/runtime/sandbox/template-plan.d.ts +5 -0
- package/dist/src/runtime/sandbox/template-plan.js +1 -1
- package/dist/src/runtime/subagents/registry.js +1 -1
- package/dist/src/runtime/types.d.ts +6 -2
- package/dist/src/shared/sandbox-definition.d.ts +22 -2
- package/package.json +6 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal GitHub REST/GraphQL wrapper used by the GitHub channel.
|
|
3
|
+
*
|
|
4
|
+
* Ash exposes channel-owned helper types and functions instead of leaking a
|
|
5
|
+
* third-party SDK through public channel APIs.
|
|
6
|
+
*/
|
|
7
|
+
import { type GitHubAuthApiOptions, type GitHubChannelCredentials } from "#public/channels/github/auth.js";
|
|
8
|
+
import { type JsonObject } from "#shared/json.js";
|
|
9
|
+
/** JSON object accepted by GitHub helper calls. */
|
|
10
|
+
export type GitHubJsonObject = JsonObject;
|
|
11
|
+
/** HTTP methods supported by the low-level GitHub request helper. */
|
|
12
|
+
export type GitHubApiMethod = "DELETE" | "GET" | "PATCH" | "POST" | "PUT";
|
|
13
|
+
/** Shared GitHub API options. */
|
|
14
|
+
export interface GitHubApiOptions extends GitHubAuthApiOptions {
|
|
15
|
+
}
|
|
16
|
+
/** Options for {@link GitHubHandle.request}. */
|
|
17
|
+
export interface GitHubRequestOptions {
|
|
18
|
+
readonly auth?: boolean;
|
|
19
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
20
|
+
readonly installationId?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Low-level GitHub API response. */
|
|
23
|
+
export interface GitHubApiResponse<T = unknown> {
|
|
24
|
+
readonly body: T;
|
|
25
|
+
readonly ok: boolean;
|
|
26
|
+
readonly status: number;
|
|
27
|
+
}
|
|
28
|
+
/** Error thrown for non-2xx GitHub REST and GraphQL responses. */
|
|
29
|
+
export declare class GitHubApiError extends Error {
|
|
30
|
+
readonly body: unknown;
|
|
31
|
+
readonly method: string;
|
|
32
|
+
readonly path: string;
|
|
33
|
+
readonly status: number;
|
|
34
|
+
constructor(input: {
|
|
35
|
+
readonly body: unknown;
|
|
36
|
+
readonly method: string;
|
|
37
|
+
readonly path: string;
|
|
38
|
+
readonly status: number;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Body accepted by GitHub comment-writing helpers. */
|
|
42
|
+
export interface GitHubCommentBody {
|
|
43
|
+
readonly body: string;
|
|
44
|
+
}
|
|
45
|
+
/** Minimal posted-comment result returned by thread helpers. */
|
|
46
|
+
export interface GitHubPostedComment {
|
|
47
|
+
readonly htmlUrl: string | undefined;
|
|
48
|
+
readonly id: number;
|
|
49
|
+
readonly raw: unknown;
|
|
50
|
+
readonly url: string | undefined;
|
|
51
|
+
}
|
|
52
|
+
/** GitHub reaction contents supported by the Reactions REST API. */
|
|
53
|
+
export type GitHubReactionContent = "+1" | "-1" | "confused" | "eyes" | "heart" | "hooray" | "laugh" | "rocket";
|
|
54
|
+
interface GitHubResourceInput {
|
|
55
|
+
readonly api?: GitHubApiOptions;
|
|
56
|
+
readonly credentials?: GitHubChannelCredentials;
|
|
57
|
+
readonly installationId?: number;
|
|
58
|
+
readonly owner: string;
|
|
59
|
+
readonly repo: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Calls a GitHub REST API path with installation-token auth by default.
|
|
63
|
+
* Pass `options.auth: false` for public unauthenticated reads.
|
|
64
|
+
*/
|
|
65
|
+
export declare function callGitHubApi<T = unknown>(input: {
|
|
66
|
+
readonly api?: GitHubApiOptions;
|
|
67
|
+
readonly body?: GitHubJsonObject;
|
|
68
|
+
readonly credentials?: GitHubChannelCredentials;
|
|
69
|
+
readonly installationId?: number;
|
|
70
|
+
readonly method: GitHubApiMethod;
|
|
71
|
+
readonly path: string;
|
|
72
|
+
readonly options?: GitHubRequestOptions;
|
|
73
|
+
}): Promise<GitHubApiResponse<T>>;
|
|
74
|
+
/** Creates an issue or PR timeline comment. */
|
|
75
|
+
export declare function createGitHubIssueComment(input: GitHubResourceInput & {
|
|
76
|
+
readonly body: string | GitHubCommentBody;
|
|
77
|
+
readonly issueNumber: number;
|
|
78
|
+
}): Promise<GitHubPostedComment>;
|
|
79
|
+
/** Updates an issue or PR timeline comment. */
|
|
80
|
+
export declare function updateGitHubIssueComment(input: GitHubResourceInput & {
|
|
81
|
+
readonly body: string | GitHubCommentBody;
|
|
82
|
+
readonly commentId: number;
|
|
83
|
+
}): Promise<GitHubPostedComment>;
|
|
84
|
+
/** Replies to an inline pull-request review comment thread. */
|
|
85
|
+
export declare function createGitHubReviewCommentReply(input: GitHubResourceInput & {
|
|
86
|
+
readonly body: string | GitHubCommentBody;
|
|
87
|
+
readonly commentId: number;
|
|
88
|
+
readonly pullRequestNumber: number;
|
|
89
|
+
}): Promise<GitHubPostedComment>;
|
|
90
|
+
/** Updates an inline pull-request review comment or reply. */
|
|
91
|
+
export declare function updateGitHubPullRequestReviewComment(input: GitHubResourceInput & {
|
|
92
|
+
readonly body: string | GitHubCommentBody;
|
|
93
|
+
readonly commentId: number;
|
|
94
|
+
}): Promise<GitHubPostedComment>;
|
|
95
|
+
/** Creates a pull-request review. */
|
|
96
|
+
export declare function createGitHubPullRequestReview(input: GitHubResourceInput & {
|
|
97
|
+
readonly body: GitHubJsonObject;
|
|
98
|
+
readonly pullRequestNumber: number;
|
|
99
|
+
}): Promise<GitHubApiResponse>;
|
|
100
|
+
/** Creates an inline pull-request review comment. */
|
|
101
|
+
export declare function createGitHubPullRequestReviewComment(input: GitHubResourceInput & {
|
|
102
|
+
readonly body: GitHubJsonObject;
|
|
103
|
+
readonly pullRequestNumber: number;
|
|
104
|
+
}): Promise<GitHubPostedComment>;
|
|
105
|
+
/** Minimal pull-request metadata returned by {@link getGitHubPullRequest}. */
|
|
106
|
+
export interface GitHubPullRequestDetails {
|
|
107
|
+
readonly additions: number | undefined;
|
|
108
|
+
readonly author: GitHubPullRequestUser | undefined;
|
|
109
|
+
readonly base: GitHubPullRequestRefDetails;
|
|
110
|
+
readonly body: string | undefined;
|
|
111
|
+
readonly changedFiles: number | undefined;
|
|
112
|
+
readonly defaultBranch: string | undefined;
|
|
113
|
+
readonly deletions: number | undefined;
|
|
114
|
+
readonly draft: boolean;
|
|
115
|
+
readonly head: GitHubPullRequestRefDetails;
|
|
116
|
+
readonly htmlUrl: string | undefined;
|
|
117
|
+
readonly mergeable: boolean | null | undefined;
|
|
118
|
+
readonly number: number;
|
|
119
|
+
readonly raw: unknown;
|
|
120
|
+
readonly state: string | undefined;
|
|
121
|
+
readonly title: string;
|
|
122
|
+
}
|
|
123
|
+
/** Minimal pull-request author metadata. */
|
|
124
|
+
export interface GitHubPullRequestUser {
|
|
125
|
+
readonly id: number;
|
|
126
|
+
readonly login: string;
|
|
127
|
+
readonly type: string;
|
|
128
|
+
}
|
|
129
|
+
/** Minimal pull-request branch metadata. */
|
|
130
|
+
export interface GitHubPullRequestRefDetails {
|
|
131
|
+
readonly ref: string | undefined;
|
|
132
|
+
readonly repoFullName: string | undefined;
|
|
133
|
+
readonly sha: string | undefined;
|
|
134
|
+
}
|
|
135
|
+
/** Fetches pull-request metadata for context injection and checkout resolution. */
|
|
136
|
+
export declare function getGitHubPullRequest(input: GitHubResourceInput & {
|
|
137
|
+
readonly pullRequestNumber: number;
|
|
138
|
+
}): Promise<GitHubPullRequestDetails>;
|
|
139
|
+
/** Minimal pull-request file metadata returned by {@link listGitHubPullRequestFiles}. */
|
|
140
|
+
export interface GitHubPullRequestFile {
|
|
141
|
+
readonly additions: number | undefined;
|
|
142
|
+
readonly changes: number | undefined;
|
|
143
|
+
readonly deletions: number | undefined;
|
|
144
|
+
readonly filename: string;
|
|
145
|
+
readonly patch: string | undefined;
|
|
146
|
+
readonly status: string | undefined;
|
|
147
|
+
}
|
|
148
|
+
/** Lists files changed by a pull request. */
|
|
149
|
+
export declare function listGitHubPullRequestFiles(input: GitHubResourceInput & {
|
|
150
|
+
readonly perPage?: number;
|
|
151
|
+
readonly pullRequestNumber: number;
|
|
152
|
+
}): Promise<readonly GitHubPullRequestFile[]>;
|
|
153
|
+
/** Creates a reaction on a GitHub issue or review comment. */
|
|
154
|
+
export declare function createGitHubReaction(input: GitHubResourceInput & {
|
|
155
|
+
readonly commentId: number;
|
|
156
|
+
readonly content: GitHubReactionContent;
|
|
157
|
+
readonly subject: "issue_comment" | "pull_request_review_comment";
|
|
158
|
+
}): Promise<GitHubApiResponse>;
|
|
159
|
+
/** Fetches repository metadata, primarily to resolve `repository.id` for receive(). */
|
|
160
|
+
export declare function getGitHubRepository(input: GitHubResourceInput & {
|
|
161
|
+
readonly auth?: boolean;
|
|
162
|
+
}): Promise<{
|
|
163
|
+
readonly id: number;
|
|
164
|
+
readonly defaultBranch: string | undefined;
|
|
165
|
+
}>;
|
|
166
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isObject}from"#shared/guards.js";import{parseJsonObject}from"#shared/json.js";import{resolveGitHubInstallationToken}from"#public/channels/github/auth.js";var GitHubApiError=class extends Error{body;method;path;status;constructor(e){super(`GitHub ${e.method} ${e.path} failed with HTTP ${e.status}.`),this.name=`GitHubApiError`,this.body=e.body,this.method=e.method,this.path=e.path,this.status=e.status}};async function callGitHubApi(e){let r=e.api?.fetch??fetch,i=e.options?.auth!==!1,a={accept:`application/vnd.github+json`,"x-github-api-version":`2022-11-28`,...e.options?.headers};e.body!==void 0&&(a[`content-type`]=`application/json; charset=utf-8`),i&&(a.authorization=`Bearer ${await resolveGitHubInstallationToken({api:e.api,credentials:e.credentials,installationId:e.options?.installationId??e.installationId})}`);let o=await r(`${e.api?.apiBaseUrl??`https://api.github.com`}${e.path}`,{body:e.body===void 0?void 0:JSON.stringify(parseJsonObject(e.body)),headers:a,method:e.method}),s=await parseResponseBody(o);if(!o.ok)throw new GitHubApiError({body:s,method:e.method,path:e.path,status:o.status});return{body:s,ok:o.ok,status:o.status}}async function createGitHubIssueComment(e){return toPostedComment((await callGitHubApi({api:e.api,body:normalizeCommentBody(e.body),credentials:e.credentials,installationId:e.installationId,method:`POST`,path:`/repos/${encodePath(e.owner)}/${encodePath(e.repo)}/issues/${e.issueNumber}/comments`})).body)}async function updateGitHubIssueComment(e){return toPostedComment((await callGitHubApi({api:e.api,body:normalizeCommentBody(e.body),credentials:e.credentials,installationId:e.installationId,method:`PATCH`,path:`/repos/${encodePath(e.owner)}/${encodePath(e.repo)}/issues/comments/${e.commentId}`})).body)}async function createGitHubReviewCommentReply(e){return toPostedComment((await callGitHubApi({api:e.api,body:normalizeCommentBody(e.body),credentials:e.credentials,installationId:e.installationId,method:`POST`,path:`/repos/${encodePath(e.owner)}/${encodePath(e.repo)}/pulls/${e.pullRequestNumber}/comments/${e.commentId}/replies`})).body)}async function updateGitHubPullRequestReviewComment(e){return toPostedComment((await callGitHubApi({api:e.api,body:normalizeCommentBody(e.body),credentials:e.credentials,installationId:e.installationId,method:`PATCH`,path:`/repos/${encodePath(e.owner)}/${encodePath(e.repo)}/pulls/comments/${e.commentId}`})).body)}function createGitHubPullRequestReview(e){return callGitHubApi({api:e.api,body:e.body,credentials:e.credentials,installationId:e.installationId,method:`POST`,path:`/repos/${encodePath(e.owner)}/${encodePath(e.repo)}/pulls/${e.pullRequestNumber}/reviews`})}async function createGitHubPullRequestReviewComment(e){return toPostedComment((await callGitHubApi({api:e.api,body:e.body,credentials:e.credentials,installationId:e.installationId,method:`POST`,path:`/repos/${encodePath(e.owner)}/${encodePath(e.repo)}/pulls/${e.pullRequestNumber}/comments`})).body)}async function getGitHubPullRequest(e){return toPullRequestDetails((await callGitHubApi({api:e.api,credentials:e.credentials,installationId:e.installationId,method:`GET`,path:`/repos/${encodePath(e.owner)}/${encodePath(e.repo)}/pulls/${e.pullRequestNumber}`})).body)}async function listGitHubPullRequestFiles(e){let t=e.perPage===void 0?``:`?per_page=${Math.min(e.perPage,100)}`;return(await callGitHubApi({api:e.api,credentials:e.credentials,installationId:e.installationId,method:`GET`,path:`/repos/${encodePath(e.owner)}/${encodePath(e.repo)}/pulls/${e.pullRequestNumber}/files${t}`})).body.map(toPullRequestFile)}function createGitHubReaction(e){let t=e.subject===`issue_comment`?`issues/comments`:`pulls/comments`;return callGitHubApi({api:e.api,body:{content:e.content},credentials:e.credentials,installationId:e.installationId,method:`POST`,options:{headers:{accept:`application/vnd.github+json`}},path:`/repos/${encodePath(e.owner)}/${encodePath(e.repo)}/${t}/${e.commentId}/reactions`})}async function getGitHubRepository(t){let n=await callGitHubApi({api:t.api,credentials:t.credentials,installationId:t.installationId,method:`GET`,options:{auth:t.auth??t.installationId!==void 0},path:`/repos/${encodePath(t.owner)}/${encodePath(t.repo)}`}),r=isObject(n.body)?n.body:{},i=typeof r.id==`number`?r.id:0;return{defaultBranch:typeof r.default_branch==`string`?r.default_branch:void 0,id:i}}function normalizeCommentBody(e){return{body:typeof e==`string`?e:e.body}}function toPostedComment(t){let n=isObject(t)?t:{};return{htmlUrl:typeof n.html_url==`string`?n.html_url:void 0,id:typeof n.id==`number`?n.id:0,raw:t,url:typeof n.url==`string`?n.url:void 0}}function toPullRequestFile(t){let n=isObject(t)?t:{};return{additions:typeof n.additions==`number`?n.additions:void 0,changes:typeof n.changes==`number`?n.changes:void 0,deletions:typeof n.deletions==`number`?n.deletions:void 0,filename:typeof n.filename==`string`?n.filename:``,patch:typeof n.patch==`string`?n.patch:void 0,status:typeof n.status==`string`?n.status:void 0}}function toPullRequestDetails(t){let n=isObject(t)?t:{},r=toPullRequestRefDetails(n.base);return{additions:typeof n.additions==`number`?n.additions:void 0,author:toPullRequestUser(n.user),base:r,body:typeof n.body==`string`&&n.body.length>0?n.body:void 0,changedFiles:typeof n.changed_files==`number`?n.changed_files:void 0,defaultBranch:readDefaultBranch(n.base),deletions:typeof n.deletions==`number`?n.deletions:void 0,draft:n.draft===!0,head:toPullRequestRefDetails(n.head),htmlUrl:typeof n.html_url==`string`?n.html_url:void 0,mergeable:typeof n.mergeable==`boolean`||n.mergeable===null?n.mergeable:void 0,number:typeof n.number==`number`?n.number:0,raw:t,state:typeof n.state==`string`?n.state:void 0,title:typeof n.title==`string`?n.title:``}}function toPullRequestRefDetails(t){let n=isObject(t)?t:{},r=isObject(n.repo)?n.repo:{};return{ref:typeof n.ref==`string`?n.ref:void 0,repoFullName:typeof r.full_name==`string`?r.full_name:void 0,sha:typeof n.sha==`string`?n.sha:void 0}}function toPullRequestUser(t){if(!(!isObject(t)||typeof t.login!=`string`))return{id:typeof t.id==`number`?t.id:0,login:t.login,type:typeof t.type==`string`?t.type:`User`}}function readDefaultBranch(t){let n=isObject(t)?t:{},r=isObject(n.repo)?n.repo:{};return typeof r.default_branch==`string`?r.default_branch:void 0}async function parseResponseBody(e){let t=await e.text();if(!t)return null;try{return JSON.parse(t)}catch{return t}}function encodePath(e){return encodeURIComponent(e)}export{GitHubApiError,callGitHubApi,createGitHubIssueComment,createGitHubPullRequestReview,createGitHubPullRequestReviewComment,createGitHubReaction,createGitHubReviewCommentReply,getGitHubPullRequest,getGitHubRepository,listGitHubPullRequestFiles,updateGitHubIssueComment,updateGitHubPullRequestReviewComment};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { GitHubWebhookVerifier } from "#public/channels/github/verify.js";
|
|
2
|
+
/** GitHub App id, supplied directly or resolved lazily from a secret manager. */
|
|
3
|
+
export type GitHubAppId = number | string | (() => number | string | Promise<number | string>);
|
|
4
|
+
/** GitHub App private key, supplied directly or resolved lazily from a secret manager. */
|
|
5
|
+
export type GitHubPrivateKey = string | (() => string | Promise<string>);
|
|
6
|
+
/** GitHub webhook secret, supplied directly or resolved lazily from a secret manager. */
|
|
7
|
+
export type GitHubWebhookSecret = string | (() => string | Promise<string>);
|
|
8
|
+
/**
|
|
9
|
+
* Pre-resolved GitHub installation access token, supplied directly or
|
|
10
|
+
* resolved lazily from a secret manager. When present, ash skips the
|
|
11
|
+
* native `appId`/`privateKey`/`installationId` JWT-minting path entirely.
|
|
12
|
+
* Typically populated by integrations (e.g. Connect) that derive the
|
|
13
|
+
* installation token out-of-band.
|
|
14
|
+
*/
|
|
15
|
+
export type GitHubInstallationToken = string | (() => string | Promise<string>);
|
|
16
|
+
/** Credentials used by the native GitHub channel. */
|
|
17
|
+
export interface GitHubChannelCredentials {
|
|
18
|
+
readonly appId?: GitHubAppId;
|
|
19
|
+
readonly privateKey?: GitHubPrivateKey;
|
|
20
|
+
readonly webhookSecret?: GitHubWebhookSecret;
|
|
21
|
+
/**
|
|
22
|
+
* Pre-resolved GitHub installation access token. When supplied, ash uses
|
|
23
|
+
* it directly for authenticated GitHub API calls and skips the native
|
|
24
|
+
* `appId`/`privateKey` JWT exchange — `installationId` is not required.
|
|
25
|
+
* Typically populated by integrations (e.g. Connect) that derive the
|
|
26
|
+
* token out-of-band.
|
|
27
|
+
*/
|
|
28
|
+
readonly installationToken?: GitHubInstallationToken;
|
|
29
|
+
/**
|
|
30
|
+
* Custom inbound webhook verifier. When supplied, ash skips the
|
|
31
|
+
* `GITHUB_WEBHOOK_SECRET` fallback and delegates verification to this
|
|
32
|
+
* function. Typically populated by integrations (e.g. Connect) that
|
|
33
|
+
* authenticate webhooks out-of-band.
|
|
34
|
+
*/
|
|
35
|
+
readonly webhookVerifier?: GitHubWebhookVerifier;
|
|
36
|
+
}
|
|
37
|
+
/** Options needed by GitHub App auth helpers. */
|
|
38
|
+
export interface GitHubAuthApiOptions {
|
|
39
|
+
readonly apiBaseUrl?: string;
|
|
40
|
+
readonly fetch?: typeof fetch;
|
|
41
|
+
}
|
|
42
|
+
/** Resolves a GitHub App id, falling back to `GITHUB_APP_ID`. */
|
|
43
|
+
export declare function resolveGitHubAppId(appId?: GitHubAppId): Promise<string>;
|
|
44
|
+
/** Resolves and normalizes a GitHub App private key. */
|
|
45
|
+
export declare function resolveGitHubPrivateKey(privateKey?: GitHubPrivateKey): Promise<string>;
|
|
46
|
+
/** Resolves a GitHub webhook secret, falling back to `GITHUB_WEBHOOK_SECRET`. */
|
|
47
|
+
export declare function resolveGitHubWebhookSecret(webhookSecret?: GitHubWebhookSecret): Promise<string>;
|
|
48
|
+
/** Converts hosted-platform escaped newlines back into PEM newlines. */
|
|
49
|
+
export declare function normalizeGitHubPrivateKey(privateKey: string): string;
|
|
50
|
+
/** Creates a short-lived RS256 GitHub App JWT. */
|
|
51
|
+
export declare function createGitHubAppJwt(input: {
|
|
52
|
+
readonly appId?: GitHubAppId;
|
|
53
|
+
readonly now?: Date;
|
|
54
|
+
readonly privateKey?: GitHubPrivateKey;
|
|
55
|
+
}): Promise<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Resolves an installation access token by minting and caching a GitHub App
|
|
58
|
+
* installation token in process memory.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveGitHubInstallationToken(input: {
|
|
61
|
+
readonly api?: GitHubAuthApiOptions;
|
|
62
|
+
readonly credentials?: GitHubChannelCredentials;
|
|
63
|
+
readonly installationId: number | undefined;
|
|
64
|
+
}): Promise<string>;
|
|
65
|
+
/**
|
|
66
|
+
* Exchanges a GitHub App JWT for an installation access token, cached until
|
|
67
|
+
* shortly before GitHub's reported expiry.
|
|
68
|
+
*/
|
|
69
|
+
export declare function createGitHubInstallationToken(input: {
|
|
70
|
+
readonly api?: GitHubAuthApiOptions;
|
|
71
|
+
readonly appId?: GitHubAppId;
|
|
72
|
+
readonly installationId: number;
|
|
73
|
+
readonly privateKey?: GitHubPrivateKey;
|
|
74
|
+
}): Promise<string>;
|
|
75
|
+
/** Clears the in-memory GitHub installation token cache. Intended for tests. */
|
|
76
|
+
export declare function clearGitHubInstallationTokenCache(): void;
|
|
77
|
+
/** Seeds the in-memory installation token cache. Intended for tests. */
|
|
78
|
+
export declare function seedGitHubInstallationTokenForTests(input: {
|
|
79
|
+
readonly apiBaseUrl?: string;
|
|
80
|
+
readonly appId?: string;
|
|
81
|
+
readonly installationId: number;
|
|
82
|
+
readonly token: string;
|
|
83
|
+
}): void;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{isObject}from"#shared/guards.js";import{createSign}from"node:crypto";const installationTokenCache=new Map;async function resolveGitHubAppId(e){let t=e??process.env.GITHUB_APP_ID;if(t===void 0||t===``)throw Error(`githubChannel: GITHUB_APP_ID is required.`);let n=typeof t==`function`?await t():t;return String(n)}async function resolveGitHubPrivateKey(e){let t=e??process.env.GITHUB_APP_PRIVATE_KEY;if(!t)throw Error(`githubChannel: GITHUB_APP_PRIVATE_KEY is required.`);return normalizeGitHubPrivateKey(typeof t==`function`?await t():t)}async function resolveGitHubWebhookSecret(e){let t=e??process.env.GITHUB_WEBHOOK_SECRET;if(!t)throw Error(`githubChannel: GITHUB_WEBHOOK_SECRET is required.`);return typeof t==`function`?await t():t}function normalizeGitHubPrivateKey(e){return e.replace(/\\n/gu,`
|
|
2
|
+
`)}async function createGitHubAppJwt(e){let n=await resolveGitHubAppId(e.appId),r=await resolveGitHubPrivateKey(e.privateKey),i=Math.floor((e.now?.getTime()??Date.now())/1e3),a={alg:`RS256`,typ:`JWT`},o={exp:i+600,iat:i-60,iss:n},s=`${base64UrlJson(a)}.${base64UrlJson(o)}`;return`${s}.${createSign(`RSA-SHA256`).update(s).sign(r,`base64url`)}`}async function resolveGitHubInstallationToken(e){let t=e.credentials?.installationToken;if(t!==void 0)return typeof t==`function`?await t():t;if(e.installationId===void 0)throw Error(`githubChannel: installationId is required for authenticated GitHub API calls.`);return createGitHubInstallationToken({api:e.api,appId:e.credentials?.appId,installationId:e.installationId,privateKey:e.credentials?.privateKey})}async function createGitHubInstallationToken(t){let r=await resolveGitHubAppId(t.appId),i=t.api?.apiBaseUrl??`https://api.github.com`,a=`${i}:${r}:${t.installationId}`,o=installationTokenCache.get(a);if(o!==void 0&&Date.now()<o.expiresAtMs-6e4)return o.token;let s=await createGitHubAppJwt({appId:r,privateKey:t.privateKey}),c=await(t.api?.fetch??fetch)(`${i}/app/installations/${t.installationId}/access_tokens`,{headers:{accept:`application/vnd.github+json`,authorization:`Bearer ${s}`,"x-github-api-version":`2022-11-28`},method:`POST`}),l=await parseJsonBody(c);if(!c.ok)throw Error(`githubChannel: create installation token failed with HTTP ${c.status}.`);if(!isObject(l)||typeof l.token!=`string`)throw Error(`githubChannel: installation token response did not include a token.`);let u=parseExpiryMs(l.expires_at);return installationTokenCache.set(a,{expiresAtMs:u,token:l.token}),l.token}function clearGitHubInstallationTokenCache(){installationTokenCache.clear()}function seedGitHubInstallationTokenForTests(e){let t=e.apiBaseUrl??`https://api.github.com`,r=e.appId??`test-app`;installationTokenCache.set(`${t}:${r}:${e.installationId}`,{expiresAtMs:Date.now()+3600*1e3,token:e.token})}function base64UrlJson(e){return Buffer.from(JSON.stringify(e)).toString(`base64url`)}async function parseJsonBody(e){let t=await e.text();if(!t)return null;try{return JSON.parse(t)}catch{return t}}function parseExpiryMs(e){if(typeof e==`string`){let t=Date.parse(e);if(Number.isFinite(t))return t}return Date.now()+3600*1e3}export{clearGitHubInstallationTokenCache,createGitHubAppJwt,createGitHubInstallationToken,normalizeGitHubPrivateKey,resolveGitHubAppId,resolveGitHubInstallationToken,resolveGitHubPrivateKey,resolveGitHubWebhookSecret,seedGitHubInstallationTokenForTests};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type GitHubApiMethod, type GitHubApiOptions, type GitHubApiResponse, type GitHubJsonObject, type GitHubPostedComment, type GitHubReactionContent } from "#public/channels/github/api.js";
|
|
2
|
+
import type { GitHubChannelCredentials } from "#public/channels/github/auth.js";
|
|
3
|
+
import type { GitHubConversationKind, GitHubRepositoryRef } from "#public/channels/github/inbound.js";
|
|
4
|
+
/** Minimal config needed to rebuild GitHub API handles. */
|
|
5
|
+
export interface GitHubBindingConfig {
|
|
6
|
+
readonly api?: GitHubApiOptions;
|
|
7
|
+
readonly credentials?: GitHubChannelCredentials;
|
|
8
|
+
}
|
|
9
|
+
/** Serializable state fields needed to rebuild GitHub API handles. */
|
|
10
|
+
export interface GitHubBindingState {
|
|
11
|
+
readonly conversationKind: GitHubConversationKind;
|
|
12
|
+
readonly installationId: number | null;
|
|
13
|
+
readonly issueNumber: number | null;
|
|
14
|
+
readonly owner: string;
|
|
15
|
+
readonly pullRequestNumber: number | null;
|
|
16
|
+
readonly repo: string;
|
|
17
|
+
readonly repositoryId: number;
|
|
18
|
+
readonly reviewCommentId: number | null;
|
|
19
|
+
readonly reviewThreadRootCommentId: number | null;
|
|
20
|
+
readonly triggeringCommentId: number | null;
|
|
21
|
+
}
|
|
22
|
+
/** GitHub operations exposed to hooks and events. */
|
|
23
|
+
export interface GitHubHandle {
|
|
24
|
+
readonly installationId: number | undefined;
|
|
25
|
+
readonly repository: GitHubRepositoryRef;
|
|
26
|
+
/**
|
|
27
|
+
* Calls an arbitrary GitHub REST path with installation-token auth. Use this
|
|
28
|
+
* for any GitHub operation the channel does not wrap natively.
|
|
29
|
+
*/
|
|
30
|
+
request<T = unknown>(input: {
|
|
31
|
+
readonly body?: GitHubJsonObject;
|
|
32
|
+
readonly method: GitHubApiMethod;
|
|
33
|
+
readonly path: string;
|
|
34
|
+
}): Promise<GitHubApiResponse<T>>;
|
|
35
|
+
}
|
|
36
|
+
/** Thread-scoped operations for the current GitHub conversation. */
|
|
37
|
+
export interface GitHubThread {
|
|
38
|
+
readonly kind: GitHubConversationKind;
|
|
39
|
+
post(message: string): Promise<GitHubPostedComment>;
|
|
40
|
+
react(content: GitHubReactionContent): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
/** Rebuilds GitHub API and thread handles from durable channel state. */
|
|
43
|
+
export declare function buildGitHubBinding(input: {
|
|
44
|
+
readonly config: GitHubBindingConfig;
|
|
45
|
+
readonly state: GitHubBindingState;
|
|
46
|
+
}): {
|
|
47
|
+
readonly github: GitHubHandle;
|
|
48
|
+
readonly thread: GitHubThread;
|
|
49
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{callGitHubApi,createGitHubIssueComment,createGitHubReaction,createGitHubReviewCommentReply}from"#public/channels/github/api.js";function buildGitHubBinding(n){let{config:r,state:i}=n,a={fullName:`${i.owner}/${i.repo}`,id:i.repositoryId,name:i.repo,owner:i.owner,private:!1},o=i.installationId??void 0;return{github:{installationId:o,repository:a,request(t){return callGitHubApi({api:r.api,body:t.body,credentials:r.credentials,installationId:o,method:t.method,path:t.path})}},thread:{kind:i.conversationKind,post(e){return i.conversationKind===`review_thread`?createGitHubReviewCommentReply({api:r.api,body:e,commentId:requiredStateNumber(i.reviewThreadRootCommentId??i.reviewCommentId,`reviewThreadRootCommentId`),credentials:r.credentials,installationId:o,owner:i.owner,pullRequestNumber:requiredStateNumber(i.pullRequestNumber,`pullRequestNumber`),repo:i.repo}):createGitHubIssueComment({api:r.api,body:e,credentials:r.credentials,installationId:o,issueNumber:requiredStateNumber(i.issueNumber??i.pullRequestNumber,`issueNumber`),owner:i.owner,repo:i.repo})},async react(e){let t=i.conversationKind===`review_thread`?i.reviewCommentId:i.triggeringCommentId;t!==null&&await createGitHubReaction({api:r.api,commentId:t,content:e,credentials:r.credentials,installationId:o,owner:i.owner,repo:i.repo,subject:i.conversationKind===`review_thread`?`pull_request_review_comment`:`issue_comment`})}}}}function requiredStateNumber(e,t){if(typeof e==`number`&&Number.isFinite(e))return e;throw Error(`githubChannel: missing ${t}.`)}export{buildGitHubBinding};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type GitHubApiOptions } from "#public/channels/github/api.js";
|
|
2
|
+
import { type GitHubChannelCredentials } from "#public/channels/github/auth.js";
|
|
3
|
+
import type { SandboxSession } from "#shared/sandbox-session.js";
|
|
4
|
+
/** Options for cloning a GitHub repository ref into the active sandbox. */
|
|
5
|
+
export interface GitHubCheckoutOptions {
|
|
6
|
+
readonly depth?: number;
|
|
7
|
+
readonly includeBase?: boolean;
|
|
8
|
+
readonly mode?: "full" | "shallow";
|
|
9
|
+
readonly path?: string;
|
|
10
|
+
readonly ref?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Result returned after a GitHub checkout completes. */
|
|
13
|
+
export interface GitHubCheckout {
|
|
14
|
+
readonly baseRef: string | null;
|
|
15
|
+
readonly path: string;
|
|
16
|
+
readonly ref: string;
|
|
17
|
+
readonly sha: string;
|
|
18
|
+
}
|
|
19
|
+
/** Internal descriptor used by channel-owned checkout paths. */
|
|
20
|
+
export interface GitHubCheckoutInput extends GitHubCheckoutOptions {
|
|
21
|
+
readonly api?: GitHubApiOptions;
|
|
22
|
+
readonly baseRef?: string | null;
|
|
23
|
+
readonly baseSha?: string | null;
|
|
24
|
+
readonly credentials?: GitHubChannelCredentials;
|
|
25
|
+
readonly defaultBranch?: string | null;
|
|
26
|
+
readonly headRef?: string | null;
|
|
27
|
+
readonly headSha?: string | null;
|
|
28
|
+
readonly installationId?: number | null;
|
|
29
|
+
readonly owner?: string;
|
|
30
|
+
readonly pullRequestNumber?: number | null;
|
|
31
|
+
readonly repo?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Clones a described GitHub repository ref into the given sandbox.
|
|
35
|
+
*
|
|
36
|
+
* Runs on every turn via the channel's `turn.started` handler, which resolves
|
|
37
|
+
* the session sandbox from its `ctx` and passes it in. The sandbox persists for
|
|
38
|
+
* the session, so when the workspace is already at the target commit this is a
|
|
39
|
+
* no-op probe — no token is minted and nothing is fetched.
|
|
40
|
+
*
|
|
41
|
+
* The installation token is brokered at the sandbox firewall
|
|
42
|
+
* (`sandbox.setNetworkPolicy`) rather than embedded in the remote URL, so it
|
|
43
|
+
* never enters the sandbox process. Requires a firewall-capable backend; the
|
|
44
|
+
* local backend rejects `setNetworkPolicy`.
|
|
45
|
+
*
|
|
46
|
+
* Channel-internal; not part of the public GitHub channel API.
|
|
47
|
+
*/
|
|
48
|
+
export declare function checkoutGitHubRepository(sandbox: SandboxSession, input: GitHubCheckoutInput): Promise<GitHubCheckout>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{resolveGitHubInstallationToken}from"#public/channels/github/auth.js";import{getGitHubPullRequest,getGitHubRepository}from"#public/channels/github/api.js";async function checkoutGitHubRepository(t,n){let r=await resolveCheckoutDescriptor(n),i=t.resolvePath(n.path??`/workspace`),a=resolveCheckoutRef(n.ref,r);if(isFullSha(a)){let e=await readCheckoutHead(t,i);if(e===a)return{baseRef:r.baseRef,path:i,ref:a,sha:e}}let o=normalizeCheckoutDepth(n.depth),s=n.mode===`full`?``:` --depth ${o}`,c=await resolveGitHubInstallationToken({api:n.api,credentials:n.credentials,installationId:r.installationId}),l=publicRemoteUrl({owner:r.owner,repo:r.repo}),u=isFullSha(a)?a:`FETCH_HEAD`;await t.setNetworkPolicy(buildBrokerNetworkPolicy(c)),await runCheckoutCommand({command:`mkdir -p ${shellQuote(i)}`,label:`create checkout directory`,sandbox:t}),await runCheckoutCommand({command:`cd ${shellQuote(i)} && git init`,label:`initialize git repository`,sandbox:t}),await runCheckoutCommand({command:`cd ${shellQuote(i)} && git remote remove origin >/dev/null 2>&1 || true`,label:`reset git remote`,sandbox:t}),await runCheckoutCommand({command:`cd ${shellQuote(i)} && git remote add origin ${shellQuote(l)}`,label:`configure git remote`,sandbox:t}),await runCheckoutCommand({command:`cd ${shellQuote(i)} && GIT_TERMINAL_PROMPT=0 git fetch${s} origin ${shellQuote(a)}`,label:`fetch GitHub ref`,sandbox:t}),await runCheckoutCommand({command:`cd ${shellQuote(i)} && git checkout --detach ${shellQuote(u)}`,label:`checkout GitHub ref`,sandbox:t}),n.includeBase===!0&&r.baseSha!==null&&await runCheckoutCommand({command:`cd ${shellQuote(i)} && GIT_TERMINAL_PROMPT=0 git fetch${s} origin ${shellQuote(r.baseSha)}`,label:`fetch GitHub base ref`,sandbox:t});let d=(await runCheckoutCommand({command:`cd ${shellQuote(i)} && git rev-parse HEAD`,label:`resolve checked out commit`,sandbox:t})).stdout.trim()||r.headSha||a;return{baseRef:r.baseRef,path:i,ref:a,sha:d}}async function resolveCheckoutDescriptor(e){let n=readNonEmptyString(e.owner),r=readNonEmptyString(e.repo);if(n===void 0||r===void 0)throw Error(`GitHub checkout requires a repository owner and name.`);if(e.installationId===void 0||e.installationId===null)throw Error(`GitHub checkout requires a GitHub App installation id.`);let i=e.pullRequestNumber??null,a=readNonEmptyString(e.ref),o=null;i!==null&&(a===void 0&&((e.headSha===void 0||e.headSha===null)&&(e.headRef===void 0||e.headRef===null)||e.defaultBranch===void 0||e.defaultBranch===null)||e.includeBase===!0&&(e.baseSha===void 0||e.baseSha===null)||e.includeBase===!0&&(e.baseRef===void 0||e.baseRef===null))&&(o=await getGitHubPullRequest({api:e.api,credentials:e.credentials,installationId:e.installationId,owner:n,pullRequestNumber:i,repo:r}));let s=readNonEmptyString(e.defaultBranch)??o?.defaultBranch??await resolveRepositoryDefaultBranch(e,{owner:n,pullRequestNumber:i,repo:r});return{baseRef:e.baseRef??o?.base.ref??null,baseSha:e.baseSha??o?.base.sha??null,defaultBranch:s,headRef:e.headRef??o?.head.ref??null,headSha:e.headSha??o?.head.sha??null,installationId:e.installationId,owner:n,pullRequestNumber:i,repo:r}}async function resolveRepositoryDefaultBranch(e,t){return t.pullRequestNumber!==null||readNonEmptyString(e.ref)!==void 0||readNonEmptyString(e.headSha)!==void 0||readNonEmptyString(e.headRef)!==void 0?null:(await getGitHubRepository({api:e.api,credentials:e.credentials,installationId:e.installationId??void 0,owner:t.owner,repo:t.repo})).defaultBranch??null}function resolveCheckoutRef(e,t){if(e!==void 0&&e.trim().length>0)return e.trim();if(t.headSha!==null)return t.headSha;if(t.pullRequestNumber!==null)return`refs/pull/${t.pullRequestNumber}/head`;if(t.headRef!==null)return t.headRef;if(t.defaultBranch!==null)return t.defaultBranch;throw Error(`GitHub checkout could not resolve a ref to fetch.`)}async function readCheckoutHead(e,t){let n=await e.run({command:`cd ${shellQuote(t)} && git rev-parse HEAD 2>/dev/null`});if(n.exitCode!==0)return null;let r=String(n.stdout??``).trim();return isFullSha(r)?r:null}async function runCheckoutCommand(e){let t=await e.sandbox.run({command:e.command}),n=String(t.stderr??``),r=String(t.stdout??``);if(t.exitCode===0)return{stderr:n,stdout:r};throw Error([`GitHub checkout failed during ${e.label} (exit ${t.exitCode}).`,n?`stderr: ${n}`:void 0,r?`stdout: ${r}`:void 0,`Verify the GitHub App installation has access to this repository.`].filter(e=>e!==void 0).join(` `))}function normalizeCheckoutDepth(e){if(e===void 0)return 1;if(!Number.isFinite(e)||e<1)throw Error(`GitHub checkout depth must be a positive number.`);return Math.floor(e)}function publicRemoteUrl(e){return`https://github.com/${e.owner}/${e.repo}.git`}function buildBrokerNetworkPolicy(e){let t=[{transform:[{headers:{Authorization:`Basic ${Buffer.from(`x-access-token:${e}`).toString(`base64`)}`}}]}];return{allow:{"github.com":t,"codeload.github.com":t,"*":[]}}}function readNonEmptyString(e){return typeof e==`string`&&e.trim().length>0?e.trim():void 0}function isFullSha(e){return/^[a-f0-9]{40}$/iu.test(e)}function shellQuote(e){return`'${e.replace(/'/gu,`'\\''`)}'`}export{checkoutGitHubRepository};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const GITHUB_CHANNEL_DEFAULT_ROUTE=`/ash/v1/github`;export{GITHUB_CHANNEL_DEFAULT_ROUTE};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SessionAuthContext } from "#channel/types.js";
|
|
2
|
+
import type { GitHubApiOptions } from "#public/channels/github/api.js";
|
|
3
|
+
import type { GitHubChannelCredentials } from "#public/channels/github/auth.js";
|
|
4
|
+
import { type GitHubComment } from "#public/channels/github/inbound.js";
|
|
5
|
+
import type { GitHubChannelEvents, GitHubInboundContext, GitHubInboundResult, GitHubProgressConfig } from "#public/channels/github/githubChannel.js";
|
|
6
|
+
/** Projects a GitHub webhook actor into Ash session auth. */
|
|
7
|
+
export declare function defaultGitHubAuth(ctx: GitHubInboundContext): SessionAuthContext;
|
|
8
|
+
/** Options used by the built-in GitHub comment dispatch hook. */
|
|
9
|
+
export interface GitHubDefaultDispatchOptions {
|
|
10
|
+
readonly botName?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Default comment hook: dispatch only when the comment `@mention`s the bot. */
|
|
13
|
+
export declare function defaultOnComment(ctx: GitHubInboundContext, comment: GitHubComment, options: GitHubDefaultDispatchOptions): GitHubInboundResult;
|
|
14
|
+
/** Options used by built-in GitHub event handlers. */
|
|
15
|
+
export interface GitHubDefaultEventOptions {
|
|
16
|
+
readonly api?: GitHubApiOptions;
|
|
17
|
+
readonly credentials?: GitHubChannelCredentials;
|
|
18
|
+
readonly progress?: GitHubProgressConfig;
|
|
19
|
+
}
|
|
20
|
+
/** Builds GitHub's built-in event handlers for acknowledgement and terminal output. */
|
|
21
|
+
export declare function createDefaultEvents(options?: GitHubDefaultEventOptions): GitHubChannelEvents;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{createLogger,extractErrorId,formatErrorHint,logError}from"#internal/logging.js";import{checkoutGitHubRepository}from"#public/channels/github/checkout.js";import{shouldDispatchGitHubComment}from"#public/channels/github/inbound.js";import{splitGitHubCommentBody}from"#public/channels/github/limits.js";const log=createLogger(`github.defaults`);function defaultGitHubAuth(e){let{sender:t}=e;return{attributes:{conversation_kind:e.conversation.kind,delivery_id:e.delivery.id,installation_id:String(e.github.installationId??``),issue_number:String(e.conversation.issueNumber??``),pull_request_number:String(e.conversation.pullRequestNumber??``),repository:e.repository.fullName,repository_id:String(e.repository.id),user_login:t.login,user_type:t.type},authenticator:`github-webhook`,issuer:`github:${e.repository.owner}`,principalId:`github:${t.id}`,principalType:t.type===`Bot`?`service`:`user`,subject:t.login}}function defaultOnComment(e,t,n){return shouldDispatchGitHubComment({author:t.author,body:t.body,botName:n.botName})?{auth:defaultGitHubAuth(e)}:null}function createDefaultEvents(e={}){return{async"turn.started"(t,n,i){if(e.progress?.reactions!==!1)try{await n.thread.react(`eyes`)}catch(e){logError(log,`GitHub reaction failed — swallowed`,e)}await checkoutRepositoryForTurn(n,i,e)},async"message.completed"(e,t,n){e.finishReason===`tool-calls`||!e.message||await postCommentChunks(t,e.message)},async"session.failed"(e,r){let i=formatErrorHint(e),a=extractErrorId(e.details);await postFailure(r,[`This session could not recover from an error${i}.`,``,`Start a new comment to continue.`,...a?[``,`Error id: ${a}`]:[]].join(`
|
|
2
|
+
`))},async"turn.failed"(e,r,i){let a=formatErrorHint(e),o=extractErrorId(e.details);await postFailure(r,[`I hit an error while handling your request${a}.`,``,`Please try again, rephrase, or reach out if it keeps failing.`,...o?[``,`Error id: ${o}`]:[]].join(`
|
|
3
|
+
`))}}}async function checkoutRepositoryForTurn(e,t,n){let{state:a}=e;try{let e=await checkoutGitHubRepository(await t.getSandbox(),{api:n.api,baseRef:a.baseRef,baseSha:a.baseSha,credentials:n.credentials,defaultBranch:a.defaultBranch,headRef:a.headRef,headSha:a.headSha,includeBase:a.pullRequestNumber!==null,installationId:a.installationId,owner:a.owner,pullRequestNumber:a.pullRequestNumber,repo:a.repo});a.checkoutPath=e.path,a.headSha=e.sha,a.baseRef=e.baseRef}catch(e){logError(log,`GitHub checkout failed — swallowed`,e)}}async function postCommentChunks(e,t){for(let n of splitGitHubCommentBody(t))await e.thread.post(n)}async function postFailure(e,t){await postCommentChunks(e,t)}export{createDefaultEvents,defaultGitHubAuth,defaultOnComment};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type GitHubIssueCommentEvent, type GitHubIssueWebhookEvent, type GitHubPullRequestReviewCommentEvent, type GitHubPullRequestWebhookEvent } from "#public/channels/github/inbound.js";
|
|
2
|
+
import { type GitHubChannelState } from "#public/channels/github/state.js";
|
|
3
|
+
import type { GitHubChannelConfig } from "#public/channels/github/githubChannel.js";
|
|
4
|
+
import type { SendFn } from "#public/definitions/defineChannel.js";
|
|
5
|
+
/** Dispatches a bot-directed issue or PR timeline comment into the runtime. */
|
|
6
|
+
export declare function dispatchIssueComment(input: {
|
|
7
|
+
readonly botName: string | undefined;
|
|
8
|
+
readonly config: GitHubChannelConfig;
|
|
9
|
+
readonly event: GitHubIssueCommentEvent;
|
|
10
|
+
readonly handler: NonNullable<GitHubChannelConfig["onComment"]>;
|
|
11
|
+
readonly send: SendFn<GitHubChannelState>;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
/** Dispatches a bot-directed inline pull-request review comment. */
|
|
14
|
+
export declare function dispatchPullRequestReviewComment(input: {
|
|
15
|
+
readonly botName: string | undefined;
|
|
16
|
+
readonly config: GitHubChannelConfig;
|
|
17
|
+
readonly event: GitHubPullRequestReviewCommentEvent;
|
|
18
|
+
readonly handler: NonNullable<GitHubChannelConfig["onComment"]>;
|
|
19
|
+
readonly send: SendFn<GitHubChannelState>;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
/** Dispatches an opt-in issue webhook event into the runtime. */
|
|
22
|
+
export declare function dispatchIssue(input: {
|
|
23
|
+
readonly config: GitHubChannelConfig;
|
|
24
|
+
readonly event: GitHubIssueWebhookEvent;
|
|
25
|
+
readonly handler: NonNullable<GitHubChannelConfig["onIssue"]>;
|
|
26
|
+
readonly send: SendFn<GitHubChannelState>;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
/** Dispatches an opt-in pull-request webhook event into the runtime. */
|
|
29
|
+
export declare function dispatchPullRequest(input: {
|
|
30
|
+
readonly config: GitHubChannelConfig;
|
|
31
|
+
readonly event: GitHubPullRequestWebhookEvent;
|
|
32
|
+
readonly handler: NonNullable<GitHubChannelConfig["onPullRequest"]>;
|
|
33
|
+
readonly send: SendFn<GitHubChannelState>;
|
|
34
|
+
}): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createLogger,logError}from"#internal/logging.js";import{extractGitHubCommentTrigger,formatGitHubContextBlock,prependGitHubContext}from"#public/channels/github/inbound.js";import{buildGitHubBinding}from"#public/channels/github/binding.js";import{buildGitHubPullRequestContext,mergeGitHubContext}from"#public/channels/github/pr-context.js";import{continuationTokenFromState,stateFromIssueCommentEvent,stateFromIssueEvent,stateFromPullRequestEvent,stateFromPullRequestReviewCommentEvent}from"#public/channels/github/state.js";const log=createLogger(`github.dispatch`);async function dispatchIssueComment(e){if(isIgnoredInboundComment(e.event.comment.body,e.event.comment.author,e.botName))return;let t=buildInboundContext(e.config,e.event);await dispatchCommentTurn({body:e.event.comment.body,botName:e.botName,commentUrl:e.event.comment.htmlUrl,event:e.event,handlerResult:()=>e.handler(t,toGitHubComment(e.event.comment)),config:e.config,send:e.send,state:stateFromIssueCommentEvent(e.event)})}async function dispatchPullRequestReviewComment(e){if(isIgnoredInboundComment(e.event.comment.body,e.event.comment.author,e.botName))return;let t=buildInboundContext(e.config,e.event);await dispatchCommentTurn({body:e.event.comment.body,botName:e.botName,commentUrl:e.event.comment.htmlUrl,event:e.event,handlerResult:()=>e.handler(t,toGitHubComment(e.event.comment)),config:e.config,send:e.send,state:stateFromPullRequestReviewCommentEvent(e.event)})}async function dispatchIssue(e){let t=buildInboundContext(e.config,e.event);await dispatchWebhookEventTurn({config:e.config,event:e.event,handlerResult:()=>e.handler(t,e.event.issue),message:formatIssueEventMessage(e.event),send:e.send,state:stateFromIssueEvent(e.event)})}async function dispatchPullRequest(e){let t=buildInboundContext(e.config,e.event);await dispatchWebhookEventTurn({config:e.config,event:e.event,handlerResult:()=>e.handler(t,e.event.pullRequest),message:formatPullRequestEventMessage(e.event),send:e.send,state:stateFromPullRequestEvent(e.event)})}async function dispatchWebhookEventTurn(e){let t=await runInboundHandler({event:e.event,handlerResult:e.handlerResult});t!=null&&await sendGitHubTurn({auth:t.auth,event:e.event,message:e.message,context:mergeGitHubContext({github:await buildPullRequestContext(e.config,e.state,e.event.delivery.id),hook:t.context}),send:e.send,state:e.state})}async function dispatchCommentTurn(e){let t=await runInboundHandler({event:e.event,handlerResult:e.handlerResult});if(t==null)return;let r=extractGitHubCommentTrigger({body:e.body,botName:e.botName})?.message??e.body.trim();await sendGitHubTurn({auth:t.auth,commentUrl:e.commentUrl,event:e.event,message:r,context:mergeGitHubContext({github:await buildPullRequestContext(e.config,e.state,e.event.delivery.id),hook:t.context}),send:e.send,state:e.state})}async function runInboundHandler(e){try{return await e.handlerResult()}catch(n){logError(log,`GitHub inbound handler failed`,n,{deliveryId:e.event.delivery.id});return}}async function sendGitHubTurn(e){let n=formatGitHubContextBlock({deliveryId:e.event.delivery.id,commentUrl:e.commentUrl,headSha:e.state.headSha,issueNumber:e.state.issueNumber,pullRequestNumber:e.state.pullRequestNumber,repository:e.event.repository,sender:e.event.sender}),i=prependGitHubContext(e.message,n);try{await e.send({message:i,context:e.context},{auth:e.auth,continuationToken:continuationTokenFromState(e.state),state:e.state})}catch(n){logError(log,e.logMessage??`GitHub delivery failed`,n,{deliveryId:e.event.delivery.id})}}async function buildPullRequestContext(e,n,r){try{return await buildGitHubPullRequestContext({api:e.api,config:e.pullRequestContext,credentials:e.credentials,installationId:n.installationId??void 0,owner:n.owner,pullRequestNumber:n.pullRequestNumber,repo:n.repo})}catch(e){logError(log,`GitHub pull-request context failed — swallowed`,e,{deliveryId:r});return}}function buildInboundContext(e,t){let n=buildGitHubBinding({config:e,state:t.kind===`issue_comment`?stateFromIssueCommentEvent(t):t.kind===`issues`?stateFromIssueEvent(t):t.kind===`pull_request`?stateFromPullRequestEvent(t):stateFromPullRequestReviewCommentEvent(t)});return{conversation:t.conversation,delivery:t.delivery,github:n.github,repository:t.repository,sender:t.sender,thread:n.thread}}function toGitHubComment(e){return{author:e.author,body:e.body,htmlUrl:e.htmlUrl,id:e.id,raw:e.raw,url:e.url}}function formatIssueEventMessage(e){let t=readString(e.issue.raw.title);return`Issue ${e.issue.action}: #${e.issue.issueNumber}${t?` ${t}`:``}`}function formatPullRequestEventMessage(e){let t=readString(e.pullRequest.raw.title);return`Pull request ${e.pullRequest.action}: #${e.pullRequest.pullRequestNumber}${t?` ${t}`:``}`}function isIgnoredInboundComment(e,t,n){if(e.includes(`<!-- ash:github:`)||t?.type===`Bot`)return!0;let r=n?`${n}[bot]`.toLowerCase():``;return r.length>0&&t?.login.toLowerCase()===r}function readString(e){return typeof e==`string`&&e.trim().length>0?e.trim():void 0}export{dispatchIssue,dispatchIssueComment,dispatchPullRequest,dispatchPullRequestReviewComment};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { SessionAuthContext } from "#channel/types.js";
|
|
2
|
+
import type { SessionContext } from "#public/definitions/callback-context.js";
|
|
3
|
+
import type { ChannelSessionOps } from "#public/definitions/defineChannel.js";
|
|
4
|
+
import type { HandleMessageStreamEvent } from "#protocol/message.js";
|
|
5
|
+
import { type GitHubHandle, type GitHubThread } from "#public/channels/github/binding.js";
|
|
6
|
+
import { type GitHubApiOptions } from "#public/channels/github/api.js";
|
|
7
|
+
import type { GitHubChannelCredentials } from "#public/channels/github/auth.js";
|
|
8
|
+
import { type GitHubComment, type GitHubConversationRef, type GitHubDelivery, type GitHubIssueEvent, type GitHubPullRequestEvent, type GitHubRepositoryRef, type GitHubUser } from "#public/channels/github/inbound.js";
|
|
9
|
+
import { type GitHubChannelState } from "#public/channels/github/state.js";
|
|
10
|
+
import type { GitHubPullRequestContextConfig } from "#public/channels/github/pr-context.js";
|
|
11
|
+
import { type Channel } from "#public/definitions/defineChannel.js";
|
|
12
|
+
type EventData<T extends HandleMessageStreamEvent["type"]> = Extract<HandleMessageStreamEvent, {
|
|
13
|
+
type: T;
|
|
14
|
+
}> extends {
|
|
15
|
+
data: infer D;
|
|
16
|
+
} ? D : undefined;
|
|
17
|
+
/** Target accepted by `receive(github, { target })` for proactive sessions. */
|
|
18
|
+
export interface GitHubReceiveTarget {
|
|
19
|
+
readonly initialMessage?: string;
|
|
20
|
+
readonly installationId?: number;
|
|
21
|
+
readonly issueNumber?: number;
|
|
22
|
+
readonly owner: string;
|
|
23
|
+
readonly pullRequestNumber?: number;
|
|
24
|
+
/** Optional shortcut that avoids a repository metadata API call. */
|
|
25
|
+
readonly repositoryId?: number;
|
|
26
|
+
readonly repo: string;
|
|
27
|
+
}
|
|
28
|
+
/** Optional acknowledgement progress surfaces for GitHub conversations. */
|
|
29
|
+
export interface GitHubProgressConfig {
|
|
30
|
+
readonly reactions?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Pre-dispatch GitHub context passed to inbound hooks. */
|
|
33
|
+
export interface GitHubInboundContext {
|
|
34
|
+
readonly conversation: GitHubConversationRef;
|
|
35
|
+
readonly delivery: GitHubDelivery;
|
|
36
|
+
readonly github: GitHubHandle;
|
|
37
|
+
readonly repository: GitHubRepositoryRef;
|
|
38
|
+
readonly sender: GitHubUser;
|
|
39
|
+
readonly thread: GitHubThread;
|
|
40
|
+
}
|
|
41
|
+
/** Channel-owned GitHub context rebuilt from persisted channel state. */
|
|
42
|
+
export interface GitHubChannelContext {
|
|
43
|
+
readonly conversation: GitHubConversationRef;
|
|
44
|
+
readonly github: GitHubHandle;
|
|
45
|
+
readonly repository: GitHubRepositoryRef;
|
|
46
|
+
readonly thread: GitHubThread;
|
|
47
|
+
state: GitHubChannelState;
|
|
48
|
+
}
|
|
49
|
+
/** Event-handler GitHub context, including session operations. */
|
|
50
|
+
export interface GitHubEventContext extends GitHubChannelContext, ChannelSessionOps {
|
|
51
|
+
}
|
|
52
|
+
/** Result of a GitHub inbound hook. Return `null` to acknowledge without dispatching. */
|
|
53
|
+
export type GitHubInboundResult = {
|
|
54
|
+
readonly auth: SessionAuthContext | null;
|
|
55
|
+
readonly context?: readonly string[];
|
|
56
|
+
} | null;
|
|
57
|
+
/** Sync or async {@link GitHubInboundResult}. */
|
|
58
|
+
export type GitHubInboundResultOrPromise = GitHubInboundResult | Promise<GitHubInboundResult>;
|
|
59
|
+
type GitHubEventHandler<T extends HandleMessageStreamEvent["type"]> = (data: EventData<T>, channel: GitHubEventContext, ctx: SessionContext) => void | Promise<void>;
|
|
60
|
+
type GitHubSessionFailedHandler = (data: EventData<"session.failed">, channel: GitHubEventContext) => void | Promise<void>;
|
|
61
|
+
/** Event handlers supported by `githubChannel({ events })`. */
|
|
62
|
+
export interface GitHubChannelEvents {
|
|
63
|
+
readonly "action.result"?: GitHubEventHandler<"action.result">;
|
|
64
|
+
readonly "actions.requested"?: GitHubEventHandler<"actions.requested">;
|
|
65
|
+
readonly "authorization.completed"?: GitHubEventHandler<"authorization.completed">;
|
|
66
|
+
readonly "authorization.required"?: GitHubEventHandler<"authorization.required">;
|
|
67
|
+
readonly "input.requested"?: GitHubEventHandler<"input.requested">;
|
|
68
|
+
readonly "message.appended"?: GitHubEventHandler<"message.appended">;
|
|
69
|
+
readonly "message.completed"?: GitHubEventHandler<"message.completed">;
|
|
70
|
+
readonly "session.completed"?: GitHubEventHandler<"session.completed">;
|
|
71
|
+
readonly "session.failed"?: GitHubSessionFailedHandler;
|
|
72
|
+
readonly "session.waiting"?: GitHubEventHandler<"session.waiting">;
|
|
73
|
+
readonly "turn.completed"?: GitHubEventHandler<"turn.completed">;
|
|
74
|
+
readonly "turn.failed"?: GitHubEventHandler<"turn.failed">;
|
|
75
|
+
readonly "turn.started"?: GitHubEventHandler<"turn.started">;
|
|
76
|
+
}
|
|
77
|
+
/** Configuration for {@link githubChannel}. */
|
|
78
|
+
export interface GitHubChannelConfig {
|
|
79
|
+
readonly api?: GitHubApiOptions;
|
|
80
|
+
readonly botName?: string;
|
|
81
|
+
readonly credentials?: GitHubChannelCredentials;
|
|
82
|
+
readonly events?: GitHubChannelEvents;
|
|
83
|
+
readonly progress?: GitHubProgressConfig;
|
|
84
|
+
readonly pullRequestContext?: GitHubPullRequestContextConfig;
|
|
85
|
+
readonly route?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Invoked for every `@mention` of the bot in an issue/PR timeline comment or
|
|
88
|
+
* an inline review comment. `ctx.conversation.kind` distinguishes the surface.
|
|
89
|
+
* Return `{ auth }` to dispatch or `null` to ignore. Replacing this fully
|
|
90
|
+
* replaces the default mention gate.
|
|
91
|
+
*/
|
|
92
|
+
onComment?(ctx: GitHubInboundContext, comment: GitHubComment): GitHubInboundResultOrPromise;
|
|
93
|
+
/**
|
|
94
|
+
* Opt-in handler for `issues` webhook events. There is no default dispatch —
|
|
95
|
+
* define this to act on issues (e.g. `issue.action === "opened"`).
|
|
96
|
+
*/
|
|
97
|
+
onIssue?(ctx: GitHubInboundContext, issue: GitHubIssueEvent): GitHubInboundResultOrPromise;
|
|
98
|
+
/**
|
|
99
|
+
* Opt-in handler for `pull_request` webhook events. There is no default
|
|
100
|
+
* dispatch — define this to act on PRs (e.g. `pullRequest.action === "opened"`).
|
|
101
|
+
*/
|
|
102
|
+
onPullRequest?(ctx: GitHubInboundContext, pullRequest: GitHubPullRequestEvent): GitHubInboundResultOrPromise;
|
|
103
|
+
}
|
|
104
|
+
/** Concrete return type of {@link githubChannel}. */
|
|
105
|
+
export interface GitHubChannel extends Channel<GitHubChannelState, GitHubReceiveTarget> {
|
|
106
|
+
}
|
|
107
|
+
/** GitHub channel factory for GitHub App webhooks and proactive comments. */
|
|
108
|
+
export declare function githubChannel(config?: GitHubChannelConfig): GitHubChannel;
|
|
109
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createLogger}from"#internal/logging.js";import{POST,defineChannel}from"#public/definitions/defineChannel.js";import{getGitHubRepository}from"#public/channels/github/api.js";import{parseGitHubWebhookEvent}from"#public/channels/github/inbound.js";import{buildGitHubBinding}from"#public/channels/github/binding.js";import{continuationTokenFromState,conversationFromState,initialGitHubState,stateFromReceiveTarget}from"#public/channels/github/state.js";import{GITHUB_CHANNEL_DEFAULT_ROUTE}from"#public/channels/github/constants.js";import{createDefaultEvents,defaultOnComment}from"#public/channels/github/defaults.js";import{dispatchIssue,dispatchIssueComment,dispatchPullRequest,dispatchPullRequestReviewComment}from"#public/channels/github/dispatch.js";import{verifyGitHubRequest}from"#public/channels/github/verify.js";const log=createLogger(`github.channel`);function githubChannel(e={}){let s=e.botName??process.env.GITHUB_APP_SLUG,u={botName:s},d={...createDefaultEvents({api:e.api,credentials:e.credentials,progress:e.progress}),...e.events};return defineChannel({kindHint:`github`,state:initialGitHubState(),context(t,n){return rebuildGitHubContext(t,n,e)},routes:[POST(e.route??GITHUB_CHANNEL_DEFAULT_ROUTE,async(t,{send:n,waitUntil:r})=>{let a=await verifyInbound(t,e.credentials);if(a===null)return new Response(`unauthorized`,{status:401});let o;try{o=parseGitHubWebhookEvent({body:a,contentType:t.headers.get(`content-type`)??void 0,headers:t.headers})}catch(e){return log.warn(`inbound GitHub body is not valid JSON`,{error:e}),jsonOk({ignored:!0,ok:!0})}return o===null?jsonOk({ignored:!0,ok:!0}):o.kind===`ping`?jsonOk({ok:!0}):o.kind===`issue_comment`&&o.action===`created`?(r(dispatchIssueComment({botName:s,config:e,event:o,handler:e.onComment??((e,t)=>defaultOnComment(e,t,u)),send:n})),jsonOk({ok:!0})):o.kind===`pull_request_review_comment`&&o.action===`created`?(r(dispatchPullRequestReviewComment({botName:s,config:e,event:o,handler:e.onComment??((e,t)=>defaultOnComment(e,t,u)),send:n})),jsonOk({ok:!0})):o.kind===`issues`&&e.onIssue!==void 0?(r(dispatchIssue({config:e,event:o,handler:e.onIssue,send:n})),jsonOk({ok:!0})):o.kind===`pull_request`&&e.onPullRequest!==void 0?(r(dispatchPullRequest({config:e,event:o,handler:e.onPullRequest,send:n})),jsonOk({ok:!0})):jsonOk({ignored:!0,ok:!0})})],async receive(t,{send:n}){let i=t.target,s=readNonEmptyString(i.owner),c=readNonEmptyString(i.repo);if(s===void 0||c===void 0)throw Error(`githubChannel().receive requires target.owner and target.repo.`);if([i.issueNumber!==void 0,i.pullRequestNumber!==void 0].filter(Boolean).length!==1)throw Error(`githubChannel().receive requires exactly one of issueNumber or pullRequestNumber.`);let l=stateFromReceiveTarget({target:i,owner:s,repo:c,repositoryId:i.repositoryId??(await getGitHubRepository({api:e.api,credentials:e.credentials,installationId:i.installationId,owner:s,repo:c})).id});if(i.initialMessage!==void 0){let{thread:t}=buildGitHubBinding({config:e,state:l});await t.post(i.initialMessage)}return n(t.message,{auth:t.auth,continuationToken:continuationTokenFromState(l),state:l})},events:d})}function rebuildGitHubContext(e,t,n){let r=buildGitHubBinding({config:n,state:e});return{conversation:conversationFromState(e),github:r.github,repository:r.github.repository,state:e,thread:r.thread}}async function verifyInbound(e,t){try{return await verifyGitHubRequest(e,{webhookSecret:t?.webhookSecret,webhookVerifier:t?.webhookVerifier})}catch(e){return log.warn(`github inbound verification failed`,{error:e}),null}}function readNonEmptyString(e){return typeof e==`string`&&e.length>0?e:void 0}function jsonOk(e){return new Response(JSON.stringify(e),{headers:{"content-type":`application/json; charset=utf-8`},status:200})}export{githubChannel};
|