Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cog-build-deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ Every input from `/cog-build/` is inherited unchanged. New on top:
| Name | Type | Default | Description |
|---|---|---|---|
| `deploy_to_cloudflare` | `bool` | `True` | Run `wrangler deploy --temporary` after the build. Set `False` to skip the deploy step entirely. |
| `wrangler_project_path` | `str` | `""` | Path inside the workspace to the directory containing a wrangler config. Default empty = auto-detect the shallowest `wrangler.toml` / `wrangler.json` / `wrangler.jsonc` under `$PASCLAW_HOME/workspace/`. Both `src/my-worker` (a dir) and `src/my-worker/wrangler.jsonc` (the file) work. Absolute paths are stripped so the operator can't escape the workspace. |
| `wrangler_project_path` | `str` | `""` | **Not required.** Path inside the workspace to the directory containing a wrangler config. Default empty = auto-detect the shallowest `wrangler.toml` / `wrangler.json` / `wrangler.jsonc` under `$PASCLAW_HOME/workspace/`. Both `src/my-worker` (a dir) and `src/my-worker/wrangler.jsonc` (the file) work. Absolute paths are stripped so the operator can't escape the workspace. |
| `install_deps_before_deploy` | `bool` | `True` | Run `npm install` (and `npm run build` when the project's `package.json` declares a `build` script) in the wrangler project dir before deploying. **Required for any project with dependencies or a bundler step** — `wrangler deploy` bundles the entry point but does *not* install `node_modules` or run build scripts, so without this a dep-using Worker fails to bundle (`Could not resolve <pkg>`) or deploys broken and crashes at runtime. Set `False` only for a hand-written, zero-dependency single-file Worker. |

The auto-detect path means **no agent-side instructions are needed** — if the build produced any wrangler config (TOML / JSON / JSONC — Cloudflare recommends `.jsonc` for new projects), it'll get found and deployed. If multiple exist (rare), set `wrangler_project_path` explicitly to disambiguate.

**Plan-only predictions never deploy.** When `mode="plan"` is used the build step is skipped, so the deploy block is also skipped even with `deploy_to_cloudflare=True` — a planning-only run can't accidentally publish stale code from a `workspace_in` that happened to contain a wrangler config. Slots `[2]` / `[3]` carry `(no deployment attempted -- plan-only mode)` in that case.

**Dependencies + build step.** `wrangler deploy` bundles the entry point with esbuild but doesn't install `node_modules` or run a project's build script — so a project that imports an npm package, or serves a built `dist/` (Vite, Workers Sites), would otherwise fail to bundle or deploy a broken Worker that crashes at runtime. With `install_deps_before_deploy=True` (the default) the cog runs `npm install` (and `npm run build` if a `build` script is declared) in the project dir first. This was validated against a real Hono + TypeScript Worker: raw deploy fails `Could not resolve "hono"`; install → build → deploy serves both `/` and `/api/hello` live. `node_modules` / `dist` are created in the live workspace *after* `pasclaw build` already wrote `workspace_out.zip`, so they don't bloat the returned archive.

## Outputs

A list of **four** file URLs (was two on `/cog-build/`):
Expand All @@ -49,6 +52,8 @@ Slots `[2]` and `[3]` always exist — empty fields would silently disappear on
| `(no deployment attempted)` | `deploy_to_cloudflare=False`. |
| `(no deployment attempted -- plan-only mode)` | `mode="plan"` -- the build step ran no agent loop, so the deploy step is skipped too even with the toggle on. |
| `(no wrangler.toml in workspace -- nothing to deploy)` | Agent didn't produce a wrangler config (TOML / JSON / JSONC); deploy was skipped automatically. |
| `(deploy failed: npm install exit N)\n<tail>` | `npm install` failed in the project dir (bad/unreachable dependency, etc.). Deploy not attempted. |
| `(deploy failed: npm run build exit N)\n<tail>` | The project's `build` script failed. Deploy not attempted. |
| `(deploy failed: wrangler exit N)\n<stderr tail>` | wrangler returned non-zero. Workspace + reply still come back, the prediction itself doesn't fail. |
| `(deploy failed: wrangler timed out after 5 min)` | Deploy hit the 5-minute timeout. |
| `(deploy failed: wrangler CLI not found -- rebuild the cog image)` | The image was built without wrangler. Check `cog.yaml`. |
Expand Down
89 changes: 87 additions & 2 deletions cog-build-deploy/predict.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,19 @@ def predict(
"specific one deployed.",
default="",
),
install_deps_before_deploy: bool = Input(
description="Before `wrangler deploy --temporary`, run "
"`npm install` (and `npm run build` when the project's "
"package.json declares a build script) in the wrangler "
"project dir. Required for any project with dependencies "
"or a bundler step (TypeScript + Vite, Hono, etc.) -- "
"wrangler bundles the entry point but does NOT install "
"node_modules or run build scripts, so without this a "
"dep-using Worker fails to bundle / deploys broken and "
"crashes at runtime. Set False only for a hand-written, "
"zero-dependency single-file Worker that needs no build.",
default=True,
),
) -> list[Path]:
"""
Run `pasclaw build` against the unzipped workspace, then ship the
Expand Down Expand Up @@ -791,7 +804,8 @@ def invoke_pasclaw(subcmd, extra_args, in_zip_arg, out_zip_arg,
claim_url = "(no deployment attempted)"
if deploy_to_cloudflare and do_build:
deployed_url, claim_url = self._deploy_to_cloudflare(
home_dir, wrangler_project_path, scratch
home_dir, wrangler_project_path, scratch,
install_deps=install_deps_before_deploy,
)
elif deploy_to_cloudflare and not do_build:
deployed_url = "(no deployment attempted -- plan-only mode)"
Expand Down Expand Up @@ -906,7 +920,8 @@ def _extract_urls(self, blob):
claim = u
return deployed, claim

def _deploy_to_cloudflare(self, home_dir, operator_override, scratch):
def _deploy_to_cloudflare(self, home_dir, operator_override, scratch,
install_deps=True):
"""Run `wrangler deploy --temporary` and parse the URLs out of stdout.

