Skip to content

Commit cf3ba03

Browse files
authored
✨ feat: add asset URL resolution for GitHub uploads (#1569)
* ✨ feat: add asset URL resolution for GitHub uploads Introduced asset URL resolution in GitHubClient's API interface. * ✨ Add asset URL resolution and corresponding tests Added `resolveAssetUrl` to fetch short-lived URLs for GitHub assets. * ✨: Enhance GitHub asset URL handling Added regex matching for GitHub asset URLs and improved its resolution. * 🐛 Fix asset URL resolution and validation in tests Ensure asset URLs are correctly resolved and types validated.
1 parent 49e3fd2 commit cf3ba03

6 files changed

Lines changed: 83 additions & 6 deletions

File tree

docs/src/content/docs/reference/scripts/github.mdx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,19 @@ const files = await github.downloadArtifact(artifact.id)
126126
console.log(files)
127127
```
128128

129+
### Assets
130+
131+
Image or video assets urls uploaded through the GitHub UI can be resolved using `resolveAssetUrl`.
132+
They are typically of the form `https://github.com/.../assets/<uuid>`. The function returns a short lived URL with an embedded
133+
access token to download the asset.
134+
135+
```js
136+
const url = await github.resolveAssetUrl(
137+
"https://github.com/user-attachments/assets/a6e1935a-868e-4cca-9531-ad0ccdb9eace"
138+
)
139+
console.log(url)
140+
```
141+
129142
### Search Code
130143

131144
Use `searchCode` for a code search on the default branch in the same repository.

packages/core/src/constants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,4 +456,6 @@ export const BOX_UP_AND_RIGHT = "╰"
456456
export const BOX_UP_AND_DOWN = "│"
457457
export const BOX_DOWN_UP_AND_RIGHT = "├"
458458
export const BOX_LEFT_AND_DOWN = "╮"
459-
export const BOX_LEFT_AND_UP = "╯"
459+
export const BOX_LEFT_AND_UP = "╯"
460+
461+
export const GITHUB_ASSET_URL_RX = /^https:\/\/github\.com\/.*\/assets\/.*$/i

packages/core/src/githubclient.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { readFile } from "node:fs/promises"
55
import { fileURLToPath } from "node:url"
66
import { isCI } from "./ci"
77
import { TestHost } from "./testhost"
8+
import { resolveBufferLike } from "./bufferlike"
9+
import { tryResolveResource } from "./resources"
810

911
describe("GitHubClient", async () => {
1012
const client = GitHubClient.default()
@@ -110,4 +112,26 @@ describe("GitHubClient", async () => {
110112
const un = await client.uploadAsset(undefined)
111113
assert(un === undefined)
112114
})
115+
await test("resolveAssetUrl -image", async () => {
116+
const resolved = await client.resolveAssetUrl(
117+
"https://github.com/user-attachments/assets/a6e1935a-868e-4cca-9531-ad0ccdb9eace"
118+
)
119+
assert(resolved)
120+
assert(resolved.includes("githubusercontent.com"))
121+
})
122+
await test("resolveAssetUrl - mp4", async () => {
123+
const resolved = await client.resolveAssetUrl(
124+
"https://github.com/user-attachments/assets/f7881bef-931d-4f76-8f63-b4d12b1f021e"
125+
)
126+
console.log(resolved)
127+
assert(resolved.includes("githubusercontent.com"))
128+
})
129+
130+
await test("resolveAssetUrl - image - indirect", async () => {
131+
const resolved = await tryResolveResource(
132+
"https://github.com/user-attachments/assets/a6e1935a-868e-4cca-9531-ad0ccdb9eace"
133+
)
134+
assert(resolved.files[0].content)
135+
assert.strictEqual(resolved.files[0].type, "image/jpeg")
136+
})
113137
})

packages/core/src/githubclient.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { PaginateInterface } from "@octokit/plugin-paginate-rest"
33
import {
44
GITHUB_API_VERSION,
55
GITHUB_ASSET_BRANCH,
6+
GITHUB_ASSET_URL_RX,
67
GITHUB_PULL_REQUEST_REVIEW_COMMENT_LINE_DISTANCE,
78
GITHUB_REST_API_CONCURRENCY_LIMIT,
89
GITHUB_REST_PAGE_DEFAULT,
@@ -32,6 +33,7 @@ import { CancellationOptions, checkCancelled } from "./cancellation"
3233
import { diagnosticToGitHubMarkdown } from "./annotations"
3334
import { TraceOptions } from "./trace"
3435
import { unzip } from "./zip"
36+
import { uriRedact, uriTryParse } from "./url"
3537
const dbg = genaiscriptDebug("github")
3638

3739
export interface GithubConnectionInfo {
@@ -1342,6 +1344,26 @@ export class GitHubClient implements GitHub {
13421344
return data
13431345
}
13441346

1347+
async resolveAssetUrl(url: string) {
1348+
if (!uriTryParse(url)) return undefined // unknown format
1349+
if (!GITHUB_ASSET_URL_RX.test(url)) return undefined // not a github asset
1350+
const { client, owner, repo } = await this.api()
1351+
dbg(`asset: resolving url for %s`, uriRedact(url))
1352+
const { data, status } = await client.rest.markdown.render({
1353+
owner,
1354+
repo,
1355+
context: `${owner}/${repo}`, // force html with token
1356+
text: `![](${url})`,
1357+
mode: "gfm",
1358+
})
1359+
dbg(`asset: resolution %s`, status)
1360+
const { resolved } =
1361+
/<img src="(?<resolved>[^"]+)"/i.exec(data)?.groups || {}
1362+
if (!resolved) dbg(`markdown:\n%s`, data)
1363+
1364+
return resolved
1365+
}
1366+
13451367
async downloadArtifactFiles(
13461368
artifactId: number | string
13471369
): Promise<WorkspaceFile[]> {

packages/core/src/resources.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ import { GitClient } from "./git"
1212
import { expandFiles } from "./fs"
1313
import { join } from "node:path"
1414
import { isCancelError } from "./error"
15+
import { GITHUB_ASSET_URL_RX } from "./constants"
1516
const dbg = genaiscriptDebug("res")
1617
const dbgAdaptors = dbg.extend("adaptors")
1718
const dbgFiles = dbg.extend("files")
1819
dbgFiles.enabled = false
1920

2021
const urlAdapters: {
2122
id: string
22-
matcher: (url: string) => string
23+
matcher: (url: string) => Awaitable<string>
2324
}[] = [
2425
{
2526
id: "github blob",
@@ -40,6 +41,17 @@ const urlAdapters: {
4041
: undefined
4142
},
4243
},
44+
{
45+
id: "github assets",
46+
matcher: async (url) => {
47+
if (GITHUB_ASSET_URL_RX.test(url)) {
48+
const client = GitHubClient.default()
49+
const resolved = await client.resolveAssetUrl(url)
50+
return resolved
51+
}
52+
return undefined
53+
},
54+
},
4355
{
4456
id: "gist",
4557
matcher: (url) => {
@@ -54,10 +66,10 @@ const urlAdapters: {
5466
},
5567
]
5668

57-
function applyUrlAdapters(url: string) {
69+
async function applyUrlAdapters(url: string) {
5870
// Use URL adapters to modify the URL if needed
5971
for (const a of urlAdapters) {
60-
const newUrl = a.matcher(url)
72+
const newUrl = await a.matcher(url)
6173
if (newUrl) {
6274
dbgAdaptors(`%s: %s`, a.id, uriRedact(url))
6375
return newUrl
@@ -83,7 +95,6 @@ const uriResolvers: Record<
8395
// https://.../.../....git
8496
if (/\.git($|\/)/.test(url.pathname))
8597
return await uriResolvers.git(dbg, url, options)
86-
8798
// regular fetch
8899
const fetch = await createFetch(options)
89100
dbg(`fetch %s`, uriRedact(url.href))
@@ -211,7 +222,7 @@ export async function tryResolveResource(
211222
options?: TraceOptions & CancellationOptions
212223
): Promise<{ uri: URL; files: WorkspaceFile[] } | undefined> {
213224
if (!url) return undefined
214-
url = applyUrlAdapters(url)
225+
url = await applyUrlAdapters(url)
215226
const uri = uriTryParse(url)
216227
if (!uri) return undefined
217228
const { cancellationToken } = options || {}

packages/core/src/types/prompt_template.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3870,6 +3870,11 @@ interface GitHub {
38703870
}
38713871
): Promise<string>
38723872

3873+
/**
3874+
* Resolves user uploaded assets to a short lived URL with access token. Returns undefined if the asset is not found.
3875+
*/
3876+
resolveAssetUrl(url: string): Promise<string | undefined>
3877+
38733878
/**
38743879
* Gets the underlying Octokit client
38753880
*/

0 commit comments

Comments
 (0)