-
Notifications
You must be signed in to change notification settings - Fork 0
/
searcher.ts
337 lines (305 loc) · 8.46 KB
/
searcher.ts
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
/*
* Copyright 2022 leorize <[email protected]>
*
* SPDX-License-Identifier: MIT
*/
/**
* Implements a simple searcher for a nimskull release via Github Release.
*/
import * as semver from "semver";
import * as os from "os";
import { HttpClient, HttpCodes } from "@actions/http-client";
import type { Octokit } from "@octokit/core";
import { Release as GhRelease, Repository } from "@octokit/graphql-schema";
const DefaultRepo = "nim-works/nimskull";
const SupportedManifestVersion = 0;
interface ArtifactDataV0 {
name: string;
sha256: string;
}
interface BinaryArtifactDataV0 extends ArtifactDataV0 {
target: string;
}
interface ReleaseManifestV0 {
manifestVersion: number;
version: string;
source: ArtifactDataV0;
binaries: BinaryArtifactDataV0[];
}
/**
* A release description
*/
export interface Release {
/**
* The UID of the release. This can be passed to Github to obtain more data.
*/
id: string;
/**
* The tag of the release. For nimskull this is also the version
*/
tag: string;
}
/**
* Find the latest nimskull release matching the specified range.
*
* Pre-releases are included, as the project does not have any stable release
* at the moment.
*
* @param client - The Octokit client used to interact with Github.
* @param range - The semver range to match against.
* @param repo - The repository to fetch versions from.
*
* @return The latest release matching the range. Null is returned if such
* version is not found.
*/
export async function findVersion(
client: Octokit,
range: string,
repo = DefaultRepo,
): Promise<Release | null> {
if (!isSpecificVersion(range)) {
for await (const release of getReleases(client, repo)) {
if (semver.satisfies(release.tag, range, { includePrerelease: true }))
return release;
}
} else {
return getRelease(client, repo, range);
}
return null;
}
/**
* Retrieve the compiler binary download link for the current system.
*
* @param client - The Octokit client used to interact with Github.
* @param releaseId - The unique release id to download binaries for.
*
* @return The link to download the binary for the current system, null if not available.
*/
export async function getDownloadUrl(
client: Octokit,
releaseId: string,
): Promise<string | null> {
const manifestReq = await new HttpClient().get(
await urlForAsset(client, releaseId, "manifest.json"),
);
if (manifestReq.message.statusCode != HttpCodes.OK)
throw `Fetching release manifest failed with status code: ${manifestReq.message.statusCode}`;
const manifest: ReleaseManifestV0 = JSON.parse(await manifestReq.readBody());
if (manifest.manifestVersion != SupportedManifestVersion)
throw `Expected manifest version ${SupportedManifestVersion} but got ${manifest.manifestVersion}`;
const targetBinary = manifest.binaries.find((x) =>
tripletMatchesSystem(x.target),
);
if (targetBinary)
return await urlForAsset(client, releaseId, targetBinary.name);
return null;
}
/**
* Retrieve the URL for a particular Github Release Asset.
*
* @param client - The Octokit client used to interact with Github.
* @param id - The release unique id.
* @param asset - The exact name of the asset.
*
* @return The URL of the requested asset if it exists.
*/
async function urlForAsset(
client: Octokit,
id: string,
asset: string,
): Promise<string> {
const {
node: {
releaseAssets: { nodes },
},
} = await client.graphql<{ node: GhRelease }>(
`
query ($id: ID!, $assetName: String!) {
node(id: $id) {
... on Release {
releaseAssets(first: 1, name: $assetName) {
nodes {
downloadUrl
}
}
}
}
}
`,
{
id: id,
assetName: asset,
},
);
return nodes?.[0]?.downloadUrl ?? null;
}
/**
* @param triplet - The triplet to check. See
* https://clang.llvm.org/docs/CrossCompilation.html#target-triple
* for the format.
*
* @return Whether the given triplet describes the current system.
* This only covers targets that are likely to be run in CI.
*/
function tripletMatchesSystem(triplet: string): boolean {
if (!triplet) return false;
const splitted = triplet.split("-");
/* If the architecture part is not defined, the triplet is invalid */
if (!splitted[0]) return false;
/* Process architecture */
switch (os.arch()) {
case "arm":
if (!splitted[0].match(/arm/)) return false;
break;
case "arm64":
if (splitted[0] !== "aarch64") return false;
break;
case "x64":
if (splitted[0] !== "x86_64") return false;
break;
default:
/* If it's an architecture that we do not know of, assume that the
* triplet did not match */
return false;
}
let scanPos = 1;
if (scanPos < splitted.length) {
/* Process vendor */
switch (splitted[scanPos]) {
case "pc":
/* Assume Darwin to be the macOS runner, which should match against
* 'apple' vendor */
if (os.platform() === "darwin") return false;
scanPos++;
break;
case "apple":
if (os.platform() !== "darwin") return false;
scanPos++;
break;
}
/* Process OS */
switch (splitted[scanPos]) {
case "darwin":
case "macosx":
if (os.platform() !== "darwin") return false;
scanPos++;
break;
case "linux":
if (os.platform() !== "linux") return false;
scanPos++;
/* Process environment (if any), accept only the version using GNU ABI */
if (splitted[scanPos])
if (!splitted[scanPos]!.match(/gnu/)) return false;
break;
case "windows":
if (os.platform() !== "win32") return false;
scanPos++;
/* Process environment (if any), accept only the version using GNU ABI */
if (splitted[scanPos])
if (!splitted[scanPos]!.match(/gnu/)) return false;
break;
}
}
return true;
}
/**
* Iterates through all releases in the given repository, from most to least recent.
*
* @param client - The authenticated octokit client.
* @param repo - The repository to obtain release data from.
*
* @return The release tag name.
*/
async function* getReleases(
client: Octokit,
repo: string,
): AsyncGenerator<Release> {
const [owner, name] = repo.split("/");
let endCursor: string | undefined;
let hasNextPage = true;
while (hasNextPage) {
const {
repository: {
releases: { edges: releaseEdges, pageInfo },
},
} = await client.graphql<{ repository: Repository }>(
`
query ($owner: String!, $name: String!, $endCursor: String, $order: ReleaseOrder!) {
repository(owner: $owner, name: $name) {
releases(after: $endCursor, first: 100, orderBy: $order) {
edges {
node {
id
tagName
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
`,
{
owner,
name,
endCursor,
order: {
direction: "DESC",
field: "CREATED_AT",
},
},
);
hasNextPage = pageInfo.hasNextPage;
endCursor = pageInfo.endCursor ?? undefined;
if (releaseEdges) {
for (const release of releaseEdges) {
if (release?.node) {
yield { id: release.node.id, tag: release.node.tagName };
}
}
}
}
}
/**
* Obtain release description of the given tag
*
* @param client - The authenticated octokit client.
* @param repo - The repository to obtain release data from.
* @param tagName - The tag to get release data of.
*
* @return The release info.
*/
async function getRelease(
client: Octokit,
repo: string,
tagName: string,
): Promise<Release | null> {
const [owner, name] = repo.split("/");
const {
repository: { release },
} = await client.graphql<{ repository: Repository }>(
`
query ($owner: String!, $name: String!, $tagName: String!) {
repository(owner: $owner, name: $name) {
release(tagName: $tagName) {
id
}
}
}
`,
{
owner: owner,
name: name,
tagName: tagName,
},
);
return release ? { id: release.id, tag: tagName } : null;
}
/**
* Returns whether `s` is a specific version
*/
function isSpecificVersion(s: string): boolean {
return typeof semver.valid(s) === "string";
}