Skip to content

Commit d6a10df

Browse files
committed
fix api-diff to handle errors better
1 parent 5304c4b commit d6a10df

File tree

1 file changed

+74
-38
lines changed

1 file changed

+74
-38
lines changed

tools/deno/api-diff.ts

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const SPEC_RAW_URL = (commit: string, filename: string) =>
4242
async function resolveCommit(ref?: string | number): Promise<string> {
4343
if (ref === undefined) return resolveCommit(await pickPr())
4444
if (typeof ref === 'number') {
45+
console.error(`Resolving PR #${ref} to commit...`)
4546
const query = `{
4647
repository(owner: "oxidecomputer", name: "omicron") {
4748
pullRequest(number: ${ref}) { headRefOid }
@@ -50,37 +51,67 @@ async function resolveCommit(ref?: string | number): Promise<string> {
5051
const pr = await $`gh api graphql -f query=${query}`.json()
5152
return pr.data.repository.pullRequest.headRefOid
5253
}
54+
// Tags, branches, and SHAs are passed through directly — the GitHub
55+
// contents API accepts any ref in the ?ref= param
5356
return ref
5457
}
5558

59+
/** 5 or fewer digits is a PR number; longer digit strings are short SHAs */
5660
const normalizeRef = (ref?: string | number) =>
57-
typeof ref === 'string' && /^\d+$/.test(ref) ? parseInt(ref, 10) : ref
61+
typeof ref === 'string' && /^\d{1,5}$/.test(ref) ? parseInt(ref, 10) : ref
62+
63+
async function listSchemaDir(ref: string) {
64+
console.error(`Fetching schema list for ${ref}...`)
65+
try {
66+
return await $`gh api ${SPEC_DIR_URL(ref)}`.stderr('null').json()
67+
} catch {
68+
throw new Error(
69+
`Could not list openapi/nexus/ at ref '${ref}'. ` +
70+
`This ref may predate the versioned schema directory.`
71+
)
72+
}
73+
}
5874

59-
async function getLatestSchema(commit: string) {
60-
const contents = await $`gh api ${SPEC_DIR_URL(commit)}`.json()
75+
async function getLatestSchema(ref: string) {
76+
const contents = await listSchemaDir(ref)
77+
const schemaFiles = contents
78+
.map((f: { name: string }) => f.name)
79+
.filter((n: string) => n.startsWith('nexus-'))
80+
.sort()
6181
const latestLink = contents.find((f: { name: string }) => f.name === 'nexus-latest.json')
62-
if (!latestLink) throw new Error('nexus-latest.json not found')
82+
if (!latestLink) {
83+
throw new Error(
84+
`nexus-latest.json not found at ref '${ref}'. ` +
85+
`Available schemas: ${schemaFiles.join(', ') || '(none)'}`
86+
)
87+
}
6388
return (await fetch(latestLink.download_url).then((r) => r.text())).trim()
6489
}
6590

66-
/** When diffing a single commit, we diff its latest schema against the previous one */
67-
async function getLatestAndPreviousSchema(commit: string) {
68-
const contents = await $`gh api ${SPEC_DIR_URL(commit)}`.json()
91+
/** When diffing a single ref, we diff its latest schema against the previous one */
92+
async function getLatestAndPreviousSchema(ref: string) {
93+
const contents = await listSchemaDir(ref)
6994

7095
const latestLink = contents.find((f: { name: string }) => f.name === 'nexus-latest.json')
71-
if (!latestLink) throw new Error('nexus-latest.json not found')
72-
const latest = (await fetch(latestLink.download_url).then((r) => r.text())).trim()
73-
7496
const schemaFiles = contents
7597
.filter(
7698
(f: { name: string }) => f.name.startsWith('nexus-') && f.name !== 'nexus-latest.json'
7799
)
78100
.map((f: { name: string }) => f.name)
79101
.sort()
80102

103+
if (!latestLink) {
104+
throw new Error(
105+
`nexus-latest.json not found at ref '${ref}'. ` +
106+
`Available schemas: ${schemaFiles.join(', ') || '(none)'}`
107+
)
108+
}
109+
const latest = (await fetch(latestLink.download_url).then((r) => r.text())).trim()
110+
81111
const latestIndex = schemaFiles.indexOf(latest)
82-
if (latestIndex === -1) throw new Error(`Latest schema ${latest} not found in dir`)
83-
if (latestIndex === 0) throw new Error('No previous schema version found')
112+
if (latestIndex === -1)
113+
throw new Error(`Latest schema ${latest} not found in dir at ref '${ref}'`)
114+
if (latestIndex === 0) throw new Error(`No previous schema version found at ref '${ref}'`)
84115

85116
return { previous: schemaFiles[latestIndex - 1], latest }
86117
}
@@ -90,24 +121,25 @@ async function resolveTarget(ref1?: string | number, ref2?: string): Promise<Dif
90121
if (ref2 !== undefined) {
91122
if (ref1 === undefined)
92123
throw new ValidationError('Provide a base ref when passing two refs')
93-
const [baseCommit, headCommit] = await Promise.all([
124+
const [baseRef, headRef] = await Promise.all([
94125
resolveCommit(normalizeRef(ref1)),
95126
resolveCommit(normalizeRef(ref2)),
96127
])
128+
console.error(`Comparing ${baseRef} vs ${headRef}`)
97129
const [baseSchema, headSchema] = await Promise.all([
98-
getLatestSchema(baseCommit),
99-
getLatestSchema(headCommit),
130+
getLatestSchema(baseRef),
131+
getLatestSchema(headRef),
100132
])
101-
return { baseCommit, baseSchema, headCommit, headSchema }
133+
return { baseCommit: baseRef, baseSchema, headCommit: headRef, headSchema }
102134
}
103135

104-
// Single ref: compare previous schema to latest within that commit
105-
const commit = await resolveCommit(normalizeRef(ref1))
106-
const { previous, latest } = await getLatestAndPreviousSchema(commit)
136+
// Single ref: compare previous schema to latest within that ref
137+
const ref = await resolveCommit(normalizeRef(ref1))
138+
const { previous, latest } = await getLatestAndPreviousSchema(ref)
107139
return {
108-
baseCommit: commit,
140+
baseCommit: ref,
109141
baseSchema: previous,
110-
headCommit: commit,
142+
headCommit: ref,
111143
headSchema: latest,
112144
}
113145
}
@@ -170,9 +202,8 @@ await new Command()
170202
171203
Arguments:
172204
No args Interactive PR picker
173-
<pr> PR number (e.g., 1234)
174-
<commit> Commit SHA
175-
<base> <head> Two refs (commits or PRs), compare latest schema on each
205+
<ref> PR number, commit SHA, branch, or tag
206+
<base> <head> Two refs, compare latest schema on each
176207
177208
Dependencies:
178209
- Deno
@@ -193,22 +224,27 @@ Dependencies:
193224
})
194225
.arguments('[ref1:string] [ref2:string]')
195226
.action(async (options, ref?: string, ref2?: string) => {
196-
const target = await resolveTarget(ref, ref2)
197-
const force = options.force ?? false
227+
try {
228+
const target = await resolveTarget(ref, ref2)
229+
const force = options.force ?? false
198230

199-
const [baseSchema, headSchema] = await Promise.all([
200-
ensureSchema(target.baseCommit, target.baseSchema, force),
201-
ensureSchema(target.headCommit, target.headSchema, force),
202-
])
203-
204-
if (options.format === 'schema') {
205-
await runDiff(baseSchema, headSchema, target.baseSchema, target.headSchema)
206-
} else {
207-
const [baseClient, headClient] = await Promise.all([
208-
ensureClient(baseSchema, force),
209-
ensureClient(headSchema, force),
231+
const [baseSchema, headSchema] = await Promise.all([
232+
ensureSchema(target.baseCommit, target.baseSchema, force),
233+
ensureSchema(target.headCommit, target.headSchema, force),
210234
])
211-
await runDiff(baseClient, headClient, target.baseSchema, target.headSchema)
235+
236+
if (options.format === 'schema') {
237+
await runDiff(baseSchema, headSchema, target.baseSchema, target.headSchema)
238+
} else {
239+
const [baseClient, headClient] = await Promise.all([
240+
ensureClient(baseSchema, force),
241+
ensureClient(headSchema, force),
242+
])
243+
await runDiff(baseClient, headClient, target.baseSchema, target.headSchema)
244+
}
245+
} catch (e) {
246+
console.error(`error: ${e instanceof Error ? e.message : e}`)
247+
Deno.exit(1)
212248
}
213249
})
214250
.parse(Deno.args)

0 commit comments

Comments
 (0)