curated_feed.deploy

Putting the built feed in front of the internet.

Rendering stops at the filesystem, because the build host and the publish host are separate (see docs/ARCHITECTURE.md, D6). This stage is the only one that knows where the feed is served from, and the only one whose failure is invisible: a reader that stops receiving items looks exactly like a quiet week.

Three things follow from that.

  • Uploading is gated on the content hash. Rendering records the hash of what it wrote; this records the hash of what it sent. A quiet day therefore costs nothing, and the host’s deployment history stays a list of real changes rather than a daily heartbeat.

  • Nothing is written unless an upload actually happened. A deployer that reports what it would do must not be recorded as having done it, or the next run would skip an upload that never took place.

  • No credential is read from configuration. The deploy tool resolves its own from the environment, exactly as the scorer does.

Deployment belongs to CI. The devcontainer can rehearse it, and does not have to be trusted with a token to be useful.

Attributes

Exceptions

DeployError

Raised when the built feed could not be uploaded.

Classes

Deployer

Anything that can publish a directory of files.

NullDeployer

Explains that no host is configured, and uploads nothing.

WranglerDeployer

Uploads a directory to a Cloudflare Worker with wrangler deploy.

Functions

wrangler_package(→ str)

The wrangler npx is asked for.

run_command(→ CompletedCommand)

Run a command to completion, capturing what it printed.

build_command(→ list[str])

The wrangler invocation for one upload.

make_deployer(→ Deployer)

The deployer named in configuration.

rendered_hash(→ str)

The content hash of the build on disk, as the last render recorded it.

deployed_hash(→ str | None)

The content hash of the last successful upload, or None if there is none.

build_parser(→ argparse.ArgumentParser)

main(→ int)

Module Contents

curated_feed.deploy.DEPLOY_META = 'deploy.json'
curated_feed.deploy.WRANGLER_PACKAGE_ENV = 'CURATED_FEED_WRANGLER'
curated_feed.deploy.DEFAULT_WRANGLER_PACKAGE = 'wrangler@4'
curated_feed.deploy.ERROR_LINES = 10
curated_feed.deploy.wrangler_package(env: collections.abc.Mapping[str, str] | None = None) → str[source]

The wrangler npx is asked for.

An empty variable means the default rather than an empty argument, as an empty [score] effort means “leave it out” rather than “send nothing”.

A bare package name is npx’s “run what is already installed” form, which is what the image sets it to: nothing is resolved and nothing is downloaded.

>>> wrangler_package({})
'wrangler@4'
>>> wrangler_package({"CURATED_FEED_WRANGLER": "wrangler"})
'wrangler'
exception curated_feed.deploy.DeployError[source]

Bases: Exception

Raised when the built feed could not be uploaded.

class curated_feed.deploy.Deployer[source]

Bases: Protocol

Anything that can publish a directory of files.

is_live says whether deploy really publishes. It is part of the protocol rather than a detail of one backend, because the caller decides on it whether to record the upload.

name: str
is_live: bool
deploy(directory: pathlib.Path) → str[source]
class curated_feed.deploy.NullDeployer[source]

Explains that no host is configured, and uploads nothing.

The default, and deliberately so: a replay of a stub day is the commonest thing to run locally, and it must not be able to reach the internet.

name = 'none'
is_live = False
deploy(directory: pathlib.Path) → str[source]
curated_feed.deploy.CompletedCommand
curated_feed.deploy.Runner
curated_feed.deploy.run_command(command: collections.abc.Sequence[str], *, timeout: float) → CompletedCommand[source]

Run a command to completion, capturing what it printed.

curated_feed.deploy.build_command(directory: pathlib.Path, *, settings: curated_feed.config.DeploySettings, package: str | None = None) → list[str][source]

The wrangler invocation for one upload.

Every value wrangler needs is passed as a flag, so that no wrangler.jsonc has to be committed. That file would hold the Worker’s name, which is the hostname the feed is served from and therefore the secret.

Without a config file wrangler asks for a compatibility date, and asks interactively when it is missing — which in CI is a hang rather than a question. It has no effect on a Worker that is only static files.

>>> settings = DeploySettings(project="p", compatibility_date="2026-08-14")
>>> build_command(Path("/build"), settings=settings)[3:]
['deploy', '--assets', '/build', '--name', 'p', '--compatibility-date', '2026-08-14']
class curated_feed.deploy.WranglerDeployer(settings: curated_feed.config.DeploySettings, *, dry_run: bool = False, runner: Runner = run_command)[source]

Uploads a directory to a Cloudflare Worker with wrangler deploy.

Wrangler resolves CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID from the environment, so neither appears in configuration.

A Worker serving static assets, rather than Cloudflare Pages: the two put the same files on the same edge network, but a Pages deployment additionally mints a permanent <hash>.<project>.pages.dev address every time it runs. A daily job would therefore accumulate public addresses for a feed whose privacy is its URL. wrangler deploy updates one address in place.

name = 'wrangler'
settings
is_live = True
deploy(directory: pathlib.Path) → str[source]
curated_feed.deploy.make_deployer(config: curated_feed.config.Config, *, dry_run: bool = False) → Deployer[source]

The deployer named in configuration.

curated_feed.deploy.rendered_hash(path: pathlib.Path) → str[source]

The content hash of the build on disk, as the last render recorded it.

curated_feed.deploy.deployed_hash(path: pathlib.Path) → str | None[source]

The content hash of the last successful upload, or None if there is none.

curated_feed.deploy.build_parser() → argparse.ArgumentParser[source]
curated_feed.deploy.main(argv: collections.abc.Sequence[str] | None = None) → int[source]