I switched all my projects to Monorepos this year, and I use Cloudlfare Pages intensively for hosting static websites. There is one small problem with Cloudlfare’s Github integration: you can only connect one project per repository. In a monorepo where I provide pages like a landing page, documentation and an app, this is a problem.

It’s good that you can also upload the assets directly. The problem with that: you lose some nice benefits:

  • Stable preview URL
  • PR comment with the links

And those are already quite nice benefits ;) So I started to rebuild the benefits myself. With the help of Wrangler and the Cloudflare API it is not difficult to achieve everything.

To get a stable URL I originally assumed that I would just get an updated stable URL with the help of the branch name.

npm i -g wrangler
cd ${{ env.ROOT_DIRECTORY }}
CF_PUBLISH_OUTPUT=$(wrangler pages deploy ${{ env.DIST_DIRECTORY }} --project-name=${{ env.CLOUDFLARE_PAGES_PROJECT_NAME }} --branch="${{ steps.extract_branch.outputs.branch }}" --commit-dirty=true --commit-hash=${{ steps.meta.outputs.sha_short }} | grep complete)
echo "cf_deployments=$CF_PUBLISH_OUTPUT" >> "$GITHUB_OUTPUT"

Unfortunately, after a few test runs, I found that this is not the case. I didn’t deal with it further at this point, but tried to take an alternative approach:

  • Search all deployments to a branch on every run.
  • Delete all deployments
  • Upload new assets

For reading and deleting deployments I wrote a small TypesScript program that I run in the CI pipeline.

Read out all previous branch deployments:

public async getDeployments(options?: { branch?: string }) {
    const { branch } = options || {}
    const { accountId, projectName, apiToken } = this.config

    const response = await fetch(
      `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/deployments`,
      {
        headers: {
          Authorization: `Bearer ${apiToken}`,
        },
      },
    ).then((res) => res.json())

    let deployments = response.result

    if (branch) {
      deployments = deployments.filter(
        (deployment) =>
          deployment.deployment_trigger?.metadata?.branch === branch,
      )
    }

    return deployments
}

Deleting a deployment:

public async deleteDeployment(id: string) {
    const { accountId, projectName, apiToken } = this.config
    await fetch(
      `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/deployments/${id}?force=true`,
      {
        method: 'DELETE',
        headers: {
          Authorization: `Bearer ${apiToken}`,
        },
      },
    ).then((res) => res.json())
}

The approach has another advantage: deployments that are no longer current are always cleaned up, since I am no longer interested in them anyway.

Published on 2023-09-30, last updated on 2025-08-28 by Adam
Comments or questions? Open a new discussion on github.
Adam Urban

Adam Urban is fullstack engineer, loves serverless and generative art, and is building side projects like weeklyfoo.com, flethy.com and diypunks.xyz in his free time.

Related posts