-
Notifications
You must be signed in to change notification settings - Fork 31.9k
Expand file tree
/
Copy pathupload-preview-tarballs.js
More file actions
165 lines (151 loc) · 5.11 KB
/
Copy pathupload-preview-tarballs.js
File metadata and controls
165 lines (151 loc) · 5.11 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
// @ts-check
const fs = require('node:fs/promises')
const path = require('node:path')
const PREVIEW_BUILDS_AUDIENCE = 'https://vercel-packages.vercel.app'
const DEFAULT_PREVIEW_BUILDS_BASE_URL =
'https://vercel-packages.vercel.app/next'
/**
* Mints a GitHub Actions OIDC token for the given audience.
*
* @param {string} audience
* @returns {Promise<string>}
*/
async function mintGitHubActionsOidcToken(audience) {
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
if (!requestUrl || !requestToken) {
throw new Error(
'ACTIONS_ID_TOKEN_REQUEST_URL is not set. ' +
'The job needs the `id-token: write` permission.'
)
}
const url = new URL(requestUrl)
url.searchParams.set('audience', audience)
const response = await fetch(url, {
headers: { authorization: `Bearer ${requestToken}` },
})
if (!response.ok) {
throw new Error(
`Failed to mint GitHub OIDC token: ${response.status} ${await response.text()}`
)
}
const { value } = await response.json()
return value
}
/**
* GitHub OIDC tokens expire about five minutes after issuance and uploading a
* batch of tarballs can take longer, so the token is re-minted shortly before
* it expires.
*
* @returns {() => Promise<string>}
*/
function createGitHubOidcTokenGetter() {
/** @type {string | null} */
let cachedToken = null
let cachedTokenExpiresAt = 0
return async () => {
if (cachedToken !== null && cachedTokenExpiresAt - 60_000 > Date.now()) {
return cachedToken
}
const token = await mintGitHubActionsOidcToken(PREVIEW_BUILDS_AUDIENCE)
const payload = JSON.parse(
Buffer.from(token.split('.')[1], 'base64url').toString()
)
cachedToken = token
cachedTokenExpiresAt = payload.exp * 1000
return token
}
}
/**
* Yields one entry per package tarball under `tarballDirectory`. Scoped
* packages are laid out one level deeper (e.g. `@next/env/<name>.tgz`), so the
* walk descends into any directory whose name starts with `@`.
*
* @param {string} tarballDirectory
* @returns {AsyncGenerator<{ packageName: string, tarballPath: string }>}
*/
async function* findTarballs(tarballDirectory) {
const entries = await fs.readdir(tarballDirectory, { withFileTypes: true })
for (const entry of entries) {
if (!entry.isDirectory()) continue
const entryPath = path.join(tarballDirectory, entry.name)
if (entry.name.startsWith('@')) {
const scopeEntries = await fs.readdir(entryPath, { withFileTypes: true })
for (const scopeEntry of scopeEntries) {
if (!scopeEntry.isDirectory()) continue
const tarballPath = await findTarballInDir(
path.join(entryPath, scopeEntry.name)
)
if (tarballPath === null) continue
yield {
packageName: `${entry.name}/${scopeEntry.name}`,
tarballPath,
}
}
} else {
const tarballPath = await findTarballInDir(entryPath)
if (tarballPath === null) continue
yield { packageName: entry.name, tarballPath }
}
}
}
/**
* @param {string} dir
* @returns {Promise<string | null>}
*/
async function findTarballInDir(dir) {
const files = await fs.readdir(dir)
const tgzFile = files.find((f) => f.endsWith('.tgz'))
return tgzFile ? path.join(dir, tgzFile) : null
}
async function main() {
const [githubHeadSha, tarballDirectory] = process.argv.slice(2)
if (!githubHeadSha || !tarballDirectory) {
throw new Error(
'Usage: node scripts/upload-preview-tarballs.js <commitSha> <tarballDirectory>'
)
}
const baseUrl =
process.env.PREVIEW_BUILDS_BASE_URL || DEFAULT_PREVIEW_BUILDS_BASE_URL
const getOidcToken = createGitHubOidcTokenGetter()
for await (const { packageName, tarballPath } of findTarballs(
tarballDirectory
)) {
// vercel-packages authorizes the OIDC token and answers with a presigned
// upload URL scoped to this exact blob path. The bytes flow directly to
// the store, so the tarball size is not limited by a function's request
// body limit.
const response = await fetch(
`${baseUrl}/commits/${githubHeadSha}/${packageName}`,
{
method: 'POST',
headers: { authorization: `Bearer ${await getOidcToken()}` },
}
)
if (!response.ok) {
throw new Error(
`Failed to authorize the upload of ${packageName}: ${response.status}. ` +
`Response headers: ${JSON.stringify(Object.fromEntries(response.headers))}`
)
}
const body = await response.json()
const fileBuffer = await fs.readFile(tarballPath)
const putResponse = await fetch(body.url, {
method: 'PUT',
body: new Uint8Array(fileBuffer),
headers: { 'content-type': 'application/gzip' },
})
if (!putResponse.ok) {
throw new Error(
`Failed to upload ${packageName}: ${putResponse.status}. ` +
`Response headers: ${JSON.stringify(Object.fromEntries(putResponse.headers))}`
)
}
console.info(`Uploaded ${packageName} -> ${body.downloadUrl}`)
}
console.info('All tarballs uploaded')
}
main().catch((err) => {
console.error(err)
process.exit(1)
})