casablanque-code/cfzt is also a composite action wrapping zt up/zt down for the PR-preview flow (see the main README),
plus a GitHub Deployment + Deployment Status so the preview URL shows up in
the PR's own UI, not just build logs.
name: PR preview
on:
pull_request:
types: [opened, synchronize, reopened, closed]
permissions:
deployments: write # needed for the GitHub Deployments UI integration
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
if: github.event.action != 'closed'
# ... build your image / start the container on localhost:3000 ...
- uses: casablanque-code/cfzt@v0.10.0
if: github.event.action != 'closed'
with:
mode: up
name: pr-${{ github.event.number }}
port: '3000'
docker: 'true'
public: 'true' # or use `allow:` to restrict to your team
domain: example.com
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- uses: casablanque-code/cfzt@v0.10.0
if: github.event.action == 'closed'
with:
mode: down
name: pr-${{ github.event.number }}
domain: example.com
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}Pin to a tagged release (@v0.10.0) rather than @main — same
reproducibility reasoning as the cloudflared-version/cfzt-version
inputs below.
namemust match between theupanddowncalls — it's the tunnel name, the hostname prefix, and whatdownresolves against Cloudflare by name (see below).- State across runs:
upanddownnormally run in entirely separate, unrelated jobs — possibly weeks apart, on unrelated ephemeral runners — so there's no local state file fordownto find the way there would be if the same machine had runzt up. The action callszt down --remote, which resolves the tunnel directly from Cloudflare by name instead of requiring it in local state. This is opt-in on the CLI itself (a plainzt downstill requires local state) because a Cloudflare Tunnel has no "created by zt" marker the way a zt-managed DNS record does — resolving by name alone means whatever tunnel exists under that exact name gets deleted, which is exactly what a CI job tearing down its own preview wants and exactly what a mistypedzt downon your own machine doesn't. See SECURITY.md for the full risk note on--remote. - Deployment matching: the
downstep marks GitHub Deployments inactive by matching ontask: "cfzt:<name>", set at creation time — not justenvironment, sinceenvironmentdefaults tonamebut is user-overridable and can be reused across multiple previews. permissions: deployments: writeis required on the calling workflow for the Deployments UI integration — a composite action can't grant itself permissions. Setcreate-deployment: 'false'to skip that part entirely and just get the tunnel.- Reproducibility:
cloudflared-versionandcfzt-versioninputs pin the exactcloudflared/ztbuilds installed by the action, instead of always trackinglatest/main. Leaving them unset keeps the simpler default behavior, with a workflow warning nudging toward pinning. - If recording the GitHub Deployment fails after
zt upalready succeeded, the action runszt down --remoteas a cleanup step so the tunnel isn't left orphaned. Validate inputschecks forgh/jqonPATHup front with an actionable error if either is missing.- The Cloudflare API token written to
~/.zt-config.jsonfor the duration of the run is removed at the end of it (only if the run itself created the file).
See action.yml for the full list of inputs.