next.js/scripts/reset-project.mjs
reset-project.mjs149 lines4.4 KB
import fetch from 'node-fetch'

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

/**
 * 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 = TEST_TEAM_NAME,
  projectName = TEST_PROJECT_NAME,
  token = TEST_TOKEN,
  disableDeploymentProtection = true,
}) {
  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}`
  )
}
Quest for Codev2.0.0
/
SIGN IN