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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions ee/apps/den-api/src/routes/org/plugin-system/github-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ export async function getGithubRepositoryTextFile(input: {
return Buffer.from(response.body.content.replace(/\n/g, ""), "base64").toString("utf8")
}

export async function getGithubRepositoryTree(input: {
async function getGithubRepositoryCommit(input: {
branch: string
config: GithubConnectorAppConfig
fetchFn?: GithubFetch
Expand Down Expand Up @@ -522,6 +522,36 @@ export async function getGithubRepositoryTree(input: {
throw new GithubConnectorRequestError("GitHub commit response was missing the head or tree sha.", 502, commitResponse.body)
}

return {
headSha,
repositoryParts,
token,
treeSha,
}
}

export async function getGithubRepositoryHeadSha(input: {
branch: string
config: GithubConnectorAppConfig
fetchFn?: GithubFetch
installationId: number
repositoryFullName: string
token?: string
}) {
const commit = await getGithubRepositoryCommit(input)
return commit.headSha
}

export async function getGithubRepositoryTree(input: {
branch: string
config: GithubConnectorAppConfig
fetchFn?: GithubFetch
installationId: number
repositoryFullName: string
token?: string
}) {
const commit = await getGithubRepositoryCommit(input)

const treeResponse = await requestGithubJson<{
truncated?: boolean
tree?: Array<{
Expand All @@ -532,8 +562,10 @@ export async function getGithubRepositoryTree(input: {
}>
}>({
fetchFn: input.fetchFn,
headers: authHeaders,
path: `/repos/${encodeURIComponent(repositoryParts.owner)}/${encodeURIComponent(repositoryParts.repo)}/git/trees/${encodeURIComponent(treeSha)}?recursive=1`,
headers: {
Authorization: `Bearer ${commit.token}`,
},
path: `/repos/${encodeURIComponent(commit.repositoryParts.owner)}/${encodeURIComponent(commit.repositoryParts.repo)}/git/trees/${encodeURIComponent(commit.treeSha)}?recursive=1`,
})

const treeEntries = Array.isArray(treeResponse.body.tree)
Expand All @@ -555,10 +587,10 @@ export async function getGithubRepositoryTree(input: {
: []

return {
headSha,
headSha: commit.headSha,
truncated: Boolean(treeResponse.body.truncated),
treeEntries,
treeSha,
treeSha: commit.treeSha,
} satisfies GithubRepositoryTreeSnapshot
}

Expand Down
17 changes: 17 additions & 0 deletions ee/apps/den-api/src/routes/org/plugin-system/github-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,23 @@ export type GithubRepoDiscoveryResult = {
warnings: string[]
}

export type GithubDiscoveryCacheIdentity = {
branch: string
ref: string
repositoryFullName: string
sourceRevisionRef: string
}

export function isGithubDiscoveryCacheFresh(input: GithubDiscoveryCacheIdentity & {
cached: GithubDiscoveryCacheIdentity | null
}) {
return Boolean(input.cached
&& input.cached.branch === input.branch
&& input.cached.ref === input.ref
&& input.cached.repositoryFullName === input.repositoryFullName
&& input.cached.sourceRevisionRef === input.sourceRevisionRef)
}

type MarketplaceEntry = {
agents?: unknown
commands?: unknown
Expand Down
19 changes: 15 additions & 4 deletions ee/apps/den-api/src/routes/org/plugin-system/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
getGithubAppSummary,
getGithubConnectorAppConfig,
getGithubInstallationAccessToken,
getGithubRepositoryHeadSha,
getGithubRepositoryTextFile,
getGithubRepositoryTree,
getGithubInstallationSummary,
Expand All @@ -40,6 +41,7 @@ import {
} from "./github-app.js"
import {
buildGithubRepoDiscovery,
isGithubDiscoveryCacheFresh,
type GithubDiscoveredPlugin,
type GithubDiscoveryClassification,
type GithubMarketplaceInfo,
Expand Down Expand Up @@ -2480,6 +2482,12 @@ async function getGithubDiscoveryContext(input: { connectorInstanceId: Connector
installationId,
ref,
repositoryFullName,
sourceRevisionRef: await getGithubRepositoryHeadSha({
branch,
config: githubConnectorAppConfig(),
installationId,
repositoryFullName,
}),
}
}

Expand Down Expand Up @@ -2727,10 +2735,13 @@ async function resolveGithubConnectorDiscovery(input: { connectorInstanceId: Con
? discoveryContext.connectorTarget.targetConfigJson as Record<string, unknown>
: null
const cached = readGithubDiscoveryCache(targetConfig)
if (cached
&& cached.branch === discoveryContext.branch
&& cached.ref === discoveryContext.ref
&& cached.repositoryFullName === discoveryContext.repositoryFullName) {
if (cached && isGithubDiscoveryCacheFresh({
cached,
branch: discoveryContext.branch,
ref: discoveryContext.ref,
repositoryFullName: discoveryContext.repositoryFullName,
sourceRevisionRef: discoveryContext.sourceRevisionRef,
})) {
return {
autoImportNewPlugins: discoveryContext.autoImportNewPlugins,
cache: cached,
Expand Down
27 changes: 27 additions & 0 deletions ee/apps/den-api/test/github-connector-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getGithubAppSummary,
getGithubConnectorAppConfig,
getGithubInstallationSummary,
getGithubRepositoryHeadSha,
listGithubInstallationRepositories,
normalizeGithubPrivateKey,
validateGithubInstallationTarget,
Expand Down Expand Up @@ -87,6 +88,32 @@ describe("github connector app helpers", () => {
])
})

test("reads the current GitHub branch head sha without fetching the full tree", async () => {
const requests: string[] = []
const headSha = await getGithubRepositoryHeadSha({
branch: "main",
config: { appId: "123456", privateKey: privateKeyPem },
fetchFn: async (url) => {
requests.push(String(url))
if (String(url).endsWith("/access_tokens")) {
return new Response(JSON.stringify({ token: "installation-token" }), { status: 201 })
}
if (String(url).endsWith("/repos/different-ai/openwork/commits/main")) {
return new Response(JSON.stringify({ sha: "head-sha-1", commit: { tree: { sha: "tree-sha-1" } } }), { status: 200 })
}
return new Response(JSON.stringify({ message: "not found" }), { status: 404 })
},
installationId: 777,
repositoryFullName: "different-ai/openwork",
})

expect(headSha).toBe("head-sha-1")
expect(requests).toEqual([
"https://api.github.com/app/installations/777/access_tokens",
"https://api.github.com/repos/different-ai/openwork/commits/main",
])
})

test("builds install URLs and validates signed state tokens", async () => {
const app = await getGithubAppSummary({
config: { appId: "123456", privateKey: privateKeyPem },
Expand Down
22 changes: 21 additions & 1 deletion ee/apps/den-api/test/github-discovery.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
import { describe, expect, test } from "bun:test"
import { buildGithubRepoDiscovery, type GithubDiscoveryTreeEntry } from "../src/routes/org/plugin-system/github-discovery.js"
import { buildGithubRepoDiscovery, isGithubDiscoveryCacheFresh, type GithubDiscoveryTreeEntry } from "../src/routes/org/plugin-system/github-discovery.js"

function blob(path: string): GithubDiscoveryTreeEntry {
return { id: path, kind: "blob", path, sha: null, size: null }
}

describe("github discovery", () => {
test("treats a changed source revision as a stale discovery cache", () => {
const cached = {
branch: "main",
ref: "refs/heads/main",
repositoryFullName: "different-ai/openwork",
sourceRevisionRef: "old-head",
}

expect(isGithubDiscoveryCacheFresh({
...cached,
cached,
sourceRevisionRef: "old-head",
})).toBe(true)
expect(isGithubDiscoveryCacheFresh({
...cached,
cached,
sourceRevisionRef: "new-head",
})).toBe(false)
})

test("classifies marketplace repos and resolves local plugin roots", () => {
const result = buildGithubRepoDiscovery({
entries: [
Expand Down