From 1cd90071b345a1bce0cad38a7e357a8aacaafc63 Mon Sep 17 00:00:00 2001 From: msukkari Date: Tue, 21 Oct 2025 18:00:45 -0700 Subject: [PATCH] github app service auth --- docs/snippets/schemas/v3/app.schema.mdx | 114 +++++++++++++++ docs/snippets/schemas/v3/githubApp.schema.mdx | 108 ++++++++++++++ packages/backend/src/ee/githubAppManager.ts | 133 ++++++++++++++++++ .../backend/src/ee/repoPermissionSyncer.ts | 1 - packages/backend/src/github.ts | 57 ++++++-- packages/backend/src/index.ts | 6 + packages/backend/src/utils.ts | 24 ++++ packages/schemas/src/v3/app.schema.ts | 113 +++++++++++++++ packages/schemas/src/v3/app.type.ts | 6 + packages/schemas/src/v3/githubApp.schema.ts | 107 ++++++++++++++ packages/schemas/src/v3/githubApp.type.ts | 50 +++++++ schemas/v3/app.json | 9 ++ schemas/v3/githubApp.json | 47 +++++++ 13 files changed, 765 insertions(+), 10 deletions(-) create mode 100644 docs/snippets/schemas/v3/app.schema.mdx create mode 100644 docs/snippets/schemas/v3/githubApp.schema.mdx create mode 100644 packages/backend/src/ee/githubAppManager.ts create mode 100644 packages/schemas/src/v3/app.schema.ts create mode 100644 packages/schemas/src/v3/app.type.ts create mode 100644 packages/schemas/src/v3/githubApp.schema.ts create mode 100644 packages/schemas/src/v3/githubApp.type.ts create mode 100644 schemas/v3/app.json create mode 100644 schemas/v3/githubApp.json diff --git a/docs/snippets/schemas/v3/app.schema.mdx b/docs/snippets/schemas/v3/app.schema.mdx new file mode 100644 index 00000000..391ac210 --- /dev/null +++ b/docs/snippets/schemas/v3/app.schema.mdx @@ -0,0 +1,114 @@ +{/* THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! */} +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AppConfig", + "oneOf": [ + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "GithubAppConfig", + "properties": { + "type": { + "const": "githubApp", + "description": "GitHub App Configuration" + }, + "deploymentHostname": { + "type": "string", + "format": "url", + "default": "github.com", + "description": "The hostname of the GitHub App deployment.", + "examples": [ + "github.com", + "github.example.com" + ], + "pattern": "^[^\\s/$.?#].[^\\s]*$" + }, + "id": { + "type": "string", + "description": "The ID of the GitHub App." + }, + "privateKey": { + "description": "The private key of the GitHub App.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "privateKeyPath": { + "description": "The path to the private key of the GitHub App.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "id" + ], + "oneOf": [ + { + "required": [ + "privateKey" + ] + }, + { + "required": [ + "privateKeyPath" + ] + } + ], + "additionalProperties": false + } + ] +} +``` diff --git a/docs/snippets/schemas/v3/githubApp.schema.mdx b/docs/snippets/schemas/v3/githubApp.schema.mdx new file mode 100644 index 00000000..4ad5eb4c --- /dev/null +++ b/docs/snippets/schemas/v3/githubApp.schema.mdx @@ -0,0 +1,108 @@ +{/* THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! */} +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "GithubAppConfig", + "properties": { + "type": { + "const": "githubApp", + "description": "GitHub App Configuration" + }, + "deploymentHostname": { + "type": "string", + "format": "url", + "default": "github.com", + "description": "The hostname of the GitHub App deployment.", + "examples": [ + "github.com", + "github.example.com" + ], + "pattern": "^[^\\s/$.?#].[^\\s]*$" + }, + "id": { + "type": "string", + "description": "The ID of the GitHub App." + }, + "privateKey": { + "description": "The private key of the GitHub App.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "privateKeyPath": { + "description": "The path to the private key of the GitHub App.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "id" + ], + "oneOf": [ + { + "required": [ + "privateKey" + ] + }, + { + "required": [ + "privateKeyPath" + ] + } + ], + "additionalProperties": false +} +``` diff --git a/packages/backend/src/ee/githubAppManager.ts b/packages/backend/src/ee/githubAppManager.ts new file mode 100644 index 00000000..71755a55 --- /dev/null +++ b/packages/backend/src/ee/githubAppManager.ts @@ -0,0 +1,133 @@ +import { GithubAppConfig, SourcebotConfig } from "@sourcebot/schemas/v3/index.type"; +import { loadConfig } from "@sourcebot/shared"; +import { env } from "../env.js"; +import { createLogger } from "@sourcebot/logger"; +import { getTokenFromConfig } from "../utils.js"; +import { PrismaClient } from "@sourcebot/db"; +import { App } from "@octokit/app"; + +const logger = createLogger('githubAppManager'); +const GITHUB_DEFAULT_DEPLOYMENT_HOSTNAME = 'github.com'; + +type Installation = { + id: number; + appId: number; + account: { + login: string; + type: 'organization' | 'user'; + }; + createdAt: string; + expiresAt: string; + token: string; +}; + +export class GithubAppManager { + private static instance: GithubAppManager | null = null; + private octokitApps: Map; + private installationMap: Map; + private db: PrismaClient | null = null; + private initialized: boolean = false; + + private constructor() { + this.octokitApps = new Map(); + this.installationMap = new Map(); + } + + public static getInstance(): GithubAppManager { + if (!GithubAppManager.instance) { + GithubAppManager.instance = new GithubAppManager(); + } + return GithubAppManager.instance; + } + + private ensureInitialized(): void { + if (!this.initialized) { + throw new Error('GithubAppManager must be initialized before use. Call init() first.'); + } + } + + public async init(db: PrismaClient) { + this.db = db; + const config = await loadConfig(env.CONFIG_PATH!); + const githubApps = config.apps?.filter(app => app.type === 'githubApp') as GithubAppConfig[]; + + logger.info(`Found ${githubApps.length} GitHub apps in config`); + + for (const app of githubApps) { + const deploymentHostname = app.deploymentHostname as string || GITHUB_DEFAULT_DEPLOYMENT_HOSTNAME; + + // @todo: we should move SINGLE_TENANT_ORG_ID to shared package or just remove the need to pass this in + // when resolving tokens + const SINGLE_TENANT_ORG_ID = 1; + const privateKey = await getTokenFromConfig(app.privateKey, SINGLE_TENANT_ORG_ID, this.db!); + + const octokitApp = new App({ + appId: Number(app.id), + privateKey: privateKey, + }); + this.octokitApps.set(Number(app.id), octokitApp); + + const installations = await octokitApp.octokit.request("GET /app/installations"); + logger.info(`Found ${installations.data.length} GitHub App installations for ${deploymentHostname}/${app.id}:`); + + for (const installationData of installations.data) { + logger.info(`\tInstallation ID: ${installationData.id}, Account: ${installationData.account?.login}, Type: ${installationData.account?.type}`); + + const owner = installationData.account?.login!; + const accountType = installationData.account?.type!.toLowerCase() as 'organization' | 'user'; + const installationOctokit = await octokitApp.getInstallationOctokit(installationData.id); + const auth = await installationOctokit.auth({ type: "installation" }) as { expires_at: string, token: string }; + + const installation: Installation = { + id: installationData.id, + appId: Number(app.id), + account: { + login: owner, + type: accountType, + }, + createdAt: installationData.created_at, + expiresAt: auth.expires_at, + token: auth.token + }; + this.installationMap.set(this.generateMapKey(owner, deploymentHostname), installation); + } + } + + this.initialized = true; + } + + public async getInstallationToken(owner: string, deploymentHostname: string = GITHUB_DEFAULT_DEPLOYMENT_HOSTNAME): Promise { + this.ensureInitialized(); + + const key = this.generateMapKey(owner, deploymentHostname); + const installation = this.installationMap.get(key) as Installation | undefined; + if (!installation) { + throw new Error(`GitHub App Installation not found for ${key}`); + } + + if (installation.expiresAt < new Date().toISOString()) { + const octokitApp = this.octokitApps.get(installation.appId) as App; + const installationOctokit = await octokitApp.getInstallationOctokit(installation.id); + const auth = await installationOctokit.auth({ type: "installation" }) as { expires_at: string, token: string }; + + const newInstallation: Installation = { + ...installation, + expiresAt: auth.expires_at, + token: auth.token + }; + this.installationMap.set(key, newInstallation); + + return newInstallation.token; + } else { + return installation.token; + } + } + + public appsConfigured() { + return this.octokitApps.size > 0; + } + + private generateMapKey(owner: string, deploymentHostname: string): string { + return `${deploymentHostname}/${owner}`; + } +} \ No newline at end of file diff --git a/packages/backend/src/ee/repoPermissionSyncer.ts b/packages/backend/src/ee/repoPermissionSyncer.ts index f411c3e3..9acf6009 100644 --- a/packages/backend/src/ee/repoPermissionSyncer.ts +++ b/packages/backend/src/ee/repoPermissionSyncer.ts @@ -18,7 +18,6 @@ const QUEUE_NAME = 'repoPermissionSyncQueue'; const logger = createLogger('repo-permission-syncer'); - export class RepoPermissionSyncer { private queue: Queue; private worker: Worker; diff --git a/packages/backend/src/github.ts b/packages/backend/src/github.ts index 2b42eed2..19752914 100644 --- a/packages/backend/src/github.ts +++ b/packages/backend/src/github.ts @@ -8,6 +8,8 @@ import { BackendException, BackendError } from "@sourcebot/error"; import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js"; import * as Sentry from "@sentry/node"; import { env } from "./env.js"; +import { GithubAppManager } from "./ee/githubAppManager.js"; +import { hasEntitlement } from "@sourcebot/shared"; const logger = createLogger('github'); const GITHUB_CLOUD_HOSTNAME = "github.com"; @@ -55,6 +57,40 @@ export const createOctokitFromToken = async ({ token, url }: { token?: string, u }; } +/** + * Helper function to get an authenticated Octokit instance using GitHub App if available, + * otherwise falls back to the provided octokit instance. + */ +const getOctokitWithGithubApp = async ( + octokit: Octokit, + owner: string, + url: string | undefined, + context: string +): Promise => { + if (!hasEntitlement('github-app') || !GithubAppManager.getInstance().appsConfigured()) { + return octokit; + } + + try { + const hostname = url ? new URL(url).hostname : GITHUB_CLOUD_HOSTNAME; + const token = await GithubAppManager.getInstance().getInstallationToken(owner, hostname); + const { octokit: octokitFromToken, isAuthenticated } = await createOctokitFromToken({ + token, + url, + }); + + if (isAuthenticated) { + return octokitFromToken; + } else { + logger.error(`Failed to authenticate with GitHub App for ${context}. Falling back to legacy token resolution.`); + return octokit; + } + } catch (error) { + logger.error(`Error getting GitHub App token for ${context}. Falling back to legacy token resolution.`, error); + return octokit; + } +} + export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, orgId: number, db: PrismaClient, signal: AbortSignal) => { const hostname = config.url ? new URL(config.url).hostname : @@ -107,19 +143,19 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, o }; if (config.orgs) { - const { validRepos, notFoundOrgs } = await getReposForOrgs(config.orgs, octokit, signal); + const { validRepos, notFoundOrgs } = await getReposForOrgs(config.orgs, octokit, signal, config.url); allRepos = allRepos.concat(validRepos); notFound.orgs = notFoundOrgs; } if (config.repos) { - const { validRepos, notFoundRepos } = await getRepos(config.repos, octokit, signal); + const { validRepos, notFoundRepos } = await getRepos(config.repos, octokit, signal, config.url); allRepos = allRepos.concat(validRepos); notFound.repos = notFoundRepos; } if (config.users) { - const { validRepos, notFoundUsers } = await getReposOwnedByUsers(config.users, octokit, signal); + const { validRepos, notFoundUsers } = await getReposOwnedByUsers(config.users, octokit, signal, config.url); allRepos = allRepos.concat(validRepos); notFound.users = notFoundUsers; } @@ -178,11 +214,12 @@ export const getReposForAuthenticatedUser = async (visibility: 'all' | 'private' } } -const getReposOwnedByUsers = async (users: string[], octokit: Octokit, signal: AbortSignal) => { +const getReposOwnedByUsers = async (users: string[], octokit: Octokit, signal: AbortSignal, url?: string) => { const results = await Promise.allSettled(users.map(async (user) => { try { logger.debug(`Fetching repository info for user ${user}...`); + const octokitToUse = await getOctokitWithGithubApp(octokit, user, url, `user ${user}`); const { durationMs, data } = await measure(async () => { const fetchFn = async () => { let query = `user:${user}`; @@ -194,7 +231,7 @@ const getReposOwnedByUsers = async (users: string[], octokit: Octokit, signal: A // the username as a parameter. // @see: https://github.com/orgs/community/discussions/24382#discussioncomment-3243958 // @see: https://api.github.com/search/repositories?q=user:USERNAME - const searchResults = await octokit.paginate(octokit.rest.search.repos, { + const searchResults = await octokitToUse.paginate(octokitToUse.rest.search.repos, { q: query, per_page: 100, request: { @@ -237,13 +274,14 @@ const getReposOwnedByUsers = async (users: string[], octokit: Octokit, signal: A }; } -const getReposForOrgs = async (orgs: string[], octokit: Octokit, signal: AbortSignal) => { +const getReposForOrgs = async (orgs: string[], octokit: Octokit, signal: AbortSignal, url?: string) => { const results = await Promise.allSettled(orgs.map(async (org) => { try { logger.info(`Fetching repository info for org ${org}...`); + const octokitToUse = await getOctokitWithGithubApp(octokit, org, url, `org ${org}`); const { durationMs, data } = await measure(async () => { - const fetchFn = () => octokit.paginate(octokit.repos.listForOrg, { + const fetchFn = () => octokitToUse.paginate(octokitToUse.repos.listForOrg, { org: org, per_page: 100, request: { @@ -283,14 +321,15 @@ const getReposForOrgs = async (orgs: string[], octokit: Octokit, signal: AbortSi }; } -const getRepos = async (repoList: string[], octokit: Octokit, signal: AbortSignal) => { +const getRepos = async (repoList: string[], octokit: Octokit, signal: AbortSignal, url?: string) => { const results = await Promise.allSettled(repoList.map(async (repo) => { try { const [owner, repoName] = repo.split('/'); logger.info(`Fetching repository info for ${repo}...`); + const octokitToUse = await getOctokitWithGithubApp(octokit, owner, url, `repo ${repo}`); const { durationMs, data: result } = await measure(async () => { - const fetchFn = () => octokit.repos.get({ + const fetchFn = () => octokitToUse.repos.get({ owner, repo: repoName, request: { diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 93f95e0b..a8d00944 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -15,6 +15,7 @@ import { PromClient } from './promClient.js'; import { RepoManager } from './repoManager.js'; import { AppContext } from "./types.js"; import { UserPermissionSyncer } from "./ee/userPermissionSyncer.js"; +import { GithubAppManager } from "./ee/githubAppManager.js"; const logger = createLogger('backend-entrypoint'); @@ -67,6 +68,11 @@ const promClient = new PromClient(); const settings = await getSettings(env.CONFIG_PATH); + +if (hasEntitlement('github-app')) { + await GithubAppManager.getInstance().init(prisma); +} + const connectionManager = new ConnectionManager(prisma, settings, redis); const repoManager = new RepoManager(prisma, settings, redis, promClient, context); const repoPermissionSyncer = new RepoPermissionSyncer(prisma, settings, redis); diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index e6ac5f93..8efe3750 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -6,6 +6,8 @@ import { getTokenFromConfig as getTokenFromConfigBase } from "@sourcebot/crypto" import { BackendException, BackendError } from "@sourcebot/error"; import * as Sentry from "@sentry/node"; import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig, BitbucketConnectionConfig, AzureDevOpsConnectionConfig } from '@sourcebot/schemas/v3/connection.type'; +import { GithubAppManager } from "./ee/githubAppManager.js"; +import { hasEntitlement } from "@sourcebot/shared"; export const measure = async (cb: () => Promise) => { const start = Date.now(); @@ -125,6 +127,28 @@ export const fetchWithRetry = async ( // may have their own token. This method will just pick the first connection that has a token (if one exists) and uses that. This // may technically cause syncing to fail if that connection's token just so happens to not have access to the repo it's referencing. export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: PrismaClient, logger?: Logger): Promise => { + // If we have github apps configured we assume that we must use them for github service auth + if (repo.external_codeHostType === 'github' && hasEntitlement('github-app') && GithubAppManager.getInstance().appsConfigured()) { + const org = repo.displayName?.split('/')[0]; + const deploymentHostname = new URL(repo.external_codeHostUrl).hostname; + if (!org || !deploymentHostname) { + throw new Error(`Failed to fetch GitHub App for repo ${repo.displayName}:Invalid repo displayName (${repo.displayName}) or deployment hostname (${deploymentHostname})`); + } + + const token = await GithubAppManager.getInstance().getInstallationToken(org, deploymentHostname); + return { + hostUrl: repo.external_codeHostUrl, + token, + cloneUrlWithToken: createGitCloneUrlWithToken( + repo.cloneUrl, + { + username: 'x-access-token', + password: token + } + ), + } + } + for (const { connection } of repo.connections) { if (connection.connectionType === 'github') { const config = connection.config as unknown as GithubConnectionConfig; diff --git a/packages/schemas/src/v3/app.schema.ts b/packages/schemas/src/v3/app.schema.ts new file mode 100644 index 00000000..d5a7dfee --- /dev/null +++ b/packages/schemas/src/v3/app.schema.ts @@ -0,0 +1,113 @@ +// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! +const schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AppConfig", + "oneOf": [ + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "GithubAppConfig", + "properties": { + "type": { + "const": "githubApp", + "description": "GitHub App Configuration" + }, + "deploymentHostname": { + "type": "string", + "format": "url", + "default": "github.com", + "description": "The hostname of the GitHub App deployment.", + "examples": [ + "github.com", + "github.example.com" + ], + "pattern": "^[^\\s/$.?#].[^\\s]*$" + }, + "id": { + "type": "string", + "description": "The ID of the GitHub App." + }, + "privateKey": { + "description": "The private key of the GitHub App.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "privateKeyPath": { + "description": "The path to the private key of the GitHub App.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "id" + ], + "oneOf": [ + { + "required": [ + "privateKey" + ] + }, + { + "required": [ + "privateKeyPath" + ] + } + ], + "additionalProperties": false + } + ] +} as const; +export { schema as appSchema }; \ No newline at end of file diff --git a/packages/schemas/src/v3/app.type.ts b/packages/schemas/src/v3/app.type.ts new file mode 100644 index 00000000..255ef033 --- /dev/null +++ b/packages/schemas/src/v3/app.type.ts @@ -0,0 +1,6 @@ +// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! + +export type AppConfig = GithubAppConfig; +export type GithubAppConfig = { + [k: string]: unknown; +}; diff --git a/packages/schemas/src/v3/githubApp.schema.ts b/packages/schemas/src/v3/githubApp.schema.ts new file mode 100644 index 00000000..27d6445d --- /dev/null +++ b/packages/schemas/src/v3/githubApp.schema.ts @@ -0,0 +1,107 @@ +// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! +const schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "GithubAppConfig", + "properties": { + "type": { + "const": "githubApp", + "description": "GitHub App Configuration" + }, + "deploymentHostname": { + "type": "string", + "format": "url", + "default": "github.com", + "description": "The hostname of the GitHub App deployment.", + "examples": [ + "github.com", + "github.example.com" + ], + "pattern": "^[^\\s/$.?#].[^\\s]*$" + }, + "id": { + "type": "string", + "description": "The ID of the GitHub App." + }, + "privateKey": { + "description": "The private key of the GitHub App.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "privateKeyPath": { + "description": "The path to the private key of the GitHub App.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "id" + ], + "oneOf": [ + { + "required": [ + "privateKey" + ] + }, + { + "required": [ + "privateKeyPath" + ] + } + ], + "additionalProperties": false +} as const; +export { schema as githubAppSchema }; \ No newline at end of file diff --git a/packages/schemas/src/v3/githubApp.type.ts b/packages/schemas/src/v3/githubApp.type.ts new file mode 100644 index 00000000..7ed48a45 --- /dev/null +++ b/packages/schemas/src/v3/githubApp.type.ts @@ -0,0 +1,50 @@ +// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! + +export type GithubAppConfig = { + /** + * GitHub App Configuration + */ + type: "githubApp"; + /** + * The hostname of the GitHub App deployment. + */ + deploymentHostname?: string; + /** + * The ID of the GitHub App. + */ + id: string; + /** + * The private key of the GitHub App. + */ + privateKey?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * The path to the private key of the GitHub App. + */ + privateKeyPath?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; +} & { + [k: string]: unknown; +}; diff --git a/schemas/v3/app.json b/schemas/v3/app.json new file mode 100644 index 00000000..00703326 --- /dev/null +++ b/schemas/v3/app.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AppConfig", + "oneOf": [ + { + "$ref": "./githubApp.json" + } + ] +} \ No newline at end of file diff --git a/schemas/v3/githubApp.json b/schemas/v3/githubApp.json new file mode 100644 index 00000000..43c0d600 --- /dev/null +++ b/schemas/v3/githubApp.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "GithubAppConfig", + "properties": { + "type": { + "const": "githubApp", + "description": "GitHub App Configuration" + }, + "deploymentHostname": { + "type": "string", + "format": "url", + "default": "github.com", + "description": "The hostname of the GitHub App deployment.", + "examples": [ + "github.com", + "github.example.com" + ], + "pattern": "^[^\\s/$.?#].[^\\s]*$" + }, + "id": { + "type": "string", + "description": "The ID of the GitHub App." + }, + "privateKey": { + "$ref": "./shared.json#/definitions/Token", + "description": "The private key of the GitHub App." + }, + "privateKeyPath": { + "$ref": "./shared.json#/definitions/Token", + "description": "The path to the private key of the GitHub App." + } + }, + "required": [ + "type", + "id" + ], + "oneOf": [ + { + "required": ["privateKey"] + }, + { + "required": ["privateKeyPath"] + } + ], + "additionalProperties": false +} \ No newline at end of file