-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrefs-rest.ts
More file actions
178 lines (170 loc) · 6.83 KB
/
refs-rest.ts
File metadata and controls
178 lines (170 loc) · 6.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/**
* @fileoverview Resolve a GitHub git ref via REST tier-cascade.
*
* Split out of `github/refs.ts` for size hygiene. Walks tag → branch →
* commit endpoints in sequence; the first 200 OK wins. If any tier
* hits the documented 200-OK-empty-body incident shape, falls back to
* the GraphQL transport in `./refs-graphql`.
*/
import { errorMessage } from '../errors/message'
import { ErrorCtor } from '../primordials/error'
import { fetchGitHub } from './fetch'
import { fetchRefShaViaGraphQL } from './refs-graphql'
import { GITHUB_API_BASE_URL } from './constants'
import { GitHubEmptyBodyError } from './errors'
import type {
GitHubCommit,
GitHubFetchOptions,
GitHubRef,
GitHubTag,
ResolveRefOptions,
} from './types'
/**
* Fetch the SHA for a git ref from GitHub API.
* Internal helper that implements the multi-strategy ref resolution logic.
* Tries tags, branches, and direct commit lookups in sequence.
*
* @param owner - Repository owner
* @param repo - Repository name
* @param ref - Git reference to resolve
* @param options - Resolution options with authentication token
* @returns The full commit SHA
*
* @throws {Error} When ref cannot be resolved after all strategies fail
*/
export async function fetchRefSha(
owner: string,
repo: string,
ref: string,
options: ResolveRefOptions,
): Promise<string> {
const fetchOptions: GitHubFetchOptions = {
token: options.token,
}
// ---------------------------------------------------------------
// Why this function has a "tier cascade" instead of a single call:
//
// The user gives us a string `ref` and we don't know whether it
// names a tag (e.g. "v1.2.3"), a branch (e.g. "main"), or a raw
// commit SHA (e.g. "abc1234..."). REST has three different
// endpoints for these — there's no single "resolve any ref"
// endpoint — so we just try each in order: tag first (most
// common), then branch, then raw commit SHA. The first 200
// wins, the rest are skipped.
//
// Why we track `sawEmptyBody` separately from "this tier 404'd":
//
// A real 404 means "this tier didn't match — keep walking" (e.g.
// "v1.2.3" isn't a branch, so the heads/v1.2.3 lookup 404s and
// we move on). But a `GitHubEmptyBodyError` means "GitHub itself
// is degraded right now and even a real match would return as
// if it didn't exist." Walking the tier cascade further when
// GitHub is down just multiplies the wasted calls — we'd 'fail'
// all three tiers, then either give up or fall back. By noting
// the empty-body signal in `sawEmptyBody`, we can fall through
// to a single GraphQL call after the cascade finishes that
// resolves all three forms in one shot via a different backend.
//
// The `note404` name is a little unfortunate — it really tracks
// "the kind of error we just caught". But the semantic intent
// from the caller's perspective IS "this tier didn't match",
// which is what 404 means in the original cascade. Renaming
// would touch every catch site for limited gain.
// ---------------------------------------------------------------
let sawEmptyBody = false
const note404 = (e: unknown): unknown => {
if (e instanceof GitHubEmptyBodyError) {
sawEmptyBody = true
}
return e
}
try {
// Try as a tag first.
const tagUrl = `${GITHUB_API_BASE_URL}/repos/${owner}/${repo}/git/refs/tags/${ref}`
const tagData = await fetchGitHub<GitHubRef>(tagUrl, fetchOptions)
// Tag might point to a tag object or directly to a commit.
if (tagData.object.type === 'tag') {
// Dereference the tag object to get the commit.
const tagObject = await fetchGitHub<GitHubTag>(
tagData.object.url,
fetchOptions,
)
return tagObject.object.sha
}
return tagData.object.sha
} catch (e) {
note404(e)
// Not a tag, try as a branch.
try {
const branchUrl = `${GITHUB_API_BASE_URL}/repos/${owner}/${repo}/git/refs/heads/${ref}`
const branchData = await fetchGitHub<GitHubRef>(branchUrl, fetchOptions)
return branchData.object.sha
} catch (e2) {
note404(e2)
// Try without refs/ prefix (for commit SHAs or other refs).
try {
const commitUrl = `${GITHUB_API_BASE_URL}/repos/${owner}/${repo}/commits/${ref}`
const commitData = await fetchGitHub<GitHubCommit>(
commitUrl,
fetchOptions,
)
return commitData.sha
} catch (e3) {
note404(e3)
// -----------------------------------------------------------
// If ANY of the three REST tiers hit the empty-body signal,
// REST is degraded — fall back to GraphQL. GraphQL hits a
// *different* backend at GitHub (not the same Elasticsearch
// index as REST listings), so it stays consistent through
// the kinds of incidents that produce empty REST bodies.
//
// We only fall back when `sawEmptyBody` is true. If all
// three tiers genuinely 404'd (the ref really doesn't exist
// anywhere — tag, branch, or commit), we DON'T trigger the
// GraphQL call. That keeps the fallback narrow: it fires
// only on the documented incident shape, not on every
// "ref not found" outcome.
//
// If GraphQL ALSO fails (network error, GraphQL errors[],
// etc.) we throw an informative "both transports failed"
// error so the operator sees the cross-backend signal
// rather than a bare last-tier REST error.
// -----------------------------------------------------------
if (sawEmptyBody) {
let graphqlSha: string | undefined
let graphqlErr: unknown
try {
graphqlSha = await fetchRefShaViaGraphQL(
owner,
repo,
ref,
fetchOptions,
)
} catch (cause) {
graphqlErr = cause
}
if (graphqlSha) {
return graphqlSha
}
// graphqlErr-defined arm fires only when the GraphQL fallback
// also failed; tested but not on every cascade entry.
/* c8 ignore start */
if (graphqlErr !== undefined) {
throw new ErrorCtor(
`Failed to resolve ref "${ref}" for ${owner}/${repo}: both REST and GraphQL backends degraded`,
{ cause: graphqlErr },
)
}
/* c8 ignore stop */
// GraphQL completed successfully but found no match — the ref
// genuinely doesn't exist (or the empty-body signal happened
// but GitHub has since recovered enough for GraphQL to confirm
// the absence). Surface the cleaner "ref not found" message.
}
throw new ErrorCtor(
`Failed to resolve ref "${ref}" for ${owner}/${repo}: ${errorMessage(e3)}`,
)
}
}
}
}