@shipfox/api-integration-github 12.3.0 → 12.6.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +19 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +15 -9
- package/dist/api/client.js.map +1 -1
- package/dist/api/github-octokit.d.ts +7 -0
- package/dist/api/github-octokit.d.ts.map +1 -0
- package/dist/api/github-octokit.js +32 -0
- package/dist/api/github-octokit.js.map +1 -0
- package/dist/api/installation-token-envelope.d.ts +5 -1
- package/dist/api/installation-token-envelope.d.ts.map +1 -1
- package/dist/api/installation-token-envelope.js +16 -5
- package/dist/api/installation-token-envelope.js.map +1 -1
- package/dist/api/installation-token-provider.d.ts.map +1 -1
- package/dist/api/installation-token-provider.js +4 -2
- package/dist/api/installation-token-provider.js.map +1 -1
- package/dist/api/shared-installation-token-cache.d.ts.map +1 -1
- package/dist/api/shared-installation-token-cache.js +10 -4
- package/dist/api/shared-installation-token-cache.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-tools.d.ts +11 -4
- package/dist/core/agent-tools.d.ts.map +1 -1
- package/dist/core/agent-tools.js +235 -24
- package/dist/core/agent-tools.js.map +1 -1
- package/dist/core/github-agent-tool-catalog.js +1 -1
- package/dist/core/github-agent-tool-catalog.js.map +1 -1
- package/dist/metrics/instance.d.ts +1 -0
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +19 -0
- package/dist/metrics/instance.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/api/client.test.ts +66 -10
- package/src/api/client.ts +44 -7
- package/src/api/github-octokit.test.ts +115 -0
- package/src/api/github-octokit.ts +49 -0
- package/src/api/installation-token-envelope.ts +24 -2
- package/src/api/installation-token-provider.test.ts +44 -5
- package/src/api/installation-token-provider.ts +5 -2
- package/src/api/shared-installation-token-cache.test.ts +28 -0
- package/src/api/shared-installation-token-cache.ts +7 -0
- package/src/config.ts +5 -0
- package/src/core/agent-tools.test.ts +1049 -8
- package/src/core/agent-tools.ts +388 -25
- package/src/core/github-agent-tool-catalog.ts +1 -1
- package/src/metrics/instance.ts +25 -0
- package/test/env.ts +1 -0
- package/test/fixtures/github-installation-token.ts +8 -0
- package/test/index.ts +4 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -55,7 +55,7 @@ export class SharedInstallationTokenCache {
|
|
|
55
55
|
return tokenFromEnvelope(envelope);
|
|
56
56
|
}
|
|
57
57
|
recordInstallationTokenLookup('backoff');
|
|
58
|
-
throw providerErrorFromBackoff(envelope?.backoffReason ?? 'provider-unavailable', (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime());
|
|
58
|
+
throw providerErrorFromBackoff(envelope?.backoffReason ?? 'provider-unavailable', (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime(), envelope?.backoffError);
|
|
59
59
|
}
|
|
60
60
|
let token;
|
|
61
61
|
try {
|
|
@@ -73,7 +73,13 @@ export class SharedInstallationTokenCache {
|
|
|
73
73
|
expiresAt: envelope?.expiresAt,
|
|
74
74
|
permissions: envelope?.permissions,
|
|
75
75
|
backoffUntil: until,
|
|
76
|
-
backoffReason: classified.reason
|
|
76
|
+
backoffReason: classified.reason,
|
|
77
|
+
backoffError: {
|
|
78
|
+
message: providerError.message,
|
|
79
|
+
...providerError.status === undefined ? {} : {
|
|
80
|
+
status: providerError.status
|
|
81
|
+
}
|
|
82
|
+
}
|
|
77
83
|
}).catch((writeError)=>{
|
|
78
84
|
logger().warn({
|
|
79
85
|
installationId: params.installationId,
|
|
@@ -142,7 +148,7 @@ export class SharedInstallationTokenCache {
|
|
|
142
148
|
}
|
|
143
149
|
if (activeBackoff(params.envelope, initialNow)) {
|
|
144
150
|
recordInstallationTokenLookup('backoff');
|
|
145
|
-
throw providerErrorFromBackoff(params.envelope.backoffReason, params.envelope.backoffUntil.getTime() - initialNow.getTime());
|
|
151
|
+
throw providerErrorFromBackoff(params.envelope.backoffReason, params.envelope.backoffUntil.getTime() - initialNow.getTime(), params.envelope.backoffError);
|
|
146
152
|
}
|
|
147
153
|
for (const delayMs of this.pollDelaysMs){
|
|
148
154
|
await this.sleep(delayMs);
|
|
@@ -154,7 +160,7 @@ export class SharedInstallationTokenCache {
|
|
|
154
160
|
}
|
|
155
161
|
if (backoffActive(envelope, now)) {
|
|
156
162
|
recordInstallationTokenLookup('backoff');
|
|
157
|
-
throw providerErrorFromBackoff(envelope?.backoffReason ?? 'provider-unavailable', (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime());
|
|
163
|
+
throw providerErrorFromBackoff(envelope?.backoffReason ?? 'provider-unavailable', (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime(), envelope?.backoffError);
|
|
158
164
|
}
|
|
159
165
|
}
|
|
160
166
|
throw new GithubIntegrationProviderError('provider-unavailable', 'GitHub installation token mint is still in progress', 1);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/api/shared-installation-token-cache.ts"],"sourcesContent":["import {setTimeout as sleepTimeout} from 'node:timers/promises';\nimport {reportError} from '@shipfox/node-error-monitoring';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {GithubIntegrationProviderError} from '#core/errors.js';\nimport {\n recordInstallationTokenBackoff,\n recordInstallationTokenLookup,\n recordInstallationTokenMint,\n} from '#metrics/index.js';\nimport type {GithubInstallationAccessToken} from './client.js';\nimport {\n backoffActive,\n backoffMs,\n classifyMintError,\n type InstallationTokenEnvelope,\n mintErrorClassForReason,\n parseInstallationTokenEnvelope,\n providerErrorFromBackoff,\n stillValid,\n toProviderError,\n usable,\n} from './installation-token-envelope.js';\n\nexport interface InstallationTokenCache {\n getOrMint(\n installationId: number,\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken>;\n}\n\nexport type InstallationTokenLockResult<T> = {acquired: true; value: T} | {acquired: false};\n\nexport interface InstallationTokenSecretStore {\n read(workspaceId: string, installationId: number): Promise<string | null>;\n write(\n workspaceId: string,\n installationId: number,\n envelope: InstallationTokenEnvelope,\n ): Promise<void>;\n}\n\nexport interface SharedInstallationTokenCacheOptions {\n secretStore: InstallationTokenSecretStore;\n withLock: <T>(\n installationId: number,\n fn: () => Promise<T>,\n ) => Promise<InstallationTokenLockResult<T>>;\n resolveWorkspaceId: (installationId: number) => Promise<string>;\n now?: (() => Date) | undefined;\n sleep?: ((ms: number) => Promise<void>) | undefined;\n pollDelaysMs?: number[] | undefined;\n workspaceCacheTtlMs?: number | undefined;\n mintTimeoutMs?: number | undefined;\n}\n\nconst DEFAULT_POLL_DELAYS_MS = [100, 200, 400, 500, 800];\nconst DEFAULT_WORKSPACE_CACHE_TTL_MS = 10 * 60 * 1000;\nconst DEFAULT_MINT_TIMEOUT_MS = 30 * 1000;\n\nexport class SharedInstallationTokenCache implements InstallationTokenCache {\n private readonly workspaceIds = new Map<number, {workspaceId: string; expiresAtMs: number}>();\n private readonly now: () => Date;\n private readonly sleep: (ms: number) => Promise<void>;\n private readonly pollDelaysMs: number[];\n private readonly workspaceCacheTtlMs: number;\n private readonly mintTimeoutMs: number;\n\n constructor(private readonly options: SharedInstallationTokenCacheOptions) {\n this.now = options.now ?? (() => new Date());\n this.sleep = options.sleep ?? ((ms) => sleepTimeout(ms).then(() => undefined));\n this.pollDelaysMs = options.pollDelaysMs ?? DEFAULT_POLL_DELAYS_MS;\n this.workspaceCacheTtlMs = options.workspaceCacheTtlMs ?? DEFAULT_WORKSPACE_CACHE_TTL_MS;\n this.mintTimeoutMs = options.mintTimeoutMs ?? DEFAULT_MINT_TIMEOUT_MS;\n }\n\n async getOrMint(\n installationId: number,\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken> {\n const workspaceId = await this.resolveWorkspaceId(installationId);\n const envelope = await this.readEnvelope(workspaceId, installationId);\n if (usable(envelope, this.now())) {\n recordInstallationTokenLookup('db-hit');\n return tokenFromEnvelope(envelope);\n }\n\n const result = await this.options.withLock(installationId, () =>\n this.mintUnderLock({workspaceId, installationId, mint}),\n );\n if (result.acquired) return result.value;\n\n return await this.serveStaleOrPoll({workspaceId, installationId, envelope});\n }\n\n private async mintUnderLock(params: {\n workspaceId: string;\n installationId: number;\n mint: () => Promise<GithubInstallationAccessToken>;\n }): Promise<GithubInstallationAccessToken> {\n const envelope = await this.readEnvelope(params.workspaceId, params.installationId);\n const now = this.now();\n if (usable(envelope, now)) {\n recordInstallationTokenLookup('db-hit');\n return tokenFromEnvelope(envelope);\n }\n\n if (activeBackoff(envelope, now)) {\n if (canServeStale(envelope, now)) {\n recordInstallationTokenLookup('served-stale');\n return tokenFromEnvelope(envelope);\n }\n recordInstallationTokenLookup('backoff');\n throw providerErrorFromBackoff(\n envelope?.backoffReason ?? 'provider-unavailable',\n (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime(),\n );\n }\n\n let token: GithubInstallationAccessToken;\n try {\n token = await this.recordMint(params.mint);\n } catch (error) {\n const providerError = toProviderError(error);\n const classified = classifyMintError(providerError);\n const until = new Date(this.now().getTime() + backoffMs(classified));\n recordInstallationTokenBackoff({reason: classified.reason, class: classified.class});\n\n await this.writeEnvelope(params.workspaceId, params.installationId, {\n token: envelope?.token,\n expiresAt: envelope?.expiresAt,\n permissions: envelope?.permissions,\n backoffUntil: until,\n backoffReason: classified.reason,\n }).catch((writeError) => {\n logger().warn(\n {installationId: params.installationId, reason: classified.reason, error: writeError},\n 'github installation token backoff write failed',\n );\n reportError(writeError, {\n boundary: 'integration.cache',\n operation: 'write-backoff-envelope',\n extra: {installationId: params.installationId},\n });\n });\n\n if (\n classified.class === 'transient' &&\n envelope?.token &&\n stillValid(envelope.expiresAt, this.now())\n ) {\n logger().warn(\n {\n installationId: params.installationId,\n expiresAt: envelope.expiresAt?.toISOString(),\n reason: classified.reason,\n backoffUntil: until.toISOString(),\n },\n 'github installation token mint failed; serving stale token',\n );\n recordInstallationTokenLookup('served-stale');\n return tokenFromEnvelope(envelope);\n }\n\n logger().warn(\n {\n installationId: params.installationId,\n reason: classified.reason,\n backoffUntil: until.toISOString(),\n error: providerError,\n },\n 'github installation token mint failed; backoff recorded',\n );\n recordInstallationTokenLookup('backoff');\n throw providerError;\n }\n\n try {\n await this.writeEnvelope(params.workspaceId, params.installationId, {\n token: token.token,\n expiresAt: token.expiresAt,\n permissions: token.permissions,\n });\n } catch (error) {\n logger().warn(\n {installationId: params.installationId, expiresAt: token.expiresAt.toISOString(), error},\n 'github installation token cache write failed after mint',\n );\n reportError(error, {\n boundary: 'integration.cache',\n operation: 'write-minted-token',\n extra: {installationId: params.installationId},\n });\n }\n\n logger().info(\n {installationId: params.installationId, expiresAt: token.expiresAt.toISOString()},\n 'github installation token minted',\n );\n recordInstallationTokenLookup('minted');\n return token;\n }\n\n private async serveStaleOrPoll(params: {\n workspaceId: string;\n installationId: number;\n envelope: InstallationTokenEnvelope | undefined;\n }): Promise<GithubInstallationAccessToken> {\n const initialNow = this.now();\n if (canServeStale(params.envelope, initialNow)) {\n recordInstallationTokenLookup('served-stale');\n return tokenFromEnvelope(params.envelope);\n }\n if (activeBackoff(params.envelope, initialNow)) {\n recordInstallationTokenLookup('backoff');\n throw providerErrorFromBackoff(\n params.envelope.backoffReason,\n params.envelope.backoffUntil.getTime() - initialNow.getTime(),\n );\n }\n\n for (const delayMs of this.pollDelaysMs) {\n await this.sleep(delayMs);\n const envelope = await this.readEnvelope(params.workspaceId, params.installationId);\n const now = this.now();\n if (usable(envelope, now)) {\n recordInstallationTokenLookup('contended-poll');\n return tokenFromEnvelope(envelope);\n }\n if (backoffActive(envelope, now)) {\n recordInstallationTokenLookup('backoff');\n throw providerErrorFromBackoff(\n envelope?.backoffReason ?? 'provider-unavailable',\n (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime(),\n );\n }\n }\n\n throw new GithubIntegrationProviderError(\n 'provider-unavailable',\n 'GitHub installation token mint is still in progress',\n 1,\n );\n }\n\n private async recordMint(\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken> {\n const startedAt = Date.now();\n try {\n const token = await withTimeout(mint(), this.mintTimeoutMs);\n recordInstallationTokenMint({outcome: 'success', durationMs: Date.now() - startedAt});\n return token;\n } catch (error) {\n recordInstallationTokenMint({outcome: 'failure', durationMs: Date.now() - startedAt});\n throw error;\n }\n }\n\n private async readEnvelope(\n workspaceId: string,\n installationId: number,\n ): Promise<InstallationTokenEnvelope | undefined> {\n const raw = await this.options.secretStore.read(workspaceId, installationId);\n if (raw === null) return undefined;\n\n const envelope = parseInstallationTokenEnvelope(raw);\n if (envelope === undefined) {\n logger().warn({installationId}, 'github installation token cache envelope failed to decode');\n }\n return envelope;\n }\n\n private async writeEnvelope(\n workspaceId: string,\n installationId: number,\n envelope: InstallationTokenEnvelope,\n ): Promise<void> {\n await this.options.secretStore.write(workspaceId, installationId, envelope);\n }\n\n private async resolveWorkspaceId(installationId: number): Promise<string> {\n const nowMs = this.now().getTime();\n const cached = this.workspaceIds.get(installationId);\n if (cached && cached.expiresAtMs > nowMs) return cached.workspaceId;\n\n const workspaceId = await this.options.resolveWorkspaceId(installationId);\n this.workspaceIds.set(installationId, {\n workspaceId,\n expiresAtMs: nowMs + this.workspaceCacheTtlMs,\n });\n return workspaceId;\n }\n}\n\ntype ActiveBackoffEnvelope = InstallationTokenEnvelope & {\n backoffUntil: Date;\n backoffReason: NonNullable<InstallationTokenEnvelope['backoffReason']>;\n};\n\ntype TokenEnvelope = InstallationTokenEnvelope & {token: string; expiresAt: Date};\n\nfunction activeBackoff(\n envelope: InstallationTokenEnvelope | undefined,\n now: Date,\n): envelope is ActiveBackoffEnvelope {\n return (\n backoffActive(envelope, now) &&\n envelope?.backoffUntil !== undefined &&\n envelope.backoffReason !== undefined\n );\n}\n\nfunction canServeStale(\n envelope: InstallationTokenEnvelope | undefined,\n now: Date,\n): envelope is TokenEnvelope {\n const terminalBackoff =\n activeBackoff(envelope, now) && mintErrorClassForReason(envelope.backoffReason) === 'terminal';\n return (\n envelope?.token !== undefined &&\n envelope.expiresAt !== undefined &&\n stillValid(envelope.expiresAt, now) &&\n !terminalBackoff\n );\n}\n\nfunction tokenFromEnvelope(envelope: InstallationTokenEnvelope): GithubInstallationAccessToken {\n if (!envelope.token || !envelope.expiresAt) {\n throw new GithubIntegrationProviderError(\n 'malformed-provider-response',\n 'GitHub installation token cache envelope is missing a token or expiry',\n );\n }\n return {\n token: envelope.token,\n expiresAt: envelope.expiresAt,\n ...(envelope.permissions === undefined ? {} : {permissions: envelope.permissions}),\n };\n}\n\nfunction withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {\n let timer: NodeJS.Timeout | undefined;\n const timeout = new Promise<T>((_, reject) => {\n timer = setTimeout(() => {\n reject(\n new GithubIntegrationProviderError(\n 'timeout',\n 'Timed out minting GitHub installation access token',\n ),\n );\n }, timeoutMs);\n });\n\n return Promise.race([promise, timeout]).finally(() => {\n if (timer) clearTimeout(timer);\n });\n}\n"],"names":["setTimeout","sleepTimeout","reportError","logger","GithubIntegrationProviderError","recordInstallationTokenBackoff","recordInstallationTokenLookup","recordInstallationTokenMint","backoffActive","backoffMs","classifyMintError","mintErrorClassForReason","parseInstallationTokenEnvelope","providerErrorFromBackoff","stillValid","toProviderError","usable","DEFAULT_POLL_DELAYS_MS","DEFAULT_WORKSPACE_CACHE_TTL_MS","DEFAULT_MINT_TIMEOUT_MS","SharedInstallationTokenCache","options","workspaceIds","Map","now","Date","sleep","ms","then","undefined","pollDelaysMs","workspaceCacheTtlMs","mintTimeoutMs","getOrMint","installationId","mint","workspaceId","resolveWorkspaceId","envelope","readEnvelope","tokenFromEnvelope","result","withLock","mintUnderLock","acquired","value","serveStaleOrPoll","params","activeBackoff","canServeStale","backoffReason","backoffUntil","getTime","token","recordMint","error","providerError","classified","until","reason","class","writeEnvelope","expiresAt","permissions","catch","writeError","warn","boundary","operation","extra","toISOString","info","initialNow","delayMs","startedAt","withTimeout","outcome","durationMs","raw","secretStore","read","write","nowMs","cached","get","expiresAtMs","set","terminalBackoff","promise","timeoutMs","timer","timeout","Promise","_","reject","race","finally","clearTimeout"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/api/shared-installation-token-cache.ts"],"sourcesContent":["import {setTimeout as sleepTimeout} from 'node:timers/promises';\nimport {reportError} from '@shipfox/node-error-monitoring';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {GithubIntegrationProviderError} from '#core/errors.js';\nimport {\n recordInstallationTokenBackoff,\n recordInstallationTokenLookup,\n recordInstallationTokenMint,\n} from '#metrics/index.js';\nimport type {GithubInstallationAccessToken} from './client.js';\nimport {\n backoffActive,\n backoffMs,\n classifyMintError,\n type InstallationTokenEnvelope,\n mintErrorClassForReason,\n parseInstallationTokenEnvelope,\n providerErrorFromBackoff,\n stillValid,\n toProviderError,\n usable,\n} from './installation-token-envelope.js';\n\nexport interface InstallationTokenCache {\n getOrMint(\n installationId: number,\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken>;\n}\n\nexport type InstallationTokenLockResult<T> = {acquired: true; value: T} | {acquired: false};\n\nexport interface InstallationTokenSecretStore {\n read(workspaceId: string, installationId: number): Promise<string | null>;\n write(\n workspaceId: string,\n installationId: number,\n envelope: InstallationTokenEnvelope,\n ): Promise<void>;\n}\n\nexport interface SharedInstallationTokenCacheOptions {\n secretStore: InstallationTokenSecretStore;\n withLock: <T>(\n installationId: number,\n fn: () => Promise<T>,\n ) => Promise<InstallationTokenLockResult<T>>;\n resolveWorkspaceId: (installationId: number) => Promise<string>;\n now?: (() => Date) | undefined;\n sleep?: ((ms: number) => Promise<void>) | undefined;\n pollDelaysMs?: number[] | undefined;\n workspaceCacheTtlMs?: number | undefined;\n mintTimeoutMs?: number | undefined;\n}\n\nconst DEFAULT_POLL_DELAYS_MS = [100, 200, 400, 500, 800];\nconst DEFAULT_WORKSPACE_CACHE_TTL_MS = 10 * 60 * 1000;\nconst DEFAULT_MINT_TIMEOUT_MS = 30 * 1000;\n\nexport class SharedInstallationTokenCache implements InstallationTokenCache {\n private readonly workspaceIds = new Map<number, {workspaceId: string; expiresAtMs: number}>();\n private readonly now: () => Date;\n private readonly sleep: (ms: number) => Promise<void>;\n private readonly pollDelaysMs: number[];\n private readonly workspaceCacheTtlMs: number;\n private readonly mintTimeoutMs: number;\n\n constructor(private readonly options: SharedInstallationTokenCacheOptions) {\n this.now = options.now ?? (() => new Date());\n this.sleep = options.sleep ?? ((ms) => sleepTimeout(ms).then(() => undefined));\n this.pollDelaysMs = options.pollDelaysMs ?? DEFAULT_POLL_DELAYS_MS;\n this.workspaceCacheTtlMs = options.workspaceCacheTtlMs ?? DEFAULT_WORKSPACE_CACHE_TTL_MS;\n this.mintTimeoutMs = options.mintTimeoutMs ?? DEFAULT_MINT_TIMEOUT_MS;\n }\n\n async getOrMint(\n installationId: number,\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken> {\n const workspaceId = await this.resolveWorkspaceId(installationId);\n const envelope = await this.readEnvelope(workspaceId, installationId);\n if (usable(envelope, this.now())) {\n recordInstallationTokenLookup('db-hit');\n return tokenFromEnvelope(envelope);\n }\n\n const result = await this.options.withLock(installationId, () =>\n this.mintUnderLock({workspaceId, installationId, mint}),\n );\n if (result.acquired) return result.value;\n\n return await this.serveStaleOrPoll({workspaceId, installationId, envelope});\n }\n\n private async mintUnderLock(params: {\n workspaceId: string;\n installationId: number;\n mint: () => Promise<GithubInstallationAccessToken>;\n }): Promise<GithubInstallationAccessToken> {\n const envelope = await this.readEnvelope(params.workspaceId, params.installationId);\n const now = this.now();\n if (usable(envelope, now)) {\n recordInstallationTokenLookup('db-hit');\n return tokenFromEnvelope(envelope);\n }\n\n if (activeBackoff(envelope, now)) {\n if (canServeStale(envelope, now)) {\n recordInstallationTokenLookup('served-stale');\n return tokenFromEnvelope(envelope);\n }\n recordInstallationTokenLookup('backoff');\n throw providerErrorFromBackoff(\n envelope?.backoffReason ?? 'provider-unavailable',\n (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime(),\n envelope?.backoffError,\n );\n }\n\n let token: GithubInstallationAccessToken;\n try {\n token = await this.recordMint(params.mint);\n } catch (error) {\n const providerError = toProviderError(error);\n const classified = classifyMintError(providerError);\n const until = new Date(this.now().getTime() + backoffMs(classified));\n recordInstallationTokenBackoff({reason: classified.reason, class: classified.class});\n\n await this.writeEnvelope(params.workspaceId, params.installationId, {\n token: envelope?.token,\n expiresAt: envelope?.expiresAt,\n permissions: envelope?.permissions,\n backoffUntil: until,\n backoffReason: classified.reason,\n backoffError: {\n message: providerError.message,\n ...(providerError.status === undefined ? {} : {status: providerError.status}),\n },\n }).catch((writeError) => {\n logger().warn(\n {installationId: params.installationId, reason: classified.reason, error: writeError},\n 'github installation token backoff write failed',\n );\n reportError(writeError, {\n boundary: 'integration.cache',\n operation: 'write-backoff-envelope',\n extra: {installationId: params.installationId},\n });\n });\n\n if (\n classified.class === 'transient' &&\n envelope?.token &&\n stillValid(envelope.expiresAt, this.now())\n ) {\n logger().warn(\n {\n installationId: params.installationId,\n expiresAt: envelope.expiresAt?.toISOString(),\n reason: classified.reason,\n backoffUntil: until.toISOString(),\n },\n 'github installation token mint failed; serving stale token',\n );\n recordInstallationTokenLookup('served-stale');\n return tokenFromEnvelope(envelope);\n }\n\n logger().warn(\n {\n installationId: params.installationId,\n reason: classified.reason,\n backoffUntil: until.toISOString(),\n error: providerError,\n },\n 'github installation token mint failed; backoff recorded',\n );\n recordInstallationTokenLookup('backoff');\n throw providerError;\n }\n\n try {\n await this.writeEnvelope(params.workspaceId, params.installationId, {\n token: token.token,\n expiresAt: token.expiresAt,\n permissions: token.permissions,\n });\n } catch (error) {\n logger().warn(\n {installationId: params.installationId, expiresAt: token.expiresAt.toISOString(), error},\n 'github installation token cache write failed after mint',\n );\n reportError(error, {\n boundary: 'integration.cache',\n operation: 'write-minted-token',\n extra: {installationId: params.installationId},\n });\n }\n\n logger().info(\n {installationId: params.installationId, expiresAt: token.expiresAt.toISOString()},\n 'github installation token minted',\n );\n recordInstallationTokenLookup('minted');\n return token;\n }\n\n private async serveStaleOrPoll(params: {\n workspaceId: string;\n installationId: number;\n envelope: InstallationTokenEnvelope | undefined;\n }): Promise<GithubInstallationAccessToken> {\n const initialNow = this.now();\n if (canServeStale(params.envelope, initialNow)) {\n recordInstallationTokenLookup('served-stale');\n return tokenFromEnvelope(params.envelope);\n }\n if (activeBackoff(params.envelope, initialNow)) {\n recordInstallationTokenLookup('backoff');\n throw providerErrorFromBackoff(\n params.envelope.backoffReason,\n params.envelope.backoffUntil.getTime() - initialNow.getTime(),\n params.envelope.backoffError,\n );\n }\n\n for (const delayMs of this.pollDelaysMs) {\n await this.sleep(delayMs);\n const envelope = await this.readEnvelope(params.workspaceId, params.installationId);\n const now = this.now();\n if (usable(envelope, now)) {\n recordInstallationTokenLookup('contended-poll');\n return tokenFromEnvelope(envelope);\n }\n if (backoffActive(envelope, now)) {\n recordInstallationTokenLookup('backoff');\n throw providerErrorFromBackoff(\n envelope?.backoffReason ?? 'provider-unavailable',\n (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime(),\n envelope?.backoffError,\n );\n }\n }\n\n throw new GithubIntegrationProviderError(\n 'provider-unavailable',\n 'GitHub installation token mint is still in progress',\n 1,\n );\n }\n\n private async recordMint(\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken> {\n const startedAt = Date.now();\n try {\n const token = await withTimeout(mint(), this.mintTimeoutMs);\n recordInstallationTokenMint({outcome: 'success', durationMs: Date.now() - startedAt});\n return token;\n } catch (error) {\n recordInstallationTokenMint({outcome: 'failure', durationMs: Date.now() - startedAt});\n throw error;\n }\n }\n\n private async readEnvelope(\n workspaceId: string,\n installationId: number,\n ): Promise<InstallationTokenEnvelope | undefined> {\n const raw = await this.options.secretStore.read(workspaceId, installationId);\n if (raw === null) return undefined;\n\n const envelope = parseInstallationTokenEnvelope(raw);\n if (envelope === undefined) {\n logger().warn({installationId}, 'github installation token cache envelope failed to decode');\n }\n return envelope;\n }\n\n private async writeEnvelope(\n workspaceId: string,\n installationId: number,\n envelope: InstallationTokenEnvelope,\n ): Promise<void> {\n await this.options.secretStore.write(workspaceId, installationId, envelope);\n }\n\n private async resolveWorkspaceId(installationId: number): Promise<string> {\n const nowMs = this.now().getTime();\n const cached = this.workspaceIds.get(installationId);\n if (cached && cached.expiresAtMs > nowMs) return cached.workspaceId;\n\n const workspaceId = await this.options.resolveWorkspaceId(installationId);\n this.workspaceIds.set(installationId, {\n workspaceId,\n expiresAtMs: nowMs + this.workspaceCacheTtlMs,\n });\n return workspaceId;\n }\n}\n\ntype ActiveBackoffEnvelope = InstallationTokenEnvelope & {\n backoffUntil: Date;\n backoffReason: NonNullable<InstallationTokenEnvelope['backoffReason']>;\n};\n\ntype TokenEnvelope = InstallationTokenEnvelope & {token: string; expiresAt: Date};\n\nfunction activeBackoff(\n envelope: InstallationTokenEnvelope | undefined,\n now: Date,\n): envelope is ActiveBackoffEnvelope {\n return (\n backoffActive(envelope, now) &&\n envelope?.backoffUntil !== undefined &&\n envelope.backoffReason !== undefined\n );\n}\n\nfunction canServeStale(\n envelope: InstallationTokenEnvelope | undefined,\n now: Date,\n): envelope is TokenEnvelope {\n const terminalBackoff =\n activeBackoff(envelope, now) && mintErrorClassForReason(envelope.backoffReason) === 'terminal';\n return (\n envelope?.token !== undefined &&\n envelope.expiresAt !== undefined &&\n stillValid(envelope.expiresAt, now) &&\n !terminalBackoff\n );\n}\n\nfunction tokenFromEnvelope(envelope: InstallationTokenEnvelope): GithubInstallationAccessToken {\n if (!envelope.token || !envelope.expiresAt) {\n throw new GithubIntegrationProviderError(\n 'malformed-provider-response',\n 'GitHub installation token cache envelope is missing a token or expiry',\n );\n }\n return {\n token: envelope.token,\n expiresAt: envelope.expiresAt,\n ...(envelope.permissions === undefined ? {} : {permissions: envelope.permissions}),\n };\n}\n\nfunction withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {\n let timer: NodeJS.Timeout | undefined;\n const timeout = new Promise<T>((_, reject) => {\n timer = setTimeout(() => {\n reject(\n new GithubIntegrationProviderError(\n 'timeout',\n 'Timed out minting GitHub installation access token',\n ),\n );\n }, timeoutMs);\n });\n\n return Promise.race([promise, timeout]).finally(() => {\n if (timer) clearTimeout(timer);\n });\n}\n"],"names":["setTimeout","sleepTimeout","reportError","logger","GithubIntegrationProviderError","recordInstallationTokenBackoff","recordInstallationTokenLookup","recordInstallationTokenMint","backoffActive","backoffMs","classifyMintError","mintErrorClassForReason","parseInstallationTokenEnvelope","providerErrorFromBackoff","stillValid","toProviderError","usable","DEFAULT_POLL_DELAYS_MS","DEFAULT_WORKSPACE_CACHE_TTL_MS","DEFAULT_MINT_TIMEOUT_MS","SharedInstallationTokenCache","options","workspaceIds","Map","now","Date","sleep","ms","then","undefined","pollDelaysMs","workspaceCacheTtlMs","mintTimeoutMs","getOrMint","installationId","mint","workspaceId","resolveWorkspaceId","envelope","readEnvelope","tokenFromEnvelope","result","withLock","mintUnderLock","acquired","value","serveStaleOrPoll","params","activeBackoff","canServeStale","backoffReason","backoffUntil","getTime","backoffError","token","recordMint","error","providerError","classified","until","reason","class","writeEnvelope","expiresAt","permissions","message","status","catch","writeError","warn","boundary","operation","extra","toISOString","info","initialNow","delayMs","startedAt","withTimeout","outcome","durationMs","raw","secretStore","read","write","nowMs","cached","get","expiresAtMs","set","terminalBackoff","promise","timeoutMs","timer","timeout","Promise","_","reject","race","finally","clearTimeout"],"mappings":"AAAA,SAAQA,cAAcC,YAAY,QAAO,uBAAuB;AAChE,SAAQC,WAAW,QAAO,iCAAiC;AAC3D,SAAQC,MAAM,QAAO,8BAA8B;AACnD,SAAQC,8BAA8B,QAAO,kBAAkB;AAC/D,SACEC,8BAA8B,EAC9BC,6BAA6B,EAC7BC,2BAA2B,QACtB,oBAAoB;AAE3B,SACEC,aAAa,EACbC,SAAS,EACTC,iBAAiB,EAEjBC,uBAAuB,EACvBC,8BAA8B,EAC9BC,wBAAwB,EACxBC,UAAU,EACVC,eAAe,EACfC,MAAM,QACD,mCAAmC;AAkC1C,MAAMC,yBAAyB;IAAC;IAAK;IAAK;IAAK;IAAK;CAAI;AACxD,MAAMC,iCAAiC,KAAK,KAAK;AACjD,MAAMC,0BAA0B,KAAK;AAErC,OAAO,MAAMC;IAQX,YAAY,AAAiBC,OAA4C,CAAE;aAA9CA,UAAAA;aAPZC,eAAe,IAAIC;QAQlC,IAAI,CAACC,GAAG,GAAGH,QAAQG,GAAG,IAAK,CAAA,IAAM,IAAIC,MAAK;QAC1C,IAAI,CAACC,KAAK,GAAGL,QAAQK,KAAK,IAAK,CAAA,CAACC,KAAO1B,aAAa0B,IAAIC,IAAI,CAAC,IAAMC,UAAS;QAC5E,IAAI,CAACC,YAAY,GAAGT,QAAQS,YAAY,IAAIb;QAC5C,IAAI,CAACc,mBAAmB,GAAGV,QAAQU,mBAAmB,IAAIb;QAC1D,IAAI,CAACc,aAAa,GAAGX,QAAQW,aAAa,IAAIb;IAChD;IAEA,MAAMc,UACJC,cAAsB,EACtBC,IAAkD,EACV;QACxC,MAAMC,cAAc,MAAM,IAAI,CAACC,kBAAkB,CAACH;QAClD,MAAMI,WAAW,MAAM,IAAI,CAACC,YAAY,CAACH,aAAaF;QACtD,IAAIlB,OAAOsB,UAAU,IAAI,CAACd,GAAG,KAAK;YAChClB,8BAA8B;YAC9B,OAAOkC,kBAAkBF;QAC3B;QAEA,MAAMG,SAAS,MAAM,IAAI,CAACpB,OAAO,CAACqB,QAAQ,CAACR,gBAAgB,IACzD,IAAI,CAACS,aAAa,CAAC;gBAACP;gBAAaF;gBAAgBC;YAAI;QAEvD,IAAIM,OAAOG,QAAQ,EAAE,OAAOH,OAAOI,KAAK;QAExC,OAAO,MAAM,IAAI,CAACC,gBAAgB,CAAC;YAACV;YAAaF;YAAgBI;QAAQ;IAC3E;IAEA,MAAcK,cAAcI,MAI3B,EAA0C;QACzC,MAAMT,WAAW,MAAM,IAAI,CAACC,YAAY,CAACQ,OAAOX,WAAW,EAAEW,OAAOb,cAAc;QAClF,MAAMV,MAAM,IAAI,CAACA,GAAG;QACpB,IAAIR,OAAOsB,UAAUd,MAAM;YACzBlB,8BAA8B;YAC9B,OAAOkC,kBAAkBF;QAC3B;QAEA,IAAIU,cAAcV,UAAUd,MAAM;YAChC,IAAIyB,cAAcX,UAAUd,MAAM;gBAChClB,8BAA8B;gBAC9B,OAAOkC,kBAAkBF;YAC3B;YACAhC,8BAA8B;YAC9B,MAAMO,yBACJyB,UAAUY,iBAAiB,wBAC3B,AAACZ,CAAAA,UAAUa,cAAcC,aAAa5B,IAAI4B,OAAO,EAAC,IAAK5B,IAAI4B,OAAO,IAClEd,UAAUe;QAEd;QAEA,IAAIC;QACJ,IAAI;YACFA,QAAQ,MAAM,IAAI,CAACC,UAAU,CAACR,OAAOZ,IAAI;QAC3C,EAAE,OAAOqB,OAAO;YACd,MAAMC,gBAAgB1C,gBAAgByC;YACtC,MAAME,aAAahD,kBAAkB+C;YACrC,MAAME,QAAQ,IAAIlC,KAAK,IAAI,CAACD,GAAG,GAAG4B,OAAO,KAAK3C,UAAUiD;YACxDrD,+BAA+B;gBAACuD,QAAQF,WAAWE,MAAM;gBAAEC,OAAOH,WAAWG,KAAK;YAAA;YAElF,MAAM,IAAI,CAACC,aAAa,CAACf,OAAOX,WAAW,EAAEW,OAAOb,cAAc,EAAE;gBAClEoB,OAAOhB,UAAUgB;gBACjBS,WAAWzB,UAAUyB;gBACrBC,aAAa1B,UAAU0B;gBACvBb,cAAcQ;gBACdT,eAAeQ,WAAWE,MAAM;gBAChCP,cAAc;oBACZY,SAASR,cAAcQ,OAAO;oBAC9B,GAAIR,cAAcS,MAAM,KAAKrC,YAAY,CAAC,IAAI;wBAACqC,QAAQT,cAAcS,MAAM;oBAAA,CAAC;gBAC9E;YACF,GAAGC,KAAK,CAAC,CAACC;gBACRjE,SAASkE,IAAI,CACX;oBAACnC,gBAAgBa,OAAOb,cAAc;oBAAE0B,QAAQF,WAAWE,MAAM;oBAAEJ,OAAOY;gBAAU,GACpF;gBAEFlE,YAAYkE,YAAY;oBACtBE,UAAU;oBACVC,WAAW;oBACXC,OAAO;wBAACtC,gBAAgBa,OAAOb,cAAc;oBAAA;gBAC/C;YACF;YAEA,IACEwB,WAAWG,KAAK,KAAK,eACrBvB,UAAUgB,SACVxC,WAAWwB,SAASyB,SAAS,EAAE,IAAI,CAACvC,GAAG,KACvC;gBACArB,SAASkE,IAAI,CACX;oBACEnC,gBAAgBa,OAAOb,cAAc;oBACrC6B,WAAWzB,SAASyB,SAAS,EAAEU;oBAC/Bb,QAAQF,WAAWE,MAAM;oBACzBT,cAAcQ,MAAMc,WAAW;gBACjC,GACA;gBAEFnE,8BAA8B;gBAC9B,OAAOkC,kBAAkBF;YAC3B;YAEAnC,SAASkE,IAAI,CACX;gBACEnC,gBAAgBa,OAAOb,cAAc;gBACrC0B,QAAQF,WAAWE,MAAM;gBACzBT,cAAcQ,MAAMc,WAAW;gBAC/BjB,OAAOC;YACT,GACA;YAEFnD,8BAA8B;YAC9B,MAAMmD;QACR;QAEA,IAAI;YACF,MAAM,IAAI,CAACK,aAAa,CAACf,OAAOX,WAAW,EAAEW,OAAOb,cAAc,EAAE;gBAClEoB,OAAOA,MAAMA,KAAK;gBAClBS,WAAWT,MAAMS,SAAS;gBAC1BC,aAAaV,MAAMU,WAAW;YAChC;QACF,EAAE,OAAOR,OAAO;YACdrD,SAASkE,IAAI,CACX;gBAACnC,gBAAgBa,OAAOb,cAAc;gBAAE6B,WAAWT,MAAMS,SAAS,CAACU,WAAW;gBAAIjB;YAAK,GACvF;YAEFtD,YAAYsD,OAAO;gBACjBc,UAAU;gBACVC,WAAW;gBACXC,OAAO;oBAACtC,gBAAgBa,OAAOb,cAAc;gBAAA;YAC/C;QACF;QAEA/B,SAASuE,IAAI,CACX;YAACxC,gBAAgBa,OAAOb,cAAc;YAAE6B,WAAWT,MAAMS,SAAS,CAACU,WAAW;QAAE,GAChF;QAEFnE,8BAA8B;QAC9B,OAAOgD;IACT;IAEA,MAAcR,iBAAiBC,MAI9B,EAA0C;QACzC,MAAM4B,aAAa,IAAI,CAACnD,GAAG;QAC3B,IAAIyB,cAAcF,OAAOT,QAAQ,EAAEqC,aAAa;YAC9CrE,8BAA8B;YAC9B,OAAOkC,kBAAkBO,OAAOT,QAAQ;QAC1C;QACA,IAAIU,cAAcD,OAAOT,QAAQ,EAAEqC,aAAa;YAC9CrE,8BAA8B;YAC9B,MAAMO,yBACJkC,OAAOT,QAAQ,CAACY,aAAa,EAC7BH,OAAOT,QAAQ,CAACa,YAAY,CAACC,OAAO,KAAKuB,WAAWvB,OAAO,IAC3DL,OAAOT,QAAQ,CAACe,YAAY;QAEhC;QAEA,KAAK,MAAMuB,WAAW,IAAI,CAAC9C,YAAY,CAAE;YACvC,MAAM,IAAI,CAACJ,KAAK,CAACkD;YACjB,MAAMtC,WAAW,MAAM,IAAI,CAACC,YAAY,CAACQ,OAAOX,WAAW,EAAEW,OAAOb,cAAc;YAClF,MAAMV,MAAM,IAAI,CAACA,GAAG;YACpB,IAAIR,OAAOsB,UAAUd,MAAM;gBACzBlB,8BAA8B;gBAC9B,OAAOkC,kBAAkBF;YAC3B;YACA,IAAI9B,cAAc8B,UAAUd,MAAM;gBAChClB,8BAA8B;gBAC9B,MAAMO,yBACJyB,UAAUY,iBAAiB,wBAC3B,AAACZ,CAAAA,UAAUa,cAAcC,aAAa5B,IAAI4B,OAAO,EAAC,IAAK5B,IAAI4B,OAAO,IAClEd,UAAUe;YAEd;QACF;QAEA,MAAM,IAAIjD,+BACR,wBACA,uDACA;IAEJ;IAEA,MAAcmD,WACZpB,IAAkD,EACV;QACxC,MAAM0C,YAAYpD,KAAKD,GAAG;QAC1B,IAAI;YACF,MAAM8B,QAAQ,MAAMwB,YAAY3C,QAAQ,IAAI,CAACH,aAAa;YAC1DzB,4BAA4B;gBAACwE,SAAS;gBAAWC,YAAYvD,KAAKD,GAAG,KAAKqD;YAAS;YACnF,OAAOvB;QACT,EAAE,OAAOE,OAAO;YACdjD,4BAA4B;gBAACwE,SAAS;gBAAWC,YAAYvD,KAAKD,GAAG,KAAKqD;YAAS;YACnF,MAAMrB;QACR;IACF;IAEA,MAAcjB,aACZH,WAAmB,EACnBF,cAAsB,EAC0B;QAChD,MAAM+C,MAAM,MAAM,IAAI,CAAC5D,OAAO,CAAC6D,WAAW,CAACC,IAAI,CAAC/C,aAAaF;QAC7D,IAAI+C,QAAQ,MAAM,OAAOpD;QAEzB,MAAMS,WAAW1B,+BAA+BqE;QAChD,IAAI3C,aAAaT,WAAW;YAC1B1B,SAASkE,IAAI,CAAC;gBAACnC;YAAc,GAAG;QAClC;QACA,OAAOI;IACT;IAEA,MAAcwB,cACZ1B,WAAmB,EACnBF,cAAsB,EACtBI,QAAmC,EACpB;QACf,MAAM,IAAI,CAACjB,OAAO,CAAC6D,WAAW,CAACE,KAAK,CAAChD,aAAaF,gBAAgBI;IACpE;IAEA,MAAcD,mBAAmBH,cAAsB,EAAmB;QACxE,MAAMmD,QAAQ,IAAI,CAAC7D,GAAG,GAAG4B,OAAO;QAChC,MAAMkC,SAAS,IAAI,CAAChE,YAAY,CAACiE,GAAG,CAACrD;QACrC,IAAIoD,UAAUA,OAAOE,WAAW,GAAGH,OAAO,OAAOC,OAAOlD,WAAW;QAEnE,MAAMA,cAAc,MAAM,IAAI,CAACf,OAAO,CAACgB,kBAAkB,CAACH;QAC1D,IAAI,CAACZ,YAAY,CAACmE,GAAG,CAACvD,gBAAgB;YACpCE;YACAoD,aAAaH,QAAQ,IAAI,CAACtD,mBAAmB;QAC/C;QACA,OAAOK;IACT;AACF;AASA,SAASY,cACPV,QAA+C,EAC/Cd,GAAS;IAET,OACEhB,cAAc8B,UAAUd,QACxBc,UAAUa,iBAAiBtB,aAC3BS,SAASY,aAAa,KAAKrB;AAE/B;AAEA,SAASoB,cACPX,QAA+C,EAC/Cd,GAAS;IAET,MAAMkE,kBACJ1C,cAAcV,UAAUd,QAAQb,wBAAwB2B,SAASY,aAAa,MAAM;IACtF,OACEZ,UAAUgB,UAAUzB,aACpBS,SAASyB,SAAS,KAAKlC,aACvBf,WAAWwB,SAASyB,SAAS,EAAEvC,QAC/B,CAACkE;AAEL;AAEA,SAASlD,kBAAkBF,QAAmC;IAC5D,IAAI,CAACA,SAASgB,KAAK,IAAI,CAAChB,SAASyB,SAAS,EAAE;QAC1C,MAAM,IAAI3D,+BACR,+BACA;IAEJ;IACA,OAAO;QACLkD,OAAOhB,SAASgB,KAAK;QACrBS,WAAWzB,SAASyB,SAAS;QAC7B,GAAIzB,SAAS0B,WAAW,KAAKnC,YAAY,CAAC,IAAI;YAACmC,aAAa1B,SAAS0B,WAAW;QAAA,CAAC;IACnF;AACF;AAEA,SAASc,YAAea,OAAmB,EAAEC,SAAiB;IAC5D,IAAIC;IACJ,MAAMC,UAAU,IAAIC,QAAW,CAACC,GAAGC;QACjCJ,QAAQ7F,WAAW;YACjBiG,OACE,IAAI7F,+BACF,WACA;QAGN,GAAGwF;IACL;IAEA,OAAOG,QAAQG,IAAI,CAAC;QAACP;QAASG;KAAQ,EAAEK,OAAO,CAAC;QAC9C,IAAIN,OAAOO,aAAaP;IAC1B;AACF"}
|
package/dist/config.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const config: Readonly<{
|
|
|
7
7
|
GITHUB_APP_SLUG: string;
|
|
8
8
|
GITHUB_APP_USERNAME: string | undefined;
|
|
9
9
|
GITHUB_API_BASE_URL: string;
|
|
10
|
+
GITHUB_INSTALLATION_TOKEN_FORMAT_OVERRIDE: "disabled" | "enabled" | undefined;
|
|
10
11
|
GITHUB_INSTALL_STATE_SECRET: string;
|
|
11
12
|
} & import("@shipfox/config").CleanedEnvAccessors>;
|
|
12
13
|
export declare function normalizedGithubPrivateKey(): string;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM;;;;;;;;;;;kDAmCjB,CAAC;AAEH,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD"}
|
package/dist/config.js
CHANGED
|
@@ -27,6 +27,14 @@ export const config = createConfig({
|
|
|
27
27
|
desc: 'Base URL used for GitHub REST API requests. Set this only for GitHub Enterprise Server or a compatible test server.',
|
|
28
28
|
default: 'https://api.github.com'
|
|
29
29
|
}),
|
|
30
|
+
GITHUB_INSTALLATION_TOKEN_FORMAT_OVERRIDE: str({
|
|
31
|
+
desc: 'Temporary GitHub installation token format override. Set this to enabled to request stateless tokens, disabled to request stateful tokens, or leave it unset to follow the GitHub rollout.',
|
|
32
|
+
choices: [
|
|
33
|
+
'enabled',
|
|
34
|
+
'disabled'
|
|
35
|
+
],
|
|
36
|
+
default: undefined
|
|
37
|
+
}),
|
|
30
38
|
GITHUB_INSTALL_STATE_SECRET: str({
|
|
31
39
|
desc: 'Secret used to sign the state token that protects the GitHub App install flow. Required.'
|
|
32
40
|
})
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {createConfig, str, url} from '@shipfox/config';\n\nconst trailingSlashesPattern = /\\/+$/u;\n\nexport const config = createConfig({\n GITHUB_APP_ID: str({\n desc: \"Numeric ID of the GitHub App, found on the app's settings page. Required.\",\n }),\n GITHUB_APP_PRIVATE_KEY: str({\n desc: 'Private key of the GitHub App in PEM format, used to sign API requests. Newlines may be written as \\\\n and are restored at runtime. Required.',\n }),\n GITHUB_APP_CLIENT_ID: str({\n desc: 'OAuth client ID of the GitHub App, used for user sign-in. Required.',\n }),\n GITHUB_APP_CLIENT_SECRET: str({\n desc: 'OAuth client secret of the GitHub App. Required.',\n }),\n GITHUB_APP_WEBHOOK_SECRET: str({\n desc: 'Secret used to verify the signature of incoming GitHub webhooks. Must match the value set on the GitHub App. Required.',\n }),\n GITHUB_APP_SLUG: str({\n desc: 'URL slug of the GitHub App, used to build install and callback links. Required.',\n }),\n GITHUB_APP_USERNAME: str({\n desc: 'GitHub App username used as the Git commit author when checkout credentials are persisted. Set this to the app username, such as my-app. The [bot] suffix is added automatically. Leave unset to keep Git author identity unset.',\n default: undefined,\n }),\n GITHUB_API_BASE_URL: url({\n desc: 'Base URL used for GitHub REST API requests. Set this only for GitHub Enterprise Server or a compatible test server.',\n default: 'https://api.github.com',\n }),\n GITHUB_INSTALL_STATE_SECRET: str({\n desc: 'Secret used to sign the state token that protects the GitHub App install flow. Required.',\n }),\n});\n\nexport function normalizedGithubPrivateKey(): string {\n return config.GITHUB_APP_PRIVATE_KEY.replaceAll('\\\\n', '\\n');\n}\n\nexport function normalizeGithubApiBaseUrl(baseUrl: string): string {\n return baseUrl.replace(trailingSlashesPattern, '');\n}\n\nexport function normalizedGithubApiBaseUrl(): string {\n return normalizeGithubApiBaseUrl(config.GITHUB_API_BASE_URL);\n}\n"],"names":["createConfig","str","url","trailingSlashesPattern","config","GITHUB_APP_ID","desc","GITHUB_APP_PRIVATE_KEY","GITHUB_APP_CLIENT_ID","GITHUB_APP_CLIENT_SECRET","GITHUB_APP_WEBHOOK_SECRET","GITHUB_APP_SLUG","GITHUB_APP_USERNAME","default","undefined","GITHUB_API_BASE_URL","GITHUB_INSTALL_STATE_SECRET","normalizedGithubPrivateKey","replaceAll","normalizeGithubApiBaseUrl","baseUrl","replace","normalizedGithubApiBaseUrl"],"mappings":"AAAA,SAAQA,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAEvD,MAAMC,yBAAyB;AAE/B,OAAO,MAAMC,SAASJ,aAAa;IACjCK,eAAeJ,IAAI;QACjBK,MAAM;IACR;IACAC,wBAAwBN,IAAI;QAC1BK,MAAM;IACR;IACAE,sBAAsBP,IAAI;QACxBK,MAAM;IACR;IACAG,0BAA0BR,IAAI;QAC5BK,MAAM;IACR;IACAI,2BAA2BT,IAAI;QAC7BK,MAAM;IACR;IACAK,iBAAiBV,IAAI;QACnBK,MAAM;IACR;IACAM,qBAAqBX,IAAI;QACvBK,MAAM;QACNO,SAASC;IACX;IACAC,qBAAqBb,IAAI;QACvBI,MAAM;QACNO,SAAS;IACX;IACAG,
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {createConfig, str, url} from '@shipfox/config';\n\nconst trailingSlashesPattern = /\\/+$/u;\n\nexport const config = createConfig({\n GITHUB_APP_ID: str({\n desc: \"Numeric ID of the GitHub App, found on the app's settings page. Required.\",\n }),\n GITHUB_APP_PRIVATE_KEY: str({\n desc: 'Private key of the GitHub App in PEM format, used to sign API requests. Newlines may be written as \\\\n and are restored at runtime. Required.',\n }),\n GITHUB_APP_CLIENT_ID: str({\n desc: 'OAuth client ID of the GitHub App, used for user sign-in. Required.',\n }),\n GITHUB_APP_CLIENT_SECRET: str({\n desc: 'OAuth client secret of the GitHub App. Required.',\n }),\n GITHUB_APP_WEBHOOK_SECRET: str({\n desc: 'Secret used to verify the signature of incoming GitHub webhooks. Must match the value set on the GitHub App. Required.',\n }),\n GITHUB_APP_SLUG: str({\n desc: 'URL slug of the GitHub App, used to build install and callback links. Required.',\n }),\n GITHUB_APP_USERNAME: str({\n desc: 'GitHub App username used as the Git commit author when checkout credentials are persisted. Set this to the app username, such as my-app. The [bot] suffix is added automatically. Leave unset to keep Git author identity unset.',\n default: undefined,\n }),\n GITHUB_API_BASE_URL: url({\n desc: 'Base URL used for GitHub REST API requests. Set this only for GitHub Enterprise Server or a compatible test server.',\n default: 'https://api.github.com',\n }),\n GITHUB_INSTALLATION_TOKEN_FORMAT_OVERRIDE: str({\n desc: 'Temporary GitHub installation token format override. Set this to enabled to request stateless tokens, disabled to request stateful tokens, or leave it unset to follow the GitHub rollout.',\n choices: ['enabled', 'disabled'] as const,\n default: undefined,\n }),\n GITHUB_INSTALL_STATE_SECRET: str({\n desc: 'Secret used to sign the state token that protects the GitHub App install flow. Required.',\n }),\n});\n\nexport function normalizedGithubPrivateKey(): string {\n return config.GITHUB_APP_PRIVATE_KEY.replaceAll('\\\\n', '\\n');\n}\n\nexport function normalizeGithubApiBaseUrl(baseUrl: string): string {\n return baseUrl.replace(trailingSlashesPattern, '');\n}\n\nexport function normalizedGithubApiBaseUrl(): string {\n return normalizeGithubApiBaseUrl(config.GITHUB_API_BASE_URL);\n}\n"],"names":["createConfig","str","url","trailingSlashesPattern","config","GITHUB_APP_ID","desc","GITHUB_APP_PRIVATE_KEY","GITHUB_APP_CLIENT_ID","GITHUB_APP_CLIENT_SECRET","GITHUB_APP_WEBHOOK_SECRET","GITHUB_APP_SLUG","GITHUB_APP_USERNAME","default","undefined","GITHUB_API_BASE_URL","GITHUB_INSTALLATION_TOKEN_FORMAT_OVERRIDE","choices","GITHUB_INSTALL_STATE_SECRET","normalizedGithubPrivateKey","replaceAll","normalizeGithubApiBaseUrl","baseUrl","replace","normalizedGithubApiBaseUrl"],"mappings":"AAAA,SAAQA,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAEvD,MAAMC,yBAAyB;AAE/B,OAAO,MAAMC,SAASJ,aAAa;IACjCK,eAAeJ,IAAI;QACjBK,MAAM;IACR;IACAC,wBAAwBN,IAAI;QAC1BK,MAAM;IACR;IACAE,sBAAsBP,IAAI;QACxBK,MAAM;IACR;IACAG,0BAA0BR,IAAI;QAC5BK,MAAM;IACR;IACAI,2BAA2BT,IAAI;QAC7BK,MAAM;IACR;IACAK,iBAAiBV,IAAI;QACnBK,MAAM;IACR;IACAM,qBAAqBX,IAAI;QACvBK,MAAM;QACNO,SAASC;IACX;IACAC,qBAAqBb,IAAI;QACvBI,MAAM;QACNO,SAAS;IACX;IACAG,2CAA2Cf,IAAI;QAC7CK,MAAM;QACNW,SAAS;YAAC;YAAW;SAAW;QAChCJ,SAASC;IACX;IACAI,6BAA6BjB,IAAI;QAC/BK,MAAM;IACR;AACF,GAAG;AAEH,OAAO,SAASa;IACd,OAAOf,OAAOG,sBAAsB,CAACa,UAAU,CAAC,OAAO;AACzD;AAEA,OAAO,SAASC,0BAA0BC,OAAe;IACvD,OAAOA,QAAQC,OAAO,CAACpB,wBAAwB;AACjD;AAEA,OAAO,SAASqB;IACd,OAAOH,0BAA0BjB,OAAOW,mBAAmB;AAC7D"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentToolSelectionCatalog, AgentToolSession, AgentToolsProvider, IntegrationConnection, OpenAgentToolsSessionInput } from '@shipfox/api-integration-spi';
|
|
2
2
|
import { type GithubInstallationTokenProvider } from '#api/installation-token-provider.js';
|
|
3
3
|
import type { GithubInstallation } from '#db/installations.js';
|
|
4
|
-
import { type GithubAgentToolCatalogEntry, type GithubAgentToolRequiredScope } from './github-agent-tool-catalog.js';
|
|
4
|
+
import { type GithubAgentToolCatalogEntry, type GithubAgentToolId, type GithubAgentToolRequiredScope } from './github-agent-tool-catalog.js';
|
|
5
5
|
export type { GithubAgentToolCatalogEntry, GithubAgentToolCategory, GithubAgentToolId, GithubAgentToolPermission, GithubAgentToolPermissionAccess, GithubAgentToolRequiredPermission, GithubAgentToolRequiredScope, GithubAgentToolSensitivity, } from './github-agent-tool-catalog.js';
|
|
6
6
|
export { buildGithubAgentToolSelectionCatalog, DEFAULT_JOB_LOG_TAIL_LINES, githubAgentToolCatalog, githubAgentToolSelectionCatalog, } from './github-agent-tool-catalog.js';
|
|
7
7
|
type GithubIntegrationConnection = IntegrationConnection<'github'>;
|
|
@@ -26,10 +26,17 @@ export interface GithubAgentToolsProviderOptions {
|
|
|
26
26
|
tokenProvider?: GithubInstallationTokenProvider | undefined;
|
|
27
27
|
createClient?: GithubToolClientFactory | undefined;
|
|
28
28
|
}
|
|
29
|
+
export interface GithubToolResponse {
|
|
30
|
+
data: unknown;
|
|
31
|
+
headers?: Record<string, string | number | undefined> | undefined;
|
|
32
|
+
status?: number | undefined;
|
|
33
|
+
url?: string | undefined;
|
|
34
|
+
}
|
|
29
35
|
export interface GithubToolClient {
|
|
30
|
-
request(route: string, parameters: Record<string, unknown>): Promise<
|
|
31
|
-
|
|
32
|
-
}>;
|
|
36
|
+
request(route: string, parameters: Record<string, unknown>): Promise<GithubToolResponse>;
|
|
37
|
+
graphql?: ((query: string, variables: Record<string, unknown>) => Promise<unknown>) | undefined;
|
|
33
38
|
}
|
|
34
39
|
export type GithubToolClientFactory = (token: string) => GithubToolClient;
|
|
40
|
+
export declare function githubOperationRoute(toolId: GithubAgentToolId, method: string | undefined, args: Record<string, unknown>): string | undefined;
|
|
41
|
+
export declare function projectGithubOperationParameters(toolId: GithubAgentToolId, method: string | undefined, args: Record<string, unknown>): Record<string, unknown>;
|
|
35
42
|
//# sourceMappingURL=agent-tools.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-tools.d.ts","sourceRoot":"","sources":["../../src/core/agent-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,yBAAyB,EACzB,gBAAgB,EAChB,kBAAkB,EAClB,qBAAqB,EACrB,0BAA0B,EAC3B,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-tools.d.ts","sourceRoot":"","sources":["../../src/core/agent-tools.ts"],"names":[],"mappings":"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"}
|
package/dist/core/agent-tools.js
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import { Octokit } from 'octokit';
|
|
2
|
+
import { mapGithubError } from '#api/client.js';
|
|
2
3
|
import { createGithubInstallationTokenProvider } from '#api/installation-token-provider.js';
|
|
3
|
-
import { normalizedGithubApiBaseUrl } from '#config.js';
|
|
4
|
+
import { config, normalizedGithubApiBaseUrl } from '#config.js';
|
|
4
5
|
import { GithubIntegrationProviderError } from './errors.js';
|
|
5
6
|
import { githubAgentToolCatalog, githubAgentToolSelectionCatalog } from './github-agent-tool-catalog.js';
|
|
6
7
|
export { buildGithubAgentToolSelectionCatalog, DEFAULT_JOB_LOG_TAIL_LINES, githubAgentToolCatalog, githubAgentToolSelectionCatalog } from './github-agent-tool-catalog.js';
|
|
8
|
+
const GITHUB_GRAPHQL_ROUTE = 'POST /graphql';
|
|
9
|
+
const GITHUB_ARTIFACT_ARCHIVE_FORMAT = 'zip';
|
|
10
|
+
const GITHUB_ARTIFACT_DOWNLOAD_ROUTE = `GET /repos/{owner}/{repo}/actions/artifacts/{resource_id}/${GITHUB_ARTIFACT_ARCHIVE_FORMAT}`;
|
|
11
|
+
const GITHUB_ARTIFACT_DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
12
|
+
const GITHUB_APP_BOT_SUFFIX = '[bot]';
|
|
13
|
+
const PENDING_REVIEW_PAGE_SIZE = 100;
|
|
14
|
+
const PENDING_REVIEW_MAX_PAGE_REQUESTS = 5;
|
|
15
|
+
const PENDING_REVIEW_LOOKUP_TIMEOUT_MS = 15_000;
|
|
16
|
+
const PENDING_REVIEW_PAGE_TIMEOUT_MS = 5_000;
|
|
17
|
+
const PENDING_REVIEW_PAGE_PATTERN = /[?&]page=(\d+)/u;
|
|
18
|
+
const NO_PENDING_REVIEW_MESSAGE = 'No pending pull request review found for the authenticated GitHub user.';
|
|
19
|
+
const ADD_PENDING_REVIEW_COMMENT_MUTATION = `
|
|
20
|
+
mutation AddCommentToPendingReview($input: AddPullRequestReviewThreadInput!) {
|
|
21
|
+
addPullRequestReviewThread(input: $input) {
|
|
22
|
+
thread {
|
|
23
|
+
id
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
7
28
|
export class GithubAgentToolsProvider {
|
|
8
29
|
constructor(options = {}){
|
|
9
30
|
this.options = options;
|
|
@@ -28,24 +49,28 @@ export class GithubAgentToolsProvider {
|
|
|
28
49
|
return {
|
|
29
50
|
call: async (call)=>{
|
|
30
51
|
const tool = input.tools.find((candidate)=>candidate.id === call.toolId);
|
|
31
|
-
if (!tool) return githubToolError(`Unknown GitHub tool: ${call.toolId}
|
|
52
|
+
if (!tool) return githubToolError(`Unknown GitHub tool: ${call.toolId}`, 'invalid-request');
|
|
32
53
|
const operation = resolveGithubOperation(tool, call);
|
|
33
|
-
if (operation === undefined) return githubToolError('Unknown GitHub tool operation');
|
|
54
|
+
if (operation === undefined) return githubToolError('Unknown GitHub tool operation', 'invalid-request');
|
|
34
55
|
const validationError = validateGithubToolArguments(tool, call.arguments);
|
|
35
|
-
if (validationError) return githubToolError(validationError);
|
|
56
|
+
if (validationError) return githubToolError(validationError, 'invalid-request');
|
|
36
57
|
tokenPromise ??= this.tokenProvider.getInstallationAccessToken(installationId);
|
|
37
58
|
const token = await tokenPromise;
|
|
38
59
|
if (!hasGrantedPermissions(token.permissions ?? {}, tool, call)) {
|
|
39
|
-
return githubToolError('GitHub installation token is missing permission for this operation');
|
|
60
|
+
return githubToolError('GitHub installation token is missing permission for this operation', 'access-denied');
|
|
40
61
|
}
|
|
41
62
|
const client = (this.options.createClient ?? createOctokitClient)(token.token);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
const method = typeof call.arguments.method === 'string' ? call.arguments.method : undefined;
|
|
64
|
+
if (operation.kind === 'graphql') {
|
|
65
|
+
const data = await mapGithubError(()=>addCommentToPendingReview(client, operation.parameters));
|
|
66
|
+
return data === undefined ? githubToolError(NO_PENDING_REVIEW_MESSAGE, 'provider-rejected') : githubToolResult(tool.id, data);
|
|
67
|
+
}
|
|
68
|
+
const operationParameters = await mapGithubError(()=>resolvePendingReviewParameters(client, operation.parameters, tool.id, method));
|
|
69
|
+
if (operationParameters === undefined) {
|
|
70
|
+
return githubToolError(NO_PENDING_REVIEW_MESSAGE, 'provider-rejected');
|
|
48
71
|
}
|
|
72
|
+
const response = await mapGithubError(()=>client.request(operation.route, operationParameters));
|
|
73
|
+
return githubToolResult(tool.id, response.data, response, operationParameters, operation.route);
|
|
49
74
|
}
|
|
50
75
|
};
|
|
51
76
|
}
|
|
@@ -59,7 +84,26 @@ function createOctokitClient(token) {
|
|
|
59
84
|
}
|
|
60
85
|
});
|
|
61
86
|
return {
|
|
62
|
-
request: async (route, parameters)=>
|
|
87
|
+
request: async (route, parameters)=>{
|
|
88
|
+
if (route !== GITHUB_ARTIFACT_DOWNLOAD_ROUTE) {
|
|
89
|
+
return await octokit.request(route, parameters);
|
|
90
|
+
}
|
|
91
|
+
const abortController = new AbortController();
|
|
92
|
+
const timeout = setTimeout(()=>abortController.abort(), GITHUB_ARTIFACT_DOWNLOAD_TIMEOUT_MS);
|
|
93
|
+
try {
|
|
94
|
+
return await octokit.request(route, {
|
|
95
|
+
...parameters,
|
|
96
|
+
request: {
|
|
97
|
+
redirect: 'manual',
|
|
98
|
+
parseSuccessResponseBody: false,
|
|
99
|
+
signal: abortController.signal
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
} finally{
|
|
103
|
+
clearTimeout(timeout);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
graphql: async (query, variables)=>await octokit.graphql(query, variables)
|
|
63
107
|
};
|
|
64
108
|
}
|
|
65
109
|
function resolveGithubOperation(tool, call) {
|
|
@@ -74,10 +118,11 @@ function resolveGithubOperation(tool, call) {
|
|
|
74
118
|
const route = githubOperationRoute(toolId, method, params);
|
|
75
119
|
return route === undefined ? undefined : {
|
|
76
120
|
route,
|
|
77
|
-
parameters: projectGithubOperationParameters(toolId, method, params)
|
|
121
|
+
parameters: projectGithubOperationParameters(toolId, method, params),
|
|
122
|
+
kind: route === GITHUB_GRAPHQL_ROUTE ? 'graphql' : 'rest'
|
|
78
123
|
};
|
|
79
124
|
}
|
|
80
|
-
function githubOperationRoute(toolId, method, args) {
|
|
125
|
+
export function githubOperationRoute(toolId, method, args) {
|
|
81
126
|
const owner = '{owner}';
|
|
82
127
|
const repo = '{repo}';
|
|
83
128
|
const issue = '{issue_number}';
|
|
@@ -114,7 +159,7 @@ function githubOperationRoute(toolId, method, args) {
|
|
|
114
159
|
case 'sub_issue_write.remove':
|
|
115
160
|
return `DELETE ${repoPath}/issues/${issue}/sub_issues/{sub_issue_id}`;
|
|
116
161
|
case 'sub_issue_write.reprioritize':
|
|
117
|
-
return `PATCH ${repoPath}/issues/${issue}/sub_issues/
|
|
162
|
+
return `PATCH ${repoPath}/issues/${issue}/sub_issues/priority`;
|
|
118
163
|
case 'pull_request_read.get':
|
|
119
164
|
return `GET ${repoPath}/pulls/${pull}`;
|
|
120
165
|
case 'pull_request_read.get_diff':
|
|
@@ -142,7 +187,7 @@ function githubOperationRoute(toolId, method, args) {
|
|
|
142
187
|
case 'update_pull_request.':
|
|
143
188
|
return `PATCH ${repoPath}/pulls/${pull}`;
|
|
144
189
|
case 'add_reply_to_pull_request_comment.':
|
|
145
|
-
return `POST ${repoPath}/pulls/{comment_id}/replies`;
|
|
190
|
+
return args.reaction !== undefined && args.body === undefined ? `POST ${repoPath}/pulls/comments/{comment_id}/reactions` : `POST ${repoPath}/pulls/${pull}/comments/{comment_id}/replies`;
|
|
146
191
|
case 'merge_pull_request.':
|
|
147
192
|
return `PUT ${repoPath}/pulls/${pull}/merge`;
|
|
148
193
|
case 'update_pull_request_branch.':
|
|
@@ -154,7 +199,7 @@ function githubOperationRoute(toolId, method, args) {
|
|
|
154
199
|
case 'pull_request_review_write.delete_pending':
|
|
155
200
|
return `DELETE ${repoPath}/pulls/${pull}/reviews/{review_id}`;
|
|
156
201
|
case 'add_comment_to_pending_review.':
|
|
157
|
-
return
|
|
202
|
+
return GITHUB_GRAPHQL_ROUTE;
|
|
158
203
|
case 'actions_list.list_workflows':
|
|
159
204
|
return `GET ${repoPath}/actions/workflows`;
|
|
160
205
|
case 'actions_list.list_workflow_runs':
|
|
@@ -170,7 +215,7 @@ function githubOperationRoute(toolId, method, args) {
|
|
|
170
215
|
case 'actions_get.get_workflow_job':
|
|
171
216
|
return `GET ${repoPath}/actions/jobs/${resource}`;
|
|
172
217
|
case 'actions_get.download_workflow_run_artifact':
|
|
173
|
-
return
|
|
218
|
+
return GITHUB_ARTIFACT_DOWNLOAD_ROUTE;
|
|
174
219
|
case 'actions_get.get_workflow_run_usage':
|
|
175
220
|
return `GET ${repoPath}/actions/runs/${resource}/timing`;
|
|
176
221
|
case 'actions_get.get_workflow_run_logs_url':
|
|
@@ -191,7 +236,30 @@ function githubOperationRoute(toolId, method, args) {
|
|
|
191
236
|
return undefined;
|
|
192
237
|
}
|
|
193
238
|
}
|
|
194
|
-
function
|
|
239
|
+
async function addCommentToPendingReview(client, args) {
|
|
240
|
+
if (client.graphql === undefined) {
|
|
241
|
+
throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub client does not support GraphQL operations');
|
|
242
|
+
}
|
|
243
|
+
const review = await latestPendingReview(client, args, 'nodeId');
|
|
244
|
+
if (review === undefined) return undefined;
|
|
245
|
+
if (review.nodeId === undefined) {
|
|
246
|
+
throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub pending pull request review did not include a node ID');
|
|
247
|
+
}
|
|
248
|
+
const input = {
|
|
249
|
+
pullRequestReviewId: review.nodeId,
|
|
250
|
+
path: args.path,
|
|
251
|
+
body: args.body,
|
|
252
|
+
subjectType: args.subject_type
|
|
253
|
+
};
|
|
254
|
+
if (args.line !== undefined) input.line = args.line;
|
|
255
|
+
if (args.side !== undefined) input.side = args.side;
|
|
256
|
+
if (args.start_line !== undefined) input.startLine = args.start_line;
|
|
257
|
+
if (args.start_side !== undefined) input.startSide = args.start_side;
|
|
258
|
+
return await client.graphql(ADD_PENDING_REVIEW_COMMENT_MUTATION, {
|
|
259
|
+
input
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
export function projectGithubOperationParameters(toolId, method, args) {
|
|
195
263
|
const parameters = {
|
|
196
264
|
...args
|
|
197
265
|
};
|
|
@@ -207,8 +275,129 @@ function projectGithubOperationParameters(toolId, method, args) {
|
|
|
207
275
|
}
|
|
208
276
|
return parameters;
|
|
209
277
|
}
|
|
210
|
-
function
|
|
211
|
-
|
|
278
|
+
async function resolvePendingReviewParameters(client, parameters, toolId, method) {
|
|
279
|
+
if (!isPendingReviewOperation(toolId, method)) return parameters;
|
|
280
|
+
const review = await latestPendingReview(client, parameters, 'id');
|
|
281
|
+
if (review === undefined) return undefined;
|
|
282
|
+
if (review.id === undefined) {
|
|
283
|
+
throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub pending pull request review did not include a numeric ID');
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
...parameters,
|
|
287
|
+
review_id: review.id
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function isPendingReviewOperation(toolId, method) {
|
|
291
|
+
return toolId === 'pull_request_review_write' && (method === 'submit_pending' || method === 'delete_pending');
|
|
292
|
+
}
|
|
293
|
+
async function latestPendingReview(client, parameters, requiredIdentifier) {
|
|
294
|
+
const lookupController = new AbortController();
|
|
295
|
+
const lookupTimeout = setTimeout(()=>lookupController.abort(), PENDING_REVIEW_LOOKUP_TIMEOUT_MS);
|
|
296
|
+
try {
|
|
297
|
+
return await latestPendingReviewBeforeDeadline(client, parameters, requiredIdentifier, lookupController.signal);
|
|
298
|
+
} finally{
|
|
299
|
+
clearTimeout(lookupTimeout);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async function latestPendingReviewBeforeDeadline(client, parameters, requiredIdentifier, lookupSignal) {
|
|
303
|
+
const firstPage = await requestPendingReviewPage(client, parameters, 1, lookupSignal);
|
|
304
|
+
const lastPage = pendingReviewLastPage(firstPage.headers);
|
|
305
|
+
let requests = 1;
|
|
306
|
+
let malformed = false;
|
|
307
|
+
for(let page = lastPage; page >= 1; page -= 1){
|
|
308
|
+
let response = firstPage;
|
|
309
|
+
if (page !== 1) {
|
|
310
|
+
if (requests >= PENDING_REVIEW_MAX_PAGE_REQUESTS) {
|
|
311
|
+
throw new GithubIntegrationProviderError('content-too-large', 'GitHub pull request review history exceeded the pending review lookup limit');
|
|
312
|
+
}
|
|
313
|
+
response = await requestPendingReviewPage(client, parameters, page, lookupSignal);
|
|
314
|
+
requests += 1;
|
|
315
|
+
}
|
|
316
|
+
if (!Array.isArray(response.data)) {
|
|
317
|
+
throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub pull request review list response was malformed');
|
|
318
|
+
}
|
|
319
|
+
const result = latestPendingReviewOnPage(response.data, requiredIdentifier);
|
|
320
|
+
if (result.review !== undefined) return result.review;
|
|
321
|
+
malformed ||= result.malformed;
|
|
322
|
+
}
|
|
323
|
+
if (malformed) {
|
|
324
|
+
throw new GithubIntegrationProviderError('malformed-provider-response', requiredIdentifier === 'nodeId' ? 'GitHub pending pull request review did not include a node ID' : 'GitHub pending pull request review did not include a numeric ID');
|
|
325
|
+
}
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
async function requestPendingReviewPage(client, parameters, page, lookupSignal) {
|
|
329
|
+
const pageController = new AbortController();
|
|
330
|
+
const abortPage = ()=>pageController.abort();
|
|
331
|
+
if (lookupSignal.aborted) abortPage();
|
|
332
|
+
else lookupSignal.addEventListener('abort', abortPage, {
|
|
333
|
+
once: true
|
|
334
|
+
});
|
|
335
|
+
const pageTimeout = setTimeout(abortPage, PENDING_REVIEW_PAGE_TIMEOUT_MS);
|
|
336
|
+
try {
|
|
337
|
+
return await client.request('GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews', {
|
|
338
|
+
owner: parameters.owner,
|
|
339
|
+
repo: parameters.repo,
|
|
340
|
+
pull_number: parameters.pull_number,
|
|
341
|
+
per_page: PENDING_REVIEW_PAGE_SIZE,
|
|
342
|
+
page,
|
|
343
|
+
request: {
|
|
344
|
+
signal: pageController.signal
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
} finally{
|
|
348
|
+
clearTimeout(pageTimeout);
|
|
349
|
+
lookupSignal.removeEventListener('abort', abortPage);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function pendingReviewLastPage(headers) {
|
|
353
|
+
const link = headers?.link;
|
|
354
|
+
if (typeof link !== 'string') return 1;
|
|
355
|
+
const lastLink = link.split(',').find((part)=>part.includes('rel="last"'));
|
|
356
|
+
if (lastLink === undefined) return 1;
|
|
357
|
+
const match = PENDING_REVIEW_PAGE_PATTERN.exec(lastLink);
|
|
358
|
+
const page = match?.[1] === undefined ? Number.NaN : Number.parseInt(match[1], 10);
|
|
359
|
+
if (!Number.isSafeInteger(page) || page < 1) {
|
|
360
|
+
throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub pull request review pagination response was malformed');
|
|
361
|
+
}
|
|
362
|
+
return page;
|
|
363
|
+
}
|
|
364
|
+
function latestPendingReviewOnPage(data, requiredIdentifier) {
|
|
365
|
+
let malformed = false;
|
|
366
|
+
const appLogin = githubAppBotLogin().toLowerCase();
|
|
367
|
+
for(let index = data.length - 1; index >= 0; index -= 1){
|
|
368
|
+
const review = data[index];
|
|
369
|
+
if (!isRecord(review) || review.state !== 'PENDING') continue;
|
|
370
|
+
const userLogin = isRecord(review.user) ? review.user.login : undefined;
|
|
371
|
+
if (typeof userLogin !== 'string' || userLogin.trim().length === 0) {
|
|
372
|
+
malformed = true;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (userLogin.trim().toLowerCase() !== appLogin) continue;
|
|
376
|
+
const id = typeof review.id === 'number' && Number.isSafeInteger(review.id) && review.id > 0 ? review.id : undefined;
|
|
377
|
+
const nodeId = typeof review.node_id === 'string' && review.node_id.trim().length > 0 ? review.node_id.trim() : undefined;
|
|
378
|
+
const reference = {
|
|
379
|
+
id,
|
|
380
|
+
nodeId
|
|
381
|
+
};
|
|
382
|
+
if (reference[requiredIdentifier] !== undefined) return {
|
|
383
|
+
malformed,
|
|
384
|
+
review: reference
|
|
385
|
+
};
|
|
386
|
+
malformed = true;
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
malformed
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function githubAppBotLogin() {
|
|
393
|
+
const configuredUsername = config.GITHUB_APP_USERNAME?.trim() || config.GITHUB_APP_SLUG.trim();
|
|
394
|
+
return configuredUsername.toLowerCase().endsWith(GITHUB_APP_BOT_SUFFIX) ? configuredUsername : `${configuredUsername}${GITHUB_APP_BOT_SUFFIX}`;
|
|
395
|
+
}
|
|
396
|
+
function githubToolResult(toolId, data, response, parameters, route) {
|
|
397
|
+
const structuredContent = projectGithubToolOutput(toolId, data, response, parameters, route);
|
|
398
|
+
if (structuredContent === undefined) {
|
|
399
|
+
return githubToolError('GitHub artifact download did not return a download URL', 'malformed-provider-response');
|
|
400
|
+
}
|
|
212
401
|
return {
|
|
213
402
|
content: [
|
|
214
403
|
{
|
|
@@ -219,7 +408,10 @@ function githubToolResult(toolId, data) {
|
|
|
219
408
|
structuredContent
|
|
220
409
|
};
|
|
221
410
|
}
|
|
222
|
-
function projectGithubToolOutput(toolId, data) {
|
|
411
|
+
function projectGithubToolOutput(toolId, data, response, parameters, route) {
|
|
412
|
+
if (route === GITHUB_ARTIFACT_DOWNLOAD_ROUTE) {
|
|
413
|
+
return projectGithubArtifactDownloadOutput(response, parameters);
|
|
414
|
+
}
|
|
223
415
|
switch(toolId){
|
|
224
416
|
case 'list_issue_types':
|
|
225
417
|
return {
|
|
@@ -256,10 +448,26 @@ function projectGithubToolOutput(toolId, data) {
|
|
|
256
448
|
};
|
|
257
449
|
}
|
|
258
450
|
}
|
|
451
|
+
function projectGithubArtifactDownloadOutput(response, parameters) {
|
|
452
|
+
if (response === undefined) return undefined;
|
|
453
|
+
const downloadUrl = response.headers?.location;
|
|
454
|
+
if (typeof downloadUrl !== 'string' || downloadUrl.length === 0) return undefined;
|
|
455
|
+
const output = {
|
|
456
|
+
archive_format: GITHUB_ARTIFACT_ARCHIVE_FORMAT,
|
|
457
|
+
download_url: downloadUrl
|
|
458
|
+
};
|
|
459
|
+
if (typeof parameters?.resource_id === 'string') output.artifact_id = parameters.resource_id;
|
|
460
|
+
const contentType = response.headers?.['content-type'];
|
|
461
|
+
if (typeof contentType === 'string') output.content_type = contentType;
|
|
462
|
+
const contentLength = response.headers?.['content-length'];
|
|
463
|
+
const sizeBytes = typeof contentLength === 'number' ? contentLength : Number(contentLength);
|
|
464
|
+
if (Number.isSafeInteger(sizeBytes) && sizeBytes >= 0) output.size_bytes = sizeBytes;
|
|
465
|
+
return output;
|
|
466
|
+
}
|
|
259
467
|
function githubSearchItems(data) {
|
|
260
468
|
return isRecord(data) ? data.items : data;
|
|
261
469
|
}
|
|
262
|
-
function githubToolError(message) {
|
|
470
|
+
function githubToolError(message, code) {
|
|
263
471
|
return {
|
|
264
472
|
isError: true,
|
|
265
473
|
content: [
|
|
@@ -267,7 +475,10 @@ function githubToolError(message) {
|
|
|
267
475
|
type: 'text',
|
|
268
476
|
text: message
|
|
269
477
|
}
|
|
270
|
-
]
|
|
478
|
+
],
|
|
479
|
+
structuredContent: {
|
|
480
|
+
code
|
|
481
|
+
}
|
|
271
482
|
};
|
|
272
483
|
}
|
|
273
484
|
function isRecord(value) {
|