Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .github/workflows/deploy-cloudflare.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,11 @@ jobs:
GITHUB_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

# The page smoke above never touches the build queue. From 2026-08-21 to
# 2026-09-01 every hosted Artifact build failed behind green deploys,
# because workerd rejects a fetch redirect mode that Node accepts. This
# step requests one public Resolution and fails the job unless the build
# reaches `ready` within two minutes.
- name: Smoke an Artifact build
run: node scripts/smoke-artifact-resolution.mjs --timeout-ms 120000
21 changes: 20 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,23 @@ export default antfu({
'vue/attribute-hyphenation': 'off',
'ts/no-redeclare': 'off',
},
}, ...harlanzw({ base: { type: 'app' } }))
}, ...harlanzw({ base: { type: 'app' } }), {
// Server code runs on workerd, which accepts only the `follow` and `manual`
// redirect modes. Node's undici also accepts `error`, so the unit suite
// cannot catch it: every hosted Artifact build failed from 2026-08-21 to
// 2026-09-01 on one such fetch. The first two selectors repeat antfu's
// defaults, since a rule override replaces the whole option list.
files: ['layers/**/server/**/*.ts', 'server/**/*.ts', 'shared/**/*.ts', 'workers/**/*.ts'],
name: 'skilld/workerd-fetch-redirect',
rules: {
'no-restricted-syntax': [
'error',
'TSEnumDeclaration[const=true]',
'TSExportAssignment',
{
selector: 'Property[key.name="redirect"] > Literal[value="error"], Property[key.value="redirect"] > Literal[value="error"]',
message: 'workerd rejects redirect: \'error\'; use fetchNoRedirect()',
},
],
},
})
33 changes: 33 additions & 0 deletions layers/artifact-delivery/server/utils/fetch-no-redirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Fetch without following redirects, on any runtime.
*
* workerd accepts only `redirect: 'follow'` and `redirect: 'manual'`. It
* rejects the `error` redirect mode with a TypeError before the request leaves the
* Worker, while Node's undici accepts it. Every hosted Artifact build failed
* from 2026-08-21 to 2026-09-01 for that reason, and the unit suite on Node
* stayed green. Callers that must not follow a redirect go through this helper
* and decide what an unexpected redirect means for them.
*/

export type FetchNoRedirectResult
= { _tag: 'ok', response: Response }
| { _tag: 'unexpected-redirect', status: number, location: string | null }

const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308])

export async function fetchNoRedirect(
fetcher: typeof globalThis.fetch,
input: string | URL,
init: Omit<RequestInit, 'redirect'> = {},
): Promise<FetchNoRedirectResult> {
const response = await fetcher(input, { ...init, redirect: 'manual' })
if (!REDIRECT_STATUSES.has(response.status))
return { _tag: 'ok', response }
// The redirect body is never read. Cancel it so the connection is released.
await response.body?.cancel()
return {
_tag: 'unexpected-redirect',
status: response.status,
location: response.headers.get('location'),
}
}
25 changes: 17 additions & 8 deletions layers/artifact-delivery/server/utils/github-app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { z } from 'zod'
import { decryptToken, encryptToken } from '#layers/identity/server/utils/crypto'
import { base64ToBytes, bytesToBase64Url } from './encoding'
import { fetchNoRedirect } from './fetch-no-redirect'

