-
Notifications
You must be signed in to change notification settings - Fork 31.9k
Expand file tree
/
Copy pathreset-project.mjs
More file actions
171 lines (154 loc) · 5.27 KB
/
Copy pathreset-project.mjs
File metadata and controls
171 lines (154 loc) · 5.27 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
export const TEST_PROJECT_NAME = 'vtest314-e2e-tests'
export const TEST_TEAM_NAME = process.env.VERCEL_TEST_TEAM
export const TEST_TOKEN = process.env.VERCEL_TEST_TOKEN
export const ADAPTER_TEST_TEAM_NAME = process.env.VERCEL_ADAPTER_TEST_TEAM
export const ADAPTER_TEST_TOKEN = process.env.VERCEL_ADAPTER_TEST_TOKEN
export const TURBOPACK_TEST_TEAM_NAME = process.env.VERCEL_TURBOPACK_TEST_TEAM
export const TURBOPACK_TEST_TOKEN = process.env.VERCEL_TURBOPACK_TEST_TOKEN
/**
* Whether a value read from the environment carries no usable value. An unset
* variable reads as `undefined`, while one set to an empty value reads as `''`;
* neither can identify a team or authenticate against one.
* @param {string | null | undefined} value
* @returns {boolean}
*/
function isAbsent(value) {
return value === undefined || value === null || value === ''
}
/**
* Retry a fetch request with exponential backoff
* @param {string} url - The URL to fetch
* @param {object} options - Fetch options
* @param {object} config - Retry configuration
* @param {number} config.maxRetries - Maximum number of retry attempts (default: 5)
* @param {number[]} config.acceptableStatuses - Status codes that are acceptable and should not retry (default: [])
* @param {string} config.operationName - Name of the operation for logging (default: 'Request')
* @returns {Promise<Response>} The fetch response
*/
async function fetchWithRetry(
url,
options = {},
{ maxRetries = 5, acceptableStatuses = [], operationName = 'Request' } = {}
) {
let lastError
let response
for (let attempt = 0; attempt < maxRetries; attempt++) {
response = await fetch(url, options)
// Check if response is acceptable
if (response.ok || acceptableStatuses.includes(response.status)) {
return response
}
// If we have attempts remaining, retry
if (attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000 // exponential backoff: 1s, 2s, 4s, 8s, 16s
const errorText = await response.text()
console.log(
`${operationName} failed with status ${response.status} (attempt ${attempt + 1}/${maxRetries}), waiting ${delay}ms before retrying...`
)
lastError = `${operationName} failed. Got status: ${response.status}, ${errorText}`
await new Promise((resolve) => setTimeout(resolve, delay))
continue
}
// Last attempt failed, capture error
lastError = `${operationName} failed. Got status: ${
response.status
}, ${await response.text()}`
}
// All retries exhausted
throw new Error(lastError)
}
export async function resetProject({
teamId,
projectName,
token,
disableDeploymentProtection = true,
}) {
// `teamId`, `projectName` and `token` together decide which project gets
// deleted and recreated, so all three are deliberately required. Defaulting
// any of them meant a caller passing an unset value silently destroyed some
// other project instead of the one it meant to reset.
if (isAbsent(teamId)) {
throw new Error('resetProject requires a teamId.')
}
if (isAbsent(projectName)) {
throw new Error(`resetProject requires a projectName for team ${teamId}.`)
}
if (isAbsent(token)) {
throw new Error(`resetProject requires a token for team ${teamId}.`)
}
console.log(`Resetting project ${teamId}/${projectName}`)
// TODO: error/bail if existing deployments are pending
await fetchWithRetry(
`https://vercel.com/api/v8/projects/${encodeURIComponent(
projectName
)}?teamId=${teamId}`,
{
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
},
{
acceptableStatuses: [404], // 404 is acceptable (project doesn't exist)
operationName: 'Delete project',
}
)
// Retry logic for project creation since deletion may be async
const createRes = await fetchWithRetry(
`https://vercel.com/api/v8/projects?teamId=${teamId}`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: projectName,
framework: 'nextjs',
resourceConfig: {
buildMachineType: 'enhanced',
},
environmentVariables: [
{
key: 'VERCEL_FORCE_NO_BUILD_CACHE_UPLOAD',
value: '1',
type: 'plain',
target: ['production', 'preview', 'development'],
},
],
}),
},
{
operationName: 'Create project',
}
)
const { id: projectId } = await createRes.json()
if (!projectId) {
throw new Error("Couldn't get projectId from create project response")
}
if (disableDeploymentProtection) {
console.log('Disabling deployment protection...')
await fetchWithRetry(
`https://vercel.com/api/v8/projects/${encodeURIComponent(
projectId
)}?teamId=${teamId}`,
{
method: 'PATCH',
headers: {
'content-type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
ssoProtection: null,
passwordProtection: null,
}),
},
{
operationName: 'Disable deployment protection',
}
)
}
console.log(
`Successfully created fresh Vercel project ${teamId}/${projectName}`
)
}