cloudflare#
Deploy to Cloudflare with wrangler: resolve credentials, derive the deploy config and the version string, confirm a destructive environment, run wrangler, and prove afterwards that what answers is what was just deployed.
This module is one half of a pair. The other half is a reusable GitHub Actions workflow that supplies credentials, the environment gate and the kill switch. Both call the same deploy sequence, so a deploy from a laptop and a deploy from CI are not two implementations that drift apart.
The mechanism is a contract of CF_DEPLOY_* environment variables. When CI has already resolved a value it exports it, and the function here returns it verbatim instead of computing its own. A single run therefore never has two live definitions of the version string or the base URL.
Return codes#
Every function in this module uses the same set:
| Code | Meaning |
|---|---|
0 |
success |
1 |
the operation failed, or a required value could not be resolved |
2 |
bad arguments, or a value outside its allowed set |
3 |
a required tool is missing |
4 |
no usable Cloudflare credentials |
5 |
refused - a protected environment was not confirmed |
No function here calls exit. These are library functions, and a sourced function that exits takes the caller's shell with it.
Functions#
- cloudflare_wrangler args...
- Purpose: Run wrangler through whichever runner the project implies.
- Behavior: Uses
CLOUDFLARE_WRANGLER_CMDwhen set; otherwise prefers the project's own lockfile (pnpm exec wrangler,yarn wrangler,npm exec -- wrangler), then awrangleronPATH, thennpx --yes wrangler@$CLOUDFLARE_WRANGLER_VERSION. Prefers the lockfile because that is the version the project was tested against; thenpxfallback is convenience, not supply-chain hygiene. The API token is passed through the environment and never in argv, where any other user on the machine could read it withps. -
Returns: wrangler's own exit status;
2with no arguments;3when no runner is available. -
cloudflare_credentials_ok
- Purpose: Establish that a deploy can authenticate before doing any work.
- Behavior: Succeeds when
CLOUDFLARE_API_TOKENis set, or whenwrangler whoamireports an authenticated interactive session. -
Returns:
0usable credentials;4none (the message names bothCLOUDFLARE_API_TOKENandwrangler login);3when wrangler cannot run. -
cloudflare_account_id [env_file]
- Purpose: Print the Cloudflare account id a deploy should target.
- Behavior: Reads
CLOUDFLARE_ACCOUNT_IDfrom the environment, then fromenv_file(default.env) viaresolve_env_value. This check exists because an empty account id is not an error to wrangler: it falls back to resolving the account from the token, which is correct for a token scoped to one account and a coin toss otherwise. A deploy that lands on the wrong account looks exactly like a deploy that worked. -
Returns:
0and prints the id;1when neither source has one. -
cloudflare_version_string [version_file]
- Purpose: Print the version string a deploy should advertise.
- Behavior: Returns
CF_DEPLOY_VERSIONverbatim when exported, so in CI the workflow's definition is the only live one and this is a pure fallback. Otherwise readsversion_file(defaultVERSION), strips whitespace, and appends-<7-char commit sha>. Outside a git checkout it appends-unknownand warns, because a version that cannot identify a commit should say so rather than look precise. -
Returns:
0;1when the file is unreadable or empty;2when it does not exist. -
cloudflare_deploy_config wrangler-config dist-dir
- Purpose: Print the path of the deploy config a bundler-generated build emits.
- Behavior: Reads the top-level
nameout of the authored wrangler config and prints<dist-dir>/<name with hyphens as underscores>/wrangler.json. Derived rather than hardcoded, so the two stay in step when the worker is renamed.CLOUDFLARE_DEPLOY_CONFIGoverrides the derivation entirely. -
Returns:
0;1when the config is unreadable or has no top-levelname;2when an argument is missing. -
cloudflare_base_url environment [env_file]
- Purpose: Print the base URL to smoke-test after a deploy.
- Behavior: Resolution order is
CF_DEPLOY_BASE_URL(CI has already decided), then<ENVIRONMENT>_BASE_URL, thenBASE_URL, the latter two read fromenv_file(default.env). The per-environment name is what lets one.envdescribe a laptop's view of several environments. Uppercasing is done withtr, not${x^^}, which is bash 4. -
Returns:
0and prints the URL;1when nothing is found (the message names the exact variable it wanted);2whenenvironmentis empty. -
cloudflare_confirm_environment environment [--yes]
- Purpose: Require a deliberate confirmation before deploying to a protected environment.
- Behavior: Only environments listed in
CLOUDFLARE_PROTECTED_ENVS(defaultproduction, space separated) prompt at all.--yes, orCLOUDFLARE_DEPLOY_YES=1, confirms without a prompt. On a terminal the operator must type the environment name exactly. With no terminal on stdin it returns5immediately rather than blocking on a read nobody can answer - an unattended run that hangs to its timeout is worse than one that says what it needed. -
Returns:
0confirmed;2empty environment;5declined, or unattended without--yes. -
cloudflare_smoke_test base-url expected-version [status-path] [field]
- Purpose: Prove that the deploy took effect, not merely that something answers.
- Behavior: Two separate assertions. First
curlthe health path (CLOUDFLARE_HEALTH_PATH, default/health) with retries, since a deploy that has just landed may take a moment to be reachable everywhere. Then, whenstatus-pathis given, fetch it and compare itsfield(defaultversion) againstexpected-version. Without the second assertion a smoke test passes against the previous release, which is the failure it exists to catch - so reachability-only runs log a warning saying so. JSON is read withjq, elsepython3, else agrep/sedfallback. -
Returns:
0pass;1unreachable, no such field, or the wrong version is live;2bad arguments, or a base URL with a scheme other thanhttporhttps(a base URL without a scheme is read ashttp);3nocurl. -
cloudflare_deploy [options] [-- extra wrangler args...]
- Purpose: The single deploy sequence, shared by
./dev deploy cloudflareand by CI. - Options:
--env NAME(required),--config PATH,--dist DIR,--source CONFIG,--build-command CMD,--command KIND,--version-file FILE,--status-path PATH,--yes,--dry-run,--no-smoke. Anything after--is passed to wrangler untouched. - Behavior: Validates
--commandagainst the allowlistdeploy/versions upload/pages deploybefore anything else, because that value becomes argv. Then confirms the environment, checks credentials, resolves and exports the account id, runs the build command withCLOUDFLARE_ENVexported, resolves the version, derives the deploy config when--distwas given, asserts the config exists, runs wrangler, and smoke-tests.--dry-runstops before any credential check or wrangler call. - Returns:
0;1a step failed, or the account id could not be resolved;2a bad option, a missing option value, or an unknown command;3wrangler could not be run;4no usable credentials;5a protected environment was not confirmed. - Every value-taking option is checked for its value before it is consumed.
--envwith nothing after it returns2with a message; it does not consume the next option, and it does not hang.
Environment#
| Variable | Effect |
|---|---|
CLOUDFLARE_API_TOKEN |
The API token. Read by wrangler from the environment; never placed in argv. |
CLOUDFLARE_ACCOUNT_ID |
The account to deploy to. Checked explicitly, because wrangler treats an empty value as "resolve from the token". |
CLOUDFLARE_WRANGLER_CMD |
Overrides how wrangler is invoked, e.g. pnpm exec wrangler. |
CLOUDFLARE_WRANGLER_VERSION |
Version used by the npx fallback. Defaults to CI_DEFAULT_WRANGLER_VERSION, which this module sources from ci_defaults so the pin has one home. |
CLOUDFLARE_PROTECTED_ENVS |
Space-separated environments needing a typed confirmation. Default production. Matched literally - globbing is disabled while the list is read, so the value cannot be altered by files in the working directory. |
CLOUDFLARE_DEPLOY_YES |
Set to 1 to confirm a protected environment without a prompt. |
CLOUDFLARE_HEALTH_PATH |
Reachability path for the smoke test. Default /health. |
CLOUDFLARE_DEPLOY_CONFIG |
Names the generated deploy config directly, skipping derivation. |
CF_DEPLOY_VERSION |
Set by CI. Taken verbatim by cloudflare_version_string. |
CF_DEPLOY_BASE_URL |
Set by CI. Taken verbatim by cloudflare_base_url. |
Example#
source helpers.sh
shlib_import cloudflare
# A laptop deploy to a non-protected environment.
cloudflare_deploy \
--env staging \
--build-command "npm run build" \
--source wrangler.toml \
--dist dist \
--status-path /api/status
# Production asks for the environment name to be typed, unless --yes is given.
cloudflare_deploy --env production --config wrangler.toml --yes
# Cloudflare Pages: no --env, and the target comes after --.
cloudflare_deploy --env staging --command "pages deploy" -- \
dist --project-name example-site
Dependencies#
curl- the smoke test.- wrangler, through a package manager, a
PATHbinary, ornpx. jqorpython3for JSON; agrep/sedfallback is used when neither is present.lib/logging.sh, andlib/env.shforresolve_env_value.
Pushing KV entries or seed data#
This module deliberately does not do it, and will not gain a function for it.
If a deploy also has to write KV entries, seed a namespace, or upload data, put
that in your own script - alongside cloudflare_deploy, not inside it.
Why it stays out:
- It needs a wider token. Deploying needs Workers Scripts -> Edit; writing KV needs Workers KV Storage -> Edit as well. Binding a namespace does not. A helper that writes data would push every caller toward the broader token whether or not they need it.
- It is the one step that cannot be undone by the next deploy. A Worker is replaced by the deploy after it. Overwritten data is gone.
- "Push the data" is not one operation. Replacing a namespace, upserting a few keys, uploading a file and applying a migration are different jobs with different failure modes. A single function covering all of them would be a shell command with extra steps.
If you write one, four things to get right:
- Guard it with a string comparison, never a bare truthiness test. In a CI
job, an output crossing a job boundary is a string, so a guard like
if: needs.x.outputs.push_datais true even when the value is the literal"false"- every non-empty string is truthy. The same shape in bash is[[ -n "$PUSH_DATA" ]], which is true for"false"too. Compare against the value you mean:[[ "$PUSH_DATA" = "true" ]]. - Make it idempotent, or make it refuse. Deploys get re-run - a retried job, a re-pushed tag, a person clicking the button again. A push that overwrites unconditionally gives a different result each time. If it cannot be idempotent, detect existing data and stop rather than clobber it.
- It is not atomic with the deploy. wrangler deploying and your data landing are two operations with no shared transaction. Decide which order fails better for your service and write that reasoning down next to the code.
- Run it under the same confirmation.
cloudflare_confirm_environmentis what stops an unattended production deploy; a data push that runs before or outside it has no such gate. Call it first, or callcloudflare_deployand do the data push only after it returns0.
source helpers.sh
shlib_import cloudflare
cloudflare_confirm_environment production "$@" || exit $?
cloudflare_deploy --env production --config wrangler.toml --yes || exit 1
# Only after the Worker is live and the smoke test passed.
if [[ "${PUSH_DATA:-false}" = "true" ]]; then
./scripts/seed-kv.sh # yours, idempotent, and it may refuse
fi
Notes#
- Cloudflare Pages is supported as an input combination (
--command "pages deploy"), not as a separate code path. The Workers path is the one exercised end to end. --dry-runvalidates and builds but never calls the Cloudflare API, so it proves the config and the bundle, not bindings, routes, or account access.--configand--distare not passed topages deploy, which does not take them in this module's invocation. Supplying one logs a warning rather than dropping it silently.- The smoke test uses
curl --retry-all-errorswhere available. That option needs curl 7.71 (2020); on an older curl the flag is omitted rather than failing, because an unknown option would otherwise read as "the service did not answer" and fail a deploy that worked. cloudflare_deployexportsCLOUDFLARE_ACCOUNT_IDinto the calling shell once resolved, so a later wrangler call in the same script sees it.