const GITHUB_API = 'https://api.github.com'
const GITHUB_API_VERSION = '2026-03-10'
Expand Down Expand Up @@ -166,7 +167,7 @@ export async function exchangeGithubAppUserCode(
code: string,
dependencies: GithubAppUserTokenDependencies,
): Promise<GithubAppUserAuthorizationResult> {
const response = await dependencies.fetch('https://github.com/login/oauth/access_token', {
const fetched = await fetchNoRedirect(dependencies.fetch, 'https://github.com/login/oauth/access_token', {
method: 'POST',
headers: {
'Accept': 'application/json',
Expand All @@ -178,9 +179,11 @@ export async function exchangeGithubAppUserCode(
client_secret: dependencies.clientSecret,
code,
}).toString(),
redirect: 'error',
signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS),
})
if (fetched._tag === 'unexpected-redirect')
throw new Error(`GitHub App authorization redirected ${fetched.status}`)
const response = fetched.response
if (response.status === 400 || response.status === 401)
return { _tag: 'rejected' }
if (!response.ok)
Expand All @@ -191,16 +194,18 @@ export async function exchangeGithubAppUserCode(
const tokens = refreshedUserTokenSchema.safeParse(body)
if (!tokens.success)
throw new Error('GitHub App authorization returned an invalid response')
const identityResponse = await dependencies.fetch(`${GITHUB_API}/user`, {
const identityFetched = await fetchNoRedirect(dependencies.fetch, `${GITHUB_API}/user`, {
headers: {
'Accept': 'application/vnd.github+json',
'Authorization': `Bearer ${tokens.data.access_token}`,
'User-Agent': 'skilld.dev',
'X-GitHub-Api-Version': GITHUB_API_VERSION,
},
redirect: 'error',
signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS),
})
if (identityFetched._tag === 'unexpected-redirect')
throw new Error(`GitHub App identity request redirected ${identityFetched.status}`)
const identityResponse = identityFetched.response
if (identityResponse.status === 401 || identityResponse.status === 403)
return { _tag: 'rejected' }
if (!identityResponse.ok)
Expand Down Expand Up @@ -286,17 +291,19 @@ export async function loadAccountGithubAppUserToken(
grant_type: 'refresh_token',
refresh_token: refreshToken,
})
const response = await dependencies.fetch('https://github.com/login/oauth/access_token', {
const fetched = await fetchNoRedirect(dependencies.fetch, 'https://github.com/login/oauth/access_token', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'skilld.dev',
},
body: body.toString(),
redirect: 'error',
signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS),
})
if (fetched._tag === 'unexpected-redirect')
throw new Error(`GitHub App token refresh redirected ${fetched.status}`)
const response = fetched.response
if (response.status === 400 || response.status === 401)
return await loadRotatedGithubUserToken(db, accountId, row, dependencies)
if (!response.ok)
Expand Down Expand Up @@ -380,7 +387,7 @@ export function createGithubAppClient(config: GithubAppConfig): GithubAppClient
schema: z.ZodType<T>,
init: { method?: 'GET' | 'POST', body?: unknown } = {},
): Promise<{ _tag: 'ok', value: T } | { _tag: 'not-found' }> => {
const response = await config.fetch(`${GITHUB_API}${path}`, {
const fetched = await fetchNoRedirect(config.fetch, `${GITHUB_API}${path}`, {
method: init.method ?? 'GET',
headers: {
'Accept': 'application/vnd.github+json',
Expand All @@ -390,9 +397,11 @@ export function createGithubAppClient(config: GithubAppConfig): GithubAppClient
'X-GitHub-Api-Version': GITHUB_API_VERSION,
},
body: init.body === undefined ? undefined : JSON.stringify(init.body),
redirect: 'error',
signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS),
})
if (fetched._tag === 'unexpected-redirect')
throw new Error(`GitHub App request redirected ${fetched.status}`)
const response = fetched.response
if (response.status === 401 || response.status === 403 || response.status === 404)
return { _tag: 'not-found' }
if (!response.ok)
Expand Down
7 changes: 5 additions & 2 deletions layers/artifact-delivery/server/utils/github-source.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ProblemCode, ResolvedSource, SourceRequest } from '../schemas/contracts'
import { z } from 'zod'
import { base64ToBytes, digestHex } from './encoding'
import { fetchNoRedirect } from './fetch-no-redirect'

const GITHUB_API = 'https://api.github.com'
const GITHUB_API_VERSION = '2026-03-10'
Expand Down Expand Up @@ -116,11 +117,13 @@ export function createGithubSourceClient(options: GithubClientOptions): PublicGi
})
if (options.token)
headers.set('Authorization', `Bearer ${options.token}`)
const response = await options.fetch(`${GITHUB_API}${path}`, {
const fetched = await fetchNoRedirect(options.fetch, `${GITHUB_API}${path}`, {
headers,
redirect: 'error',
signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS),
})
if (fetched._tag === 'unexpected-redirect')
throw new Error(`GitHub redirected ${fetched.status} to ${fetched.location ?? 'an unknown location'}`)
const response = fetched.response
if (response.status === 404)
return { _tag: 'not-found' }
if (response.status === 401 || response.status === 403) {
Expand Down
13 changes: 7 additions & 6 deletions layers/artifact-delivery/server/utils/update-plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
comparisonRelationMatches,
relationMatchesDirectionalCounts,
} from '../schemas/update-plans'
import { fetchNoRedirect } from './fetch-no-redirect'

const GITHUB_API = 'https://api.github.com'
const GITHUB_API_VERSION = '2026-03-10'
Expand Down Expand Up @@ -407,18 +408,18 @@ async function requestComparisonPage(
if (etag)
headers.set('If-None-Match', etag)
const url = `${GITHUB_API}/repos/${encodeURIComponent(task.owner)}/${encodeURIComponent(task.repository)}/compare/${task.input.baseSha}...${task.input.headSha}?per_page=${COMMITS_PER_PAGE}&page=${page}`
const responseOutcome = await dependencies.fetch(url, {
const responseOutcome = await fetchNoRedirect(dependencies.fetch, url, {
headers,
redirect: 'error',
signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS),
}).then(
response => ({ _tag: 'response' as const, response }),
() => ({ _tag: 'failed' as const }),
)
}).catch(() => ({ _tag: 'failed' as const }))
if (responseOutcome._tag === 'failed') {
dependencies.reportFailure('github-compare')
return { _tag: 'provider_failure', status: null }
}
if (responseOutcome._tag === 'unexpected-redirect') {
dependencies.reportFailure('github-compare')
return { _tag: 'provider_failure', status: responseOutcome.status }
}

const response = responseOutcome.response
if (response.status === 304) {
Expand Down
Loading
Loading