Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[admin]: deleting a project with high deployment count on Cloudflare through Nuxthub #460

Open
zackha opened this issue Feb 6, 2025 · 1 comment

Comments

@zackha
Copy link

zackha commented Feb 6, 2025

Hey @atinux,

Today, I encountered an issue related to deleting a project both in production and preview environments. The project had over 120 deployments in total. I enabled the option to delete the project's source files from Cloudflare, as shown below.

Image

However, after the project was completely deleted from Nuxthub, no warnings were displayed.

Upon checking Cloudflare, I noticed that the project had not been removed. When I tried to delete it manually, I received a notification stating that projects with a high number of deployments must be deleted according to the instructions provided in this link:
Cloudflare Known Issues - Deleting a Project with a High Number of Deployments.

Following the steps outlined in the documentation, I was finally able to delete the project from Cloudflare.

Could you please check if this is an expected behavior or if additional improvements can be made to handle this scenario?

Thank you in advance!

Copy link
Contributor

atinux commented Feb 7, 2025

Damn thank you for reporting this, I was not aware of this Cloudflare issue.

This is the code in the zip:

const fetch = require('node-fetch')
const { backOff } = require('exponential-backoff')

const CF_API_TOKEN = process.env.CF_API_TOKEN
const CF_ACCOUNT_ID = process.env.CF_ACCOUNT_ID
const CF_PAGES_PROJECT_NAME = process.env.CF_PAGES_PROJECT_NAME
const CF_DELETE_ALIASED_DEPLOYMENTS = process.env.CF_DELETE_ALIASED_DEPLOYMENTS

const MAX_ATTEMPTS = 5

const sleep = (ms) =>
  new Promise((resolve) => {
    setTimeout(resolve, ms)
  })

const headers = {
  Authorization: `Bearer ${CF_API_TOKEN}`,
}

/** Get the cononical deployment (the live deployment) */
async function getProductionDeploymentId() {
  const response = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/${CF_PAGES_PROJECT_NAME}`,
    {
      method: 'GET',
      headers,
    }
  )
  const body = await response.json()
  if (!body.success) {
    throw new Error(body.errors[0].message)
  }
  const prodDeploymentId = body.result.canonical_deployment.id
  if (!prodDeploymentId) {
    throw new Error('Unable to fetch production deployment ID')
  }
  return prodDeploymentId
}

async function deleteDeployment(id) {
  let params = ''
  if (CF_DELETE_ALIASED_DEPLOYMENTS === 'true') {
    params = '?force=true' // Forces deletion of aliased deployments
  }
  const response = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/${CF_PAGES_PROJECT_NAME}/deployments/${id}${params}`,
    {
      method: 'DELETE',
      headers,
    }
  )
  const body = await response.json()
  if (!body.success) {
    throw new Error(body.errors[0].message)
  }
  console.log(`Deleted deployment ${id} for project ${CF_PAGES_PROJECT_NAME}`)
}

async function listDeploymentsPerPage(page) {
  const response = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/${CF_PAGES_PROJECT_NAME}/deployments?per_page=10&page=${page}`,
    {
      method: 'GET',
      headers,
    }
  )
  const body = await response.json()
  if (!body.success) {
    throw new Error(`Could not fetch deployments for ${CF_PAGES_PROJECT_NAME}`)
  }
  return body.result
}

async function listAllDeployments() {
  let page = 1
  const deploymentIds = []

  while (true) {
    let result
    try {
      result = await backOff(() => listDeploymentsPerPage(page), {
        numOfAttempts: 5,
        startingDelay: 1000, // 1s, 2s, 4s, 8s, 16s
        retry: (_, attempt) => {
          console.warn(
            `Failed to list deployments on page ${page}... retrying (${attempt}/${MAX_ATTEMPTS})`
          )
          return true
        },
      })
    } catch (err) {
      console.warn(`Failed to list deployments on page ${page}.`)
      console.warn(err)

      process.exit(1)
    }

    for (const deployment of result) {
      deploymentIds.push(deployment.id)
    }

    if (result.length) {
      page = page + 1
      await sleep(500)
    } else {
      return deploymentIds
    }
  }
}

async function main() {
  if (!CF_API_TOKEN) {
    throw new Error('Please set CF_API_TOKEN as an env variable to your API Token')
  }

  if (!CF_ACCOUNT_ID) {
    throw new Error('Please set CF_ACCOUNT_ID as an env variable to your Account ID')
  }

  if (!CF_PAGES_PROJECT_NAME) {
    throw new Error(
      'Please set CF_PAGES_PROJECT_NAME as an env variable to your Pages project name'
    )
  }

  const productionDeploymentId = await getProductionDeploymentId()
  console.log(
    `Found live production deployment to exclude from deletion: ${productionDeploymentId}`
  )

  console.log('Listing all deployments, this may take a while...')
  const deploymentIds = await listAllDeployments()

  for (id of deploymentIds) {
    if (id === productionDeploymentId) {
      console.log(`Skipping production deployment: ${id}`)
    } else {
      try {
        await deleteDeployment(id)
        await sleep(500)
      } catch (error) {
        console.log(error)
      }
    }
  }
}

main()

From what I see, we will need to loop through all the deployments and delete them. I will add this issue to track, I am waiting for us to move to Workers so we can leverage the Queues for such tasks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants