Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ jobs:
# Rebuild the default deployment before the normal runtime and package checks.
- run: bun run test:cloudflare
- run: bun run typecheck:cloudflare
- name: Install celld 0.4.1
- name: Install celld 0.5.0
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download v0.4.1 --repo denoland/celld --pattern celld-x86_64-unknown-linux-gnu.gz --dir "$RUNNER_TEMP"
gh release download v0.5.0 --repo denoland/celld --pattern celld-x86_64-unknown-linux-gnu.gz --dir "$RUNNER_TEMP"
gzip -d "$RUNNER_TEMP/celld-x86_64-unknown-linux-gnu.gz"
chmod +x "$RUNNER_TEMP/celld-x86_64-unknown-linux-gnu"
echo "CELLD_BIN=$RUNNER_TEMP/celld-x86_64-unknown-linux-gnu" >> "$GITHUB_ENV"
Expand Down
3 changes: 2 additions & 1 deletion cloudflare/backend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ let directory = "", binary = "", url = "", logs = "";
let child: ReturnType<typeof Bun.spawn> | undefined;
async function startNative() {
child = Bun.spawn([binary, "dev", directory, "--host", "127.0.0.1", "--port", new URL(url).port, "--no-watch"], {
cwd: directory, env: { ...process.env, CELLD_WORKER_LOADER: "LOADER", CELLD_ESBUILD: join(import.meta.dir, "../node_modules/.bin/esbuild") }, stdout: "pipe", stderr: "pipe",
cwd: directory, env: { ...process.env, CELLD_ESBUILD: join(import.meta.dir, "../node_modules/.bin/esbuild") }, stdout: "pipe", stderr: "pipe",
});
for (const stream of [child.stdout, child.stderr]) if (typeof stream !== "number") void (async () => {
for await (const chunk of stream) logs += new TextDecoder().decode(chunk);
Expand Down Expand Up @@ -43,6 +43,7 @@ beforeAll(async () => {
await Bun.write(join(directory, "worker.js"), await build.outputs[0]!.text());
await Bun.write(join(directory, "wrangler.jsonc"), JSON.stringify({ name: "backend-test", main: "worker.js", compatibility_date: "2026-09-06", compatibility_flags: ["nodejs_compat"],
durable_objects: { bindings: [{ name: "BACKENDS", class_name: "ArtifactBackend" }] }, migrations: [{ tag: "v1", new_sqlite_classes: ["ArtifactBackend"] }],
worker_loaders: [{ binding: "LOADER" }],
}));
await startNative();
return;
Expand Down
2 changes: 1 addition & 1 deletion cloudflare/celld.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async function startCelld(compiler = esbuild) {
processHandle = Bun.spawn({
cmd: [binary, "dev", project, "--host", "127.0.0.1", "--port", String(port), "--no-watch"],
cwd: project,
env: { ...process.env, CELLD_ESBUILD: compiler, CELLD_WORKER_LOADER: "LOADER" },
env: { ...process.env, CELLD_ESBUILD: compiler },
stdout: "pipe",
stderr: "pipe",
});
Expand Down
8 changes: 5 additions & 3 deletions cloudflare/scheduling-celld.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import {mkdtemp,rm} from "node:fs/promises";
import {join} from "node:path";
import {tmpdir} from "node:os";
import {ensureCelldRuntime} from "../src/local/celld-runtime";
import {prepareCelldConfig} from "../scripts/prepare-celld";
const enabled=process.env.CELLD_SCHEDULING_INTEGRATION==="1";
let directory="",url="",binary="",logs="";
let child:ReturnType<typeof Bun.spawn>|undefined;
async function start(){
child=Bun.spawn([binary,"dev",directory,"--host","127.0.0.1","--port",new URL(url).port,"--no-watch"],{cwd:directory,env:{...process.env,CELLD_WORKER_LOADER:"LOADER",CELLD_ESBUILD:join(import.meta.dir,"../node_modules/.bin/esbuild")},stdout:"pipe",stderr:"pipe"});
child=Bun.spawn([binary,"dev",directory,"--host","127.0.0.1","--port",new URL(url).port,"--no-watch","--logs"],{cwd:directory,env:{...process.env,CELLD_ESBUILD:join(import.meta.dir,"../node_modules/.bin/esbuild")},stdout:"pipe",stderr:"pipe"});
for(const stream of [child.stdout,child.stderr]) if(typeof stream!=="number")void(async()=>{for await(const chunk of stream)logs+=new TextDecoder().decode(chunk);})();
const deadline=Date.now()+45000;
while(Date.now()<deadline){try{if((await fetch(url+"/logs")).ok)return;}catch{}if(child.exitCode!==null)break;await Bun.sleep(100);}
Expand All @@ -22,7 +23,8 @@ beforeAll(async()=>{
const bundle=await Bun.build({entrypoints:[join(import.meta.dir,"scripts-test-worker.ts")],target:"browser",format:"esm",external:["cloudflare:workers","node:*","fs","fs/promises"]});
if(!bundle.success)throw new Error(bundle.logs.join("\n"));
await Bun.write(join(directory,"worker.js"),await bundle.outputs[0]!.text());
await Bun.write(join(directory,"wrangler.jsonc"),JSON.stringify({name:"schedule-test",main:"worker.js",compatibility_date:"2026-09-06",compatibility_flags:["nodejs_compat"],durable_objects:{bindings:[{name:"SCRIPTS",class_name:"ScriptLibrary"},{name:"SCRIPT_BACKENDS",class_name:"ScriptBackend"},{name:"LINKS",class_name:"ArtifactLinks"}]},migrations:[{tag:"v1",new_sqlite_classes:["ScriptLibrary","ScriptBackend","ArtifactLinks"]}]}));
await Bun.write(join(directory,"wrangler.jsonc"),JSON.stringify({name:"schedule-test",main:"worker.js",compatibility_date:"2026-09-06",compatibility_flags:["nodejs_compat"],worker_loaders:[{binding:"LOADER"}],durable_objects:{bindings:[{name:"SCRIPTS",class_name:"ScriptLibrary"},{name:"SCRIPT_BACKENDS",class_name:"ScriptBackend"},{name:"LINKS",class_name:"ArtifactLinks"}]},migrations:[{tag:"v1",new_sqlite_classes:["ScriptLibrary","ScriptBackend","ArtifactLinks"]}]}));
await prepareCelldConfig(join(directory,"wrangler.jsonc"),join(directory,"wrangler.jsonc"),{main:"worker.js"});
await start();
},180000);
afterAll(async()=>{await stop();if(directory)await rm(directory,{recursive:true,force:true});});
Expand All @@ -36,7 +38,7 @@ async function call(path:string,input:unknown={}){const response=await fetch(`${
await call("backend/schedule",{action:"set",identity:{...identity,libraryKey:"scheduled",origin:url},cron:"* * * * *",timezone:"UTC",request:{path:"/tick",method:"POST",headers:[["x-artifact-internal-run","caller-value"]]}});
await call("backend/schedule",{action:"pause"});
await call("backend/schedule",{action:"run_now"});
expect((await call("backend/runs"))[0]).toMatchObject({status:"succeeded",trigger:"manual"});
expect((await call("backend/runs"))[0],JSON.stringify(await call("backend/logs"))+logs).toMatchObject({status:"succeeded",trigger:"manual"});
await stop();await start();
expect(await call("backend/schedule")).toMatchObject({paused:true,cron:"* * * * *",timezone:"UTC"});
expect(await call("backend/runs")).toHaveLength(1);
Expand Down
10 changes: 6 additions & 4 deletions cloudflare/script-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import { nativeRequest } from "./backend";
import type { ArtifactHttpRequest } from "../src/httpTypes";
import { sha256 } from "./library";
import { scriptResponse } from "./script-service-http";
import { scriptRuntimeConfig } from "./script-runtime";

export type ScriptLog = { timestamp: string; level: string; message: string; run_id?: string };
export type ScriptRequest = {code: string; hash: string; secrets: Record<string,string>; request: Request; trigger?: "http" | "manual" | "schedule"; run_id?: string};
export type ScheduleIdentity = {libraryKey: string; workspace: string; name: string; origin: string};
export type ScriptSchedule = ScheduleTiming & { paused: boolean; next_run_at: number | null; request: ArtifactHttpRequest};
export type ScriptRun = {id: string; revision: string; trigger: string; started_at: string; finished_at: string | null; duration_ms: number | null; status: string; http_status: number | null};
type Env = { LOADER: WorkerLoader; SCRIPTS: DurableObjectNamespace<ScriptLibrary>; LINKS: DurableObjectNamespace<ArtifactLinks> };
const runtimeConfig = { compatibilityDate: "2026-09-06", compatibilityFlags: ["nodejs_compat"], limits: { cpuMs: 30000, subRequests: 50 } };
type Env = { LOADER: WorkerLoader; SCRIPTS: DurableObjectNamespace<ScriptLibrary>; LINKS: DurableObjectNamespace<ArtifactLinks>; ARTIFACTS_RUNTIME?: string };
declare abstract class ScriptFacet extends DurableObject { takeLogs(): ScriptLog[]; }
const RUN_HEADER = "x-artifact-internal-run";
export const SCRIPT_HEADER = "x-artifact-internal-script";
Expand Down Expand Up @@ -64,10 +64,12 @@ export default { fetch() { return new Response(typeof handler?.fetch === "functi
`;

export class ScriptBackend extends DurableObject<Env> {
private readonly runtimeConfig: ReturnType<typeof scriptRuntimeConfig>;
private activeKey?: string;
private facet?: Fetcher<ScriptFacet>;
constructor(state: DurableObjectState, env: Env) {
super(state,env);
this.runtimeConfig = scriptRuntimeConfig(env.ARTIFACTS_RUNTIME);
state.storage.sql.exec("create table if not exists execution_logs(id integer primary key autoincrement,timestamp text not null,level text not null,message text not null)");
if (!state.storage.sql.exec<{name: string}>("pragma table_info(execution_logs)").toArray().some(column => column.name === "run_id")) state.storage.sql.exec("alter table execution_logs add column run_id text");
state.storage.sql.exec("create table if not exists execution_runs(id text primary key,revision text not null,trigger text not null,started_at text not null,finished_at text,duration_ms integer,status text not null,http_status integer)");
Expand All @@ -76,12 +78,12 @@ export class ScriptBackend extends DurableObject<Env> {
state.storage.sql.exec("update execution_runs set status='interrupted' where status='running'");
}
private cacheKey(input: Omit<ScriptRequest,"request">) {
return sha256(JSON.stringify([this.ctx.id.toString(), input.code, input.secrets, runtimeConfig, adapter, logging]));
return sha256(JSON.stringify([this.ctx.id.toString(), input.code, input.secrets, this.runtimeConfig, adapter, logging]));
}
private worker(input: Omit<ScriptRequest,"request">) {
const hash=this.cacheKey(input);
return this.env.LOADER.get(hash,async()=>({
...runtimeConfig,
...this.runtimeConfig,
mainModule:"adapter.js",modules:{"adapter.js":adapter,"logging.js":logging.replace("__ARTIFACTS_SCRIPT_SECRETS__", () => JSON.stringify(input.secrets)),"user.js":input.code},
env:{secrets:input.secrets},
}));
Expand Down
10 changes: 10 additions & 0 deletions cloudflare/script-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { expect, test } from "bun:test";
import { scriptRuntimeConfig } from "./script-runtime";

test("only an explicit celld deployment omits unsupported script limits", () => {
for (const runtime of [undefined, "cloudflare", "", "unknown"]) {
expect(scriptRuntimeConfig(runtime).limits).toEqual({ cpuMs: 30000, subRequests: 50 });
}
expect(scriptRuntimeConfig("celld")).not.toHaveProperty("limits");
expect(scriptRuntimeConfig("celld").compatibilityFlags).toContain("nodejs_compat");
});
9 changes: 9 additions & 0 deletions cloudflare/script-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function scriptRuntimeConfig(runtime?: string) {
return {
compatibilityDate: "2026-09-06",
compatibilityFlags: ["nodejs_compat"],
// celld 0.5.0 rejects limits and cannot enforce these per-script budgets.
// Keep the Cloudflare limits unless the deployment explicitly selects celld.
...(runtime === "celld" ? {} : { limits: { cpuMs: 30000, subRequests: 50 } }),
};
}
15 changes: 8 additions & 7 deletions docs/celld-deployment.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Deploying Artifacts with celld

The bundled runtime is pinned to celld 0.4.1. This guidance follows the
[upstream deployment documentation](https://github.com/denoland/celld/blob/v0.4.1/docs/README.md)
and [security model](https://github.com/denoland/celld/blob/v0.4.1/docs/security.md).
The bundled runtime is pinned to celld 0.5.0. This guidance follows the
[upstream deployment documentation](https://github.com/denoland/celld/blob/v0.5.0/docs/README.md)
and [security model](https://github.com/denoland/celld/blob/v0.5.0/docs/security.md).

## Local persistent host

Expand All @@ -25,7 +25,7 @@ For production or multiple machines, use celld's bucket-backed node mode:
1. Choose a supported object store with conditional writes and consistent reads.
Upstream qualifies Amazon S3, Cloudflare R2, Google Cloud Storage, Tigris, and
Azure Blob Storage. Not every S3-compatible provider meets the requirements;
consult the [storage guarantees](https://github.com/denoland/celld/blob/v0.4.1/docs/guarantees.md).
consult the [storage guarantees](https://github.com/denoland/celld/blob/v0.5.0/docs/guarantees.md).
2. Prepare the Artifacts Worker with `bun run build:package`, configure application
authentication and runtime variables, and deploy the prepared Wrangler project
through `celld deploy`. Preserve JavaScript and WASM modules together. Do not
Expand All @@ -42,9 +42,10 @@ For production or multiple machines, use celld's bucket-backed node mode:
the supervisor's stop grace must exceed celld's configured shutdown bound
(40 seconds by default), with enough time for the expected drain and handoff.
6. Update app code through `celld deploy`. Running nodes adopt deployments in
place; a failed build leaves the previous deployment serving. Roll runtime
upgrades with readiness checks and spare capacity rather than restarting every
node together.
place; a failed build leaves the previous deployment serving. For runtime
upgrades, follow the release-specific shutdown requirements. Upgrading from
0.4.1 to 0.5.0 requires stopping the whole fleet before starting upgraded nodes;
see the [0.5.0 release notes](https://github.com/denoland/celld/releases/tag/v0.5.0).

Two or more nodes reduce write latency through peer durability; a single node
waits for bucket persistence. Monitor memory headroom, cold activation queues,
Expand Down
10 changes: 8 additions & 2 deletions docs/cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,8 +377,14 @@ backend. Their browser views can use ordinary local artifact interactions, but

[celld](https://github.com/denoland/celld) is a Cloudflare-compatible runtime.
Wrangler/workerd remains the default Cloudflare development path. Artifact also
qualifies the released celld v0.4.1 against the built application, including
targets the released celld v0.5.0 with integration tests for the built application, including
generated `ArtifactServer` execution on raw SQL and KV facets.
The generated config preserves the `worker_loaders` binding used by celld 0.5.0;
the removed `CELLD_WORKER_LOADER` environment variable must no longer be set.
The generated config sets `ARTIFACTS_RUNTIME=celld`, which omits the unsupported
script CPU and subrequest limits. celld does not enforce these per-script budgets.
Cloudflare retains the 30,000 ms CPU and 50 subrequest limits by default; do not
set this variable to `celld` on Cloudflare. See the [upstream compatibility notes](https://github.com/denoland/celld/blob/v0.5.0/docs/cloudflare-compat.md#dynamic-workers).

```sh
bun run dev:celld
Expand Down Expand Up @@ -414,7 +420,7 @@ carries that D1 binding. celld v0.4.1 can run the checked-in Better Auth schema
local D1 and preserve it across a restart. Its `celld d1 migrations apply` command
targets deployed bucket storage rather than the local development database, so
the Artifacts celld integration uses a temporary bootstrap Worker for local schema
setup. This fixture qualifies the complete OAuth/session flow on celld v0.4.1;
setup. This fixture exercises the complete OAuth/session flow on celld v0.5.0;
ordinary `dev:celld` does not apply auth migrations automatically. Use
Wrangler/workerd's local migration command for routine Better Auth development.

Expand Down
2 changes: 1 addition & 1 deletion docs/daemon.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The server keeps its project and native state under the Artifacts data directory
`--state-dir PATH` overrides that project directory for a foreground run. Press
Ctrl-C to request a graceful stop. SIGTERM uses the same shutdown path.

On first use, Artifacts downloads celld 0.4.1 for a supported platform, verifies
On first use, Artifacts downloads celld 0.5.0 for a supported platform, verifies
the pinned archive and executable SHA-256 values, and installs it in the Artifact
data directory with user-only permissions. This release supports Apple Silicon
macOS and glibc Linux on arm64 or x64. The ordinary Bun commands do not require this native runtime; `artifacts server`
Expand Down
2 changes: 1 addition & 1 deletion docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ the runtime suite, and daemon lifecycle on supported service managers. The local
packaged server uses loopback single-user mode; deploying Better Auth remains the
separate configuration described in [Cloudflare setup](cloudflare.md).

celld v0.4.1 publishes Apple Silicon macOS and glibc Linux arm64/x64 binaries.
celld v0.5.0 publishes Apple Silicon macOS and glibc Linux arm64/x64 binaries.
Intel macOS and other targets cannot use this managed native server version.
The ordinary Bun CLI, stdio MCP, and local file gallery do not download celld.

Expand Down
2 changes: 1 addition & 1 deletion docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default {
} satisfies ExportedHandler<ScriptEnv>;
```

Do not assume an in-memory global persists between requests or updates. Each execution is limited to 30 seconds of CPU and 50 subrequests, in addition to the Workers runtime limits. Logs retain the latest 100 entries with messages capped at 2 KiB. Secret strings are redacted from captured console output; handlers still control their own response bodies and outbound requests.
Do not assume an in-memory global persists between requests or updates. On Cloudflare, each execution is limited to 30 seconds of CPU and 50 subrequests, in addition to the Workers runtime limits. The celld configuration omits these per-script budgets because celld 0.5.0 rejects them and does not enforce them. Logs retain the latest 100 entries with messages capped at 2 KiB. Secret strings are redacted from captured console output; handlers still control their own response bodies and outbound requests.

## Access

Expand Down
2 changes: 1 addition & 1 deletion e2e/celld-auth-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function spawnCelld(binary: string, esbuild: string, project: string, port: numb
const process = Bun.spawn({
cmd: [binary, "dev", project, "--host", "127.0.0.1", "--port", String(port), "--no-watch", "--logs"],
cwd: project,
env: { ...globalThis.process.env, CELLD_ESBUILD: esbuild, CELLD_WORKER_LOADER: "LOADER" },
env: { ...globalThis.process.env, CELLD_ESBUILD: esbuild },
stdout: "pipe",
stderr: "pipe",
});
Expand Down
2 changes: 1 addition & 1 deletion examples/runner-status/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export async function main() {
await writeFile(runtimeConfig, JSON.stringify(runtime), { mode: 0o600 });
if (stopping) throw new Error("Startup interrupted");
child = Bun.spawn([binary, "dev", runtimeConfig, "--host", "127.0.0.1", "--port", String(port), "--no-watch"], {
cwd: directory, env: { ...environment, CELLD_ESBUILD: join(root, "node_modules/.bin/esbuild"), CELLD_WORKER_LOADER: "LOADER" },
cwd: directory, env: { ...environment, CELLD_ESBUILD: join(root, "node_modules/.bin/esbuild") },
stdout: "inherit", stderr: "inherit",
});
const origin = `http://127.0.0.1:${port}`;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
"build:cloudflare-compiler": "wrangler deploy --config cloudflare/wrangler.compiler.jsonc --dry-run --outdir ../dist/worker",
"typecheck:cloudflare": "tsc -p cloudflare/tsconfig.json --noEmit && tsc -p examples/runner-status/tsconfig.worker.json --noEmit",
"test:cloudflare": "bun run build:cloudflare-compiler && bun run build:cloudflare && bun test cloudflare --timeout 60000",
"test:celld": "bun run build:cloudflare && bun run prepare:celld && CELLD_INTEGRATION=1 CELLD_AUTH_INTEGRATION=1 CELLD_BACKEND_INTEGRATION=1 bun test cloudflare/backend.test.ts cloudflare/celld.test.ts cloudflare/auth-flow.test.ts --timeout 180000",
"test:celld": "bun run build:cloudflare && bun run prepare:celld && CELLD_INTEGRATION=1 CELLD_AUTH_INTEGRATION=1 CELLD_BACKEND_INTEGRATION=1 CELLD_SCHEDULING_INTEGRATION=1 bun test cloudflare/backend.test.ts cloudflare/celld.test.ts cloudflare/auth-flow.test.ts cloudflare/scheduling-celld.test.ts --timeout 180000",
"deploy:cloudflare": "wrangler deploy",
"seed:cloudflare": "bun run scripts/seed-cloudflare.ts",
"example:runner-status": "bun run examples/runner-status/local.ts",
Expand Down
2 changes: 1 addition & 1 deletion scripts/dev-celld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ if (!existsSync(esbuild)) throw new Error(`esbuild was not found at ${esbuild};
const child = Bun.spawn({
cmd: [binary, "dev", resolve(root, "wrangler.celld.jsonc"), "--host", "127.0.0.1", "--port", process.env.CELLD_PORT ?? "4786"],
cwd: root,
env: { ...process.env, CELLD_ESBUILD: esbuild, CELLD_WORKER_LOADER: "LOADER" },
env: { ...process.env, CELLD_ESBUILD: esbuild },
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
Expand Down
Loading
Loading