Metadata-Version: 2.5
Name: make-cloudflare
Version: 0.4.0
Summary: Cloudflare Pages direct upload for mkrun -- deploy a built site with no Node and no wrangler.
Project-URL: Homepage, https://make.optersoft.com
Author-email: "Optersoft, S.L." <david@optersoft.com>
License-Expression: MIT OR Apache-2.0
Keywords: cloudflare,deploy,direct upload,mk,mkrun,pages
Requires-Python: >=3.11
Requires-Dist: blake3>=1.0
Requires-Dist: mkrun>=0.4.1
Description-Content-Type: text/markdown

# make-cloudflare

Cloudflare Pages **direct upload** for [`mkrun`](https://pypi.org/project/mkrun/), in
Python: publish a built directory with no Node on the machine and no `wrangler`
in the pipeline.

```python
# Makefile.py in a consuming repo
# /// script
# requires-python = ">=3.11"
# dependencies = ["mkrun>=0.4.1", "make-cloudflare>=0.3"]
# ///
from make_cloudflare import cloudflare  # importing is what registers the group
```

```console
$ mk cloudflare.deploy site/dist --project mkrun     # production (branch main)
$ mk cloudflare.deploy site/dist --branch try        # a preview, on its own URL
$ mk cloudflare.deploy dist --functions functions    # with Pages Functions
$ mk cloudflare.projects                             # what the account has
$ mk cloudflare.deployments --limit 5                # one project's recent ones
$ mk cloudflare.prune --keep 10                      # delete all but the newest ten
$ mk -n cloudflare.deploy site/dist                  # the plan; nothing is sent
```

Credentials are the two variables wrangler already reads, so a repository that
deploys from CI needs no new secret: `CLOUDFLARE_ACCOUNT_ID` and
`CLOUDFLARE_API_TOKEN` (a token with **Cloudflare Pages: Edit**). The token's
name ends in `TOKEN`, so mkrun treats it as a credential — it is withheld from
the environment until a task asks for it, redacted in everything printed, and
readable from the encrypted store. The project can come from `--project` or from
`CLOUDFLARE_PAGES_PROJECT` in the repo's env layer.

**Every deploy prunes the project to its newest 10 deployments** (`--keep N`
to change it, `--keep 0` to keep them all; `pages.deploy(..., keep=)` from
Python). Pages never deletes one by itself, and each stays reachable on its own
`<hash>.<project>.pages.dev` URL forever. The live production deployment is
never deleted, however old; a pruned preview that was still its branch's alias
takes that branch URL with it.

**Pages Functions go up with the deployment.** `--functions functions` (or
`pages.deploy(..., functions=)`) names the tree. It sits **beside** the built
directory, as wrangler expects, and `/api/...` answers from the same project and
origin. Bundling it is a build step, not an upload, so that one step is
`wrangler pages functions build --outfile` (from PATH, or through `npx`). It
writes exactly the `_worker.bundle` the deployment takes, plus
`functions-filepath-routing-config.json` and a generated `_routes.json`; the
site's own `_routes.json` wins. Everything else is still the four calls below.
Bindings (D1, KV) and secrets are settings on the Pages project, never part of
the upload.

**Those settings are one call.** `pages.configure(project, d1={"DB": "<id>"},
vars={...}, compatibility_date="2026-10-01")` PATCHes the project's
`production` (or `preview`) configuration. Only what you pass is sent, and
Cloudflare merges it in. It takes effect on the **next** deployment. Secrets
are not set this way: a plain variable is shown in the dashboard, so a secret
goes through `wrangler pages secret put`.

## Why this exists

`wrangler pages deploy` is four HTTPS calls and a hash. Reaching them through
`npx` costs a Node toolchain in repositories that otherwise have none — a Rust
one, a [frontage](https://github.com/optersoft/frontage) one — and a
`node_modules` in the deploy image of every one of them.

```
POST /accounts/<acct>/pages/projects/<p>/upload-token   -> a short-lived JWT
POST /pages/assets/check-missing                        -> what is not stored yet
POST /pages/assets/upload                               -> the files, batched
POST /pages/assets/upsert-hashes
POST /accounts/<acct>/pages/projects/<p>/deployments    -> the manifest
```

Three things make a naive port wrong, and all three fail **silently** — every
call returns 200 and the site serves the old bytes, or none:

- **The hash is BLAKE3 of a strange input**: the base64 *text* of the contents
  with the extension (no dot) appended, first 32 hex characters. Not of the
  bytes, and not SHA-256. `tests/test_pages.py` locks it with a golden value.
- **`_headers`, `_redirects` and `_routes.json` are not assets.** They are
  fields on the deployment. Walk them in as ordinary files and the deploy
  reports success while the site's CSP stops applying.
- **`check-missing`, `upload` and `upsert-hashes` take the JWT, not the account
  token, and carry no `/accounts/<id>` prefix** — the JWT already names the
  account.

## What it does not do

**Advanced-mode `_worker.js`.** A `_worker.js` (or a `functions/`) *inside*
the built directory is refused by name: it would otherwise be served as a
static file. Pages Functions go beside it, through `--functions`.

**Steps 2 and 3 are undocumented.** They exist because wrangler uses them, and
Cloudflare can change them without a changelog; the documented `deployments`
endpoint alone cannot upload a file. That is the real cost of dropping
wrangler, and it is the reason this is a small module with a golden test rather
than a wrapper nobody reads.

Nothing here is specific to any owner or account. The group name is
`cloudflare`, and it merges with a repo's own `cloudflare.*` tasks — groups are
namespaces — so only a same-named task collides; this package claims `deploy`,
`projects`, `deployments` and `prune`, nothing else. `blake3` is the one dependency, and
it lives here rather than in the runner so `mkrun` itself stays dependency-free.
