Tobias Davis

Designing better software systems.

Articles | Contact | Newsletter | RSS

Site logo image

Deploying from GitHub to Cloudflare with Wrangler

During a recent project with PowerOutage.us we wanted to use Cloudflare’s Preview URLs to get per-branch (technically per pull request) URLs for our Workers (to be clear we were not using Cloudflare Pages). It turned out to be a little trickier than I anticipated.

The Fundamentals #

The core command we have to work with is versions upload, specifically this:

wrangler versions upload \
--env $ENVIRONMENT \
--tag $VERSION_TAG \
--preview-alias $PREVIEW_ALIAS

This will deploy a Worker that is configured like the Worker in the same environment, e.g. similar variables and bindings, but with a special URL.

One additional annoyance is that wrangler doesn’t give you the generated URLs in any useful output stream, so we have to use this bash trick to capture the output of Wrangler:

TEMP_FILE=$(mktemp)
wrangler versions upload \
--env $ENVIRONMENT \
--tag $VERSION_TAG \
--preview-alias $PREVIEW_ALIAS \
| tee "${TEMP_FILE}"
WRANGLER_OUTPUT=$(cat "${TEMP_FILE}")

The tee lets us pipe the logs into the temp file while also piping to the GitHub Workflow logger, that way we see logs stream in instead of all showing up at the end if you tried WRANGLER_OUTPUT=$(wrangler ...) instead.

The Details #

This assumes you only have one application in your repo to deploy. If you are running a monorepo with multiple deployable apps, you will need to adapt these things.

Dependencies #

Make sure you have wrangler as a dependency, so that you can do npx wrangler in your scripts.

Workflow #

For preview URLs you probably want it on pull requests:

on:
pull_request:
branches:
- "main" # only PRs into main, for example

You of course need to checkout your repo, install dependencies, and so on:

jobs:
preview:
environment: staging
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ">=24.13.0"
- run: npm install
# ... run tests and so on if you want ...

Then we’ll add this step which does the heavy lifting:

jobs:
preview:
steps:
# ... previous steps, then ...
- name: Preview URLs
id: preview_urls
env:
ENVIRONMENT: staging # or whatever
CLOUDFLARE_ACCOUNT_ID: $
CLOUDFLARE_API_TOKEN: $
PULL_REQUEST_ID: $
PULL_REQUEST_NUMBER: $
run: |
VERSION_TAG=pr:${PULL_REQUEST_NUMBER}
PREVIEW_ALIAS=preview-$PULL_REQUEST_ID
# Capture output of Wrangler to a temp file
TEMP_FILE=$(mktemp)
wrangler versions upload --env $ENVIRONMENT --tag $VERSION_TAG --preview-alias $PREVIEW_ALIAS | tee "${TEMP_FILE}"
WRANGLER_OUTPUT=$(cat "${TEMP_FILE}")
# Now we grep for the generated preview URL, which is the per-deploy version
PREVIEW_URL=$(echo "${WRANGLER_OUTPUT}" | grep "^Version Preview URL: " | sed 's/^Version Preview URL: //')
# And the same for the alias, which is the per-PR version
PREVIEW_ALIAS_URL=$(echo "${WRANGLER_OUTPUT}" | grep "^Version Preview Alias URL: " | sed 's/^Version Preview Alias URL: //')
# Then echo them out so we can get at them in next steps
echo "PREVIEW_DEPLOY_URL=${PREVIEW_URL}" >> $GITHUB_OUTPUT
echo "PREVIEW_PR_URL=${PREVIEW_ALIAS_URL}" >> $GITHUB_OUTPUT

Some notes:

Technically that’s it, the hard part is done: you’ve deployed your Worker and you’ve got a preview URL.

Add a Comment #

Since this is running for a PR, one thing I like to do is add a comment to the PR with the URL.

And since I always end up having a bunch of back and forth, I like to upsert the comment, so that there’s only the one.

jobs:
preview:
steps:
# ... previous steps, then ...
- name: Add PR Comment
uses: actions/github-script@v8
env:
PREVIEW_DEPLOY_URL: $
PREVIEW_PR_URL: $
with:
script: |
const { PREVIEW_DEPLOY_URL, PREVIEW_PR_URL } = process.env;
const body = [
`🛠️ As of ${context.payload.pull_request?.head?.sha} this PR has the following URLs:`,
`- For this PR: ${PREVIEW_PR_URL}`,
`- For the most recent deploy: ${PREVIEW_DEPLOY_URL}`,
'<!-- workflow-pr-bot -->'
].join('\n');
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c =>
c.user.type === "Bot" &&
c.body.includes("<!-- workflow-pr-bot -->")
);
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}

Some notes:


Your thoughts and feedback are always welcome! 🙇‍♂️