Skip to content

Commit

Permalink
feat: support filtering on deployment trigger and keep parameters (#33)
Browse files Browse the repository at this point in the history
By default delete all deployments. If keep is specified keep the
assigned number of deployments by slicing the filtered deployments list.

In order to test the action locally avoid using kebab case for input
name to allow POSIX compliant environment variables. I.e. use
deployment_trigger instead of deployment-trigger so we can write
INPUT_DEPLOYMENT_TRIGGER while testing or running in Docker.

Default to empty string for deployment type.

Signed-off-by: Snorre Magnus DavΓΈen <snorre.magnus.davoen@adventuretech.no>
Co-authored-by: Zao Soula <contact@zaosoula.fr>
  • Loading branch information
snorremd and zaosoula authored Jan 19, 2023
1 parent b15b669 commit 33716c8
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 62 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@ typings/

# next.js build output
.next

testrun.sh
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@

### Inputs

| Name | Description | Required |
| --- | --- | --- |
| `token` | The Cloudflare API token to use for authentication. | Yes |
| `project` | The Cloudflare project name to delete deployments from. | Yes |
| `account` | The Cloudflare account id to delete deployments from. | Yes |
| `branch` | The branch to delete deployments from. | Yes |
| `since` | Filter deployments to those deployed after since, in ISO8601 format | No |
| Name | Description | Required | Default |
| -------------------- | -------------------------------------------------------------------- | -------- | ---------- |
| `token` | The Cloudflare API token to use for authentication. | Yes | |
| `project` | The Cloudflare project name to delete deployments from. | Yes | |
| `account` | The Cloudflare account id to delete deployments from. | Yes | |
| `branch` | The branch to delete deployments from. | Yes | |
| `since` | Filter deployments to those deployed after since, in ISO8601 format | No | 1970-01-01 |
| `deployment_trigger` | Filter deployments to those deployed by the given deployment trigger | No | |

#### Token

Use the worker template as a base when generating the token in the Cloudflare dashboard.
Expand Down Expand Up @@ -130,4 +132,4 @@ Note: We recommend using the `--license` option for ncc, which will create a lic

Your action is now published! :rocket:

See the [versioning documentation](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md)
See the [versioning documentation](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md)
7 changes: 7 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ inputs:
description: 'Filter deployments to those deployed after since, in ISO 8601 format'
default: '1970-01-01T00:00:00Z'
required: false
deployment_trigger:
description: 'The type of deployement to filter with (e.g. github:push or ad_hoc)'
required: false
keep:
description: 'The number of deployments to keep (default: 0)'
default: '0'
required: false
runs:
using: 'node16'
main: 'dist/index.js'
85 changes: 61 additions & 24 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ const { main } = require('./main')
const project = core.getInput('project')
const account = core.getInput('account')
const branch = core.getInput('branch')
const since = core.getInput('since')
const since = core.getInput('since', { required: false })
const deploymentTriggerType = core.getInput('deployment_trigger', { required: false })
const keep = core.getInput('keep', { required: false })

// Sensitive input parameters to authenticate with the API
const token = core.getInput('token')
core.setSecret(token) // Ensure Cloudflare token does not leak into logs

try {
main(project, account, branch, since, token)
main({ project, account, branch, since, token, deploymentTriggerType, keep })
} catch (error) {
core.setFailed(error.message)
}
79 changes: 57 additions & 22 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,21 @@ const http = require('@actions/http-client')
const apiUrl = 'https://api.cloudflare.com/client/v4'
const httpClient = new http.HttpClient('Cloudflare Pages Deployments Delete Action')

// Fetch list of Cloudflare deployments
// @param {string} project
// @param {string} account
// @param {Date} since
// @param {string} token
/**
* Return a promise that resolves after the delay
*
* @param {number} delay
*/
const wait = delay => new Promise(resolve => setTimeout(resolve, delay))

/**
* Fetch list of Cloudflare deployments
*
* @param {string} project
* @param {string} account
* @param {Date} since
* @param {string} token
*/
const getDeployments = async (project, account, since, token) => {
core.startGroup('Fetching deployments')

Expand All @@ -26,10 +36,15 @@ const getDeployments = async (project, account, since, token) => {
let dateSinceNotReached

do {
core.info(`Fetching page ${page} of deployments`)
core.info(
`Fetching page ${page} / ${
resultInfo ? Math.ceil(resultInfo.total_count / resultInfo.per_page) : '?'
} of deployments`
)
/** @type {ListDependenciesResponse} */

const res = await httpClient.getJson(
`${apiUrl}/accounts/${account}/pages/projects/${project}/deployments?page=${page}`,
`${apiUrl}/accounts/${account}/pages/projects/${project}/deployments?page=${page}&sort_by=created_on&sort_order=desc`,
{
Authorization: `Bearer ${token}`,
}
Expand All @@ -50,6 +65,7 @@ const getDeployments = async (project, account, since, token) => {
deployments.push(...nextResults)
hasNextPage = page++ < Math.ceil(resultInfo.total_count / resultInfo.per_page)
dateSinceNotReached = new Date(lastResult.created_on).getTime() >= since.getTime()
await wait(250) // Api rate limit is 1200req/5min <--> 4req/s
} while (hasNextPage && dateSinceNotReached)

core.endGroup()
Expand All @@ -59,9 +75,11 @@ const getDeployments = async (project, account, since, token) => {
/**
* @param {import('./typings/dependencies').Deployment} deployment
*/
const deleteDeployment = async (project, account, token, deployment) => {
const deleteDeployment = async (project, account, token, force, deployment) => {
const res = await httpClient.del(
`https://api.cloudflare.com/client/v4/accounts/${account}/pages/projects/${project}/deployments/${deployment.id}`,
`https://api.cloudflare.com/client/v4/accounts/${account}/pages/projects/${project}/deployments/${deployment.id}${
force ? '?force=true' : ''
}`,
{
authorization: `Bearer ${token}`,
}
Expand All @@ -70,6 +88,8 @@ const deleteDeployment = async (project, account, token, deployment) => {
/** @type {DeleteDependencies} */
const body = JSON.parse(await res.readBody())

await wait(250) // Api rate limit is 1200req/5min <--> 4req/s

if (!body.success) {
throw new Error(body.errors.map(e => e.message).join('\n'))
}
Expand All @@ -78,6 +98,11 @@ const deleteDeployment = async (project, account, token, deployment) => {
return null
}

/**
* Transform a date string to a Date object
*
* @param {string} input
*/
const sinceDate = input => {
if (input === '') return new Date(0)

Expand All @@ -90,10 +115,21 @@ const sinceDate = input => {
return date
}

const main = async (project, account, branch, since, token) => {
const parseNumber = input => {
core.info('Keep value: ' + input)
const number = Number.parseInt(input, 10)
if (isNaN(number)) {
throw new Error(`Invalid keep value: ${input}`)
}

return number
}

const main = async ({ project, account, branch, since, token, deploymentTriggerType, keep }) => {
core.info('πŸƒβ€β™€οΈ Running Cloudflare Deployments Delete Action')

const sinceSafe = sinceDate(since)
const keepNumber = parseNumber(keep)

core.info(`Fetching deployments for project ${project} since ${sinceSafe.toISOString()}`)

Expand All @@ -105,31 +141,30 @@ const main = async (project, account, branch, since, token) => {
// Filter deployments by branch name
const branchDeployments = deployments
.filter(d => new Date(d.created_on).getTime() >= sinceSafe.getTime())
.filter(d => d.deployment_trigger.type === 'github:push')
.filter(d => deploymentTriggerType === '' || d.deployment_trigger.type === deploymentTriggerType)
.filter(d => d.deployment_trigger.metadata.branch === branch)
.slice(1)
.slice(keepNumber)

core.info(`πŸͺ“ Deleting ${branchDeployments.length} deployments matching branch ${branch}`)

// Delete all deployments for the branch
const deleted = await Promise.allSettled(branchDeployments.map(d => deleteDeployment(project, account, token, d)))

core.startGroup('Deleted Deployments')

// Log the results of the deletion, index should match
deleted.forEach((d, i) => {
if (d.status === 'fulfilled') {
// Delete all deployments for the branch
let deleted = 0
for (let i = 0; i < branchDeployments.length; i++) {
try {
await deleteDeployment(project, account, token, keep === 0, branchDeployments[i])
deleted = deleted + 1
core.info(`🟒 Deleted deployment ${branchDeployments[i].id}`)
} else {
} catch (e) {
core.error(`πŸ”΄ Failed to delete deployment ${branchDeployments[i].id}`)
}
})
}
core.endGroup()

core.info('πŸŽ‰ Finished Cloudflare Deployments Delete Action')

// Used mainly for testing purposes
return deleted.length
return deleted
}

exports.main = main
Loading

0 comments on commit 33716c8

Please sign in to comment.