Runs wrangler with a PER-PREDICTION HOME so the temporary
Expand Down Expand Up @@ -967,6 +982,76 @@ def _deploy_to_cloudflare(self, home_dir, operator_override, scratch):
):
env.pop(k, None)

# ---- npm install + build before deploy ----------------------
# `wrangler deploy` auto-bundles the entry point with esbuild,
# but it does NOT install dependencies or run a project's build
# script. Anything beyond a zero-dependency single-file Worker
# therefore fails: a TS/Vite project that imports an npm package
# dies at bundle time with "Could not resolve <pkg>" (no
# node_modules), and a Workers-Sites project that serves a built
# dist/ dir deploys an empty site and the Worker throws at
# runtime ("Worker crashed" when you hit the URL). So when the
# project has a package.json we run `npm install` and, if a
# `build` script exists, `npm run build` first.
#
# This runs the agent-authored package.json's install/build
# scripts (arbitrary code), but the build step already ran
# arbitrary agent tool calls and the temp account + container
# are throwaway -- consistent with the existing trust model.
# node_modules / dist land in the live workspace AFTER pasclaw
# build already wrote workspace_out.zip, so they don't bloat
# the returned archive. The operator can set install_deps=False
# to skip (e.g. a hand-written single-file Worker needing no
# deps). Codex follow-up after a real Vite-project deploy
# surfaced the "Worker crashed" gap.
pkg_json = os.path.join(proj, "package.json")
if install_deps and os.path.isfile(pkg_json):
Comment on lines +1007 to +1008

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Install from the package root for nested Wrangler configs

When the Wrangler config lives below the JavaScript package root (for example workspace/worker/wrangler.toml with workspace/package.json holding the dependencies and build script), this guard skips the new install/build step entirely because there is no package.json in the config directory. I checked npm's prefix resolution from a child directory and it finds the parent package root, but this code never invokes npm in that case, so dependency-using nested Worker projects still go straight to wrangler deploy without installed deps and can reproduce the crash this change is meant to prevent.

Useful? React with 👍 / 👎.

print(f"deploy: package.json found in {proj}; running npm install")
try:
ires = subprocess.run(
["npm", "install", "--no-audit", "--no-fund"],
cwd=proj, capture_output=True, text=True,
timeout=600, env=env, # installs can be slow
)
except subprocess.TimeoutExpired:
return ("(deploy failed: npm install timed out after 10 min)",
"(deploy failed: npm install timed out after 10 min)")
except FileNotFoundError:
return ("(deploy failed: npm not found -- rebuild the cog image)",
"(deploy failed: npm not found -- rebuild the cog image)")
if ires.returncode != 0:
tail = ((ires.stdout or "") + (ires.stderr or "")).strip().splitlines()[-8:]
msg = "(deploy failed: npm install exit %d)\n%s" % (
ires.returncode, "\n".join(tail))
return (msg, msg)

# Run `npm run build` only when the package declares a build
# script -- otherwise npm errors "missing script: build".
has_build = False
try:
import json as _json
with open(pkg_json) as f:
has_build = bool(_json.load(f).get("scripts", {}).get("build"))
except Exception as e:
print(f"deploy: could not parse package.json for a build "
f"script ({e}); skipping npm run build")
if has_build:
print("deploy: running npm run build")
try:
bres = subprocess.run(
["npm", "run", "build"],
cwd=proj, capture_output=True, text=True,
timeout=600, env=env,
)
except subprocess.TimeoutExpired:
return ("(deploy failed: npm run build timed out after 10 min)",
"(deploy failed: npm run build timed out after 10 min)")
if bres.returncode != 0:
tail = ((bres.stdout or "") + (bres.stderr or "")).strip().splitlines()[-8:]
msg = "(deploy failed: npm run build exit %d)\n%s" % (
bres.returncode, "\n".join(tail))
return (msg, msg)

print(f"deploy: running `wrangler deploy --temporary` in {proj} "
f"(HOME={wrangler_home})")
try:
Expand Down