isomorphic-git
Use isomorphic-git ↗ to run Git operations on Artifacts repos directly from a Cloudflare Worker.
The Artifacts binding creates and manages repos, but it cannot read or write files inside them — for that, you need Git. Since Workers do not have a git binary or a local filesystem, isomorphic-git fills that gap. It provides Git operations like init, commit, and push as JavaScript function calls, using an in-memory filesystem in place of a real disk.
Use this when your Worker needs to programmatically build and push file trees to an Artifacts repo — for example, an AI agent that generates code and commits it, or an automation that clones a repo, modifies files, and pushes changes back.
Follow the Artifacts Workers setup guide to set up a Worker with an Artifacts binding.
Install isomorphic-git in your Worker project:
npm i isomorphic-git yarn add isomorphic-git pnpm add isomorphic-git bun add isomorphic-git This demo creates a new repo on each request, writes two files, commits them, and pushes main to the new remote.
Use this as a reference for the end-to-end flow. In a production Worker, look up or reuse an existing repo instead of creating a new one for every request.
import git from "isomorphic-git";import http from "isomorphic-git/http/web";import { MemoryFS } from "./memory-fs";
export default { async fetch(_request, env) { // Create a new Artifacts repo const repoName = `worker-demo-${crypto.randomUUID().slice(0, 8)}`; const created = await env.ARTIFACTS.create(repoName);
// Artifacts tokens include expiry metadata: art_v1_<secret>?expires=<unix_seconds> // Git Basic auth expects only the token secret as the password const tokenSecret = created.token.split("?expires=")[0];
// Set up an in-memory filesystem and initialize a Git repo const dir = "/workspace"; const fs = new MemoryFS(); await git.init({ fs, dir, defaultBranch: "main" });
// Write files await fs.promises.writeFile( `${dir}/README.md`, "# Artifacts repo created from a Worker\n", ); await fs.promises.writeFile( `${dir}/src/index.ts`, 'export const message = "hello from Artifacts";\n', );
// Stage and commit await git.add({ fs, dir, filepath: "README.md" }); await git.add({ fs, dir, filepath: "src/index.ts" });
const commit = await git.commit({ fs, dir, message: "Create starter files", author: { name: "Artifacts example", email: "artifacts@example.com", }, });
// Push to the Artifacts remote const push = await git.push({ fs, http, dir, url: created.remote, ref: "main", onAuth: () => ({ username: "x", password: tokenSecret, }), });
return Response.json({ repo: created.name, remote: created.remote, commit, refs: push.refs, }); },};import git from "isomorphic-git";import http from "isomorphic-git/http/web";import { MemoryFS } from "./memory-fs";
export interface Env { ARTIFACTS: Artifacts;}
export default { async fetch(_request: Request, env: Env) { // Create a new Artifacts repo const repoName = `worker-demo-${crypto.randomUUID().slice(0, 8)}`; const created = await env.ARTIFACTS.create(repoName);
// Artifacts tokens include expiry metadata: art_v1_<secret>?expires=<unix_seconds> // Git Basic auth expects only the token secret as the password const tokenSecret = created.token.split("?expires=")[0];
// Set up an in-memory filesystem and initialize a Git repo const dir = "/workspace"; const fs = new MemoryFS(); await git.init({ fs, dir, defaultBranch: "main" });
// Write files await fs.promises.writeFile( `${dir}/README.md`, "# Artifacts repo created from a Worker\n", ); await fs.promises.writeFile( `${dir}/src/index.ts`, 'export const message = "hello from Artifacts";\n', );
// Stage and commit await git.add({ fs, dir, filepath: "README.md" }); await git.add({ fs, dir, filepath: "src/index.ts" });
const commit = await git.commit({ fs, dir, message: "Create starter files", author: { name: "Artifacts example", email: "artifacts@example.com", }, });
// Push to the Artifacts remote const push = await git.push({ fs, http, dir, url: created.remote, ref: "main", onAuth: () => ({ username: "x", password: tokenSecret, }), });
return Response.json({ repo: created.name, remote: created.remote, commit, refs: push.refs, }); },} satisfies ExportedHandler<Env>;In-memory filesystem helper
Use this helper with isomorphic-git in Workers when you need a short-lived working tree in memory.
class MemoryStats { entry;
constructor(entry) { this.entry = entry; }
get size() { return this.entry.kind === "file" ? this.entry.data.byteLength : 0; }
get mtimeMs() { return this.entry.mtimeMs; }
get ctimeMs() { return this.entry.mtimeMs; }
get mode() { return this.entry.kind === "file" ? 0o100644 : 0o040000; }
isFile() { return this.entry.kind === "file"; }
isDirectory() { return this.entry.kind === "dir"; }
isSymbolicLink() { return false; }}
export class MemoryFS { encoder = new TextEncoder(); decoder = new TextDecoder(); entries = new Map([ ["/", { kind: "dir", children: new Set(), mtimeMs: Date.now() }], ]);
promises = { readFile: this.readFile.bind(this), writeFile: this.writeFile.bind(this), unlink: this.unlink.bind(this), readdir: this.readdir.bind(this), mkdir: this.mkdir.bind(this), rmdir: this.rmdir.bind(this), stat: this.stat.bind(this), lstat: this.lstat.bind(this), };
normalize(input) { const segments = [];
for (const part of input.split("/")) { if (!part || part === ".") { continue; }
if (part === "..") { segments.pop(); continue; }
segments.push(part); }
return `/${segments.join("/")}` || "/"; }
parent(path) { const normalized = this.normalize(path); if (normalized === "/") { return "/"; }
const parts = normalized.split("/").filter(Boolean); parts.pop(); return parts.length ? `/${parts.join("/")}` : "/"; }
basename(path) { return this.normalize(path).split("/").filter(Boolean).pop() ?? ""; }
getEntry(path) { return this.entries.get(this.normalize(path)); }
requireEntry(path) { const entry = this.getEntry(path); if (!entry) { throw new Error(`ENOENT: ${path}`); }
return entry; }
requireDir(path) { const entry = this.requireEntry(path); if (entry.kind !== "dir") { throw new Error(`ENOTDIR: ${path}`); }
return entry; }
async mkdir(path, options) { const target = this.normalize(path); if (target === "/") { return; }
const recursive = typeof options === "object" && options !== null && options.recursive; const parent = this.parent(target);
if (!this.entries.has(parent)) { if (!recursive) { throw new Error(`ENOENT: ${parent}`); }
await this.mkdir(parent, { recursive: true }); }
if (this.entries.has(target)) { return; }
this.entries.set(target, { kind: "dir", children: new Set(), mtimeMs: Date.now(), });
this.requireDir(parent).children.add(this.basename(target)); }
async writeFile(path, data) { const target = this.normalize(path); await this.mkdir(this.parent(target), { recursive: true });
const bytes = typeof data === "string" ? this.encoder.encode(data) : data instanceof Uint8Array ? data : new Uint8Array(data);
this.entries.set(target, { kind: "file", data: bytes, mtimeMs: Date.now(), });
this.requireDir(this.parent(target)).children.add(this.basename(target)); }
async readFile(path, options) { const entry = this.requireEntry(path); if (entry.kind !== "file") { throw new Error(`EISDIR: ${path}`); }
const encoding = typeof options === "string" ? options : options?.encoding; return encoding ? this.decoder.decode(entry.data) : entry.data; }
async readdir(path) { return [...this.requireDir(path).children].sort(); }
async unlink(path) { const target = this.normalize(path); const entry = this.requireEntry(target); if (entry.kind !== "file") { throw new Error(`EISDIR: ${path}`); }
this.entries.delete(target); this.requireDir(this.parent(target)).children.delete(this.basename(target)); }
async rmdir(path) { const target = this.normalize(path); const entry = this.requireDir(target); if (entry.children.size > 0) { throw new Error(`ENOTEMPTY: ${path}`); }
this.entries.delete(target); this.requireDir(this.parent(target)).children.delete(this.basename(target)); }
async stat(path) { return new MemoryStats(this.requireEntry(path)); }
async lstat(path) { return this.stat(path); }}type Entry = | { kind: "dir"; children: Set<string>; mtimeMs: number; } | { kind: "file"; data: Uint8Array; mtimeMs: number; };
class MemoryStats { entry: Entry;
constructor(entry: Entry) { this.entry = entry; }
get size() { return this.entry.kind === "file" ? this.entry.data.byteLength : 0; }
get mtimeMs() { return this.entry.mtimeMs; }
get ctimeMs() { return this.entry.mtimeMs; }
get mode() { return this.entry.kind === "file" ? 0o100644 : 0o040000; }
isFile() { return this.entry.kind === "file"; }
isDirectory() { return this.entry.kind === "dir"; }
isSymbolicLink() { return false; }}
export class MemoryFS { encoder = new TextEncoder(); decoder = new TextDecoder(); entries = new Map<string, Entry>([ ["/", { kind: "dir", children: new Set(), mtimeMs: Date.now() }], ]);
promises = { readFile: this.readFile.bind(this), writeFile: this.writeFile.bind(this), unlink: this.unlink.bind(this), readdir: this.readdir.bind(this), mkdir: this.mkdir.bind(this), rmdir: this.rmdir.bind(this), stat: this.stat.bind(this), lstat: this.lstat.bind(this), };
normalize(input: string) { const segments: string[] = [];
for (const part of input.split("/")) { if (!part || part === ".") { continue; }
if (part === "..") { segments.pop(); continue; }
segments.push(part); }
return `/${segments.join("/")}` || "/"; }
parent(path: string) { const normalized = this.normalize(path); if (normalized === "/") { return "/"; }
const parts = normalized.split("/").filter(Boolean); parts.pop(); return parts.length ? `/${parts.join("/")}` : "/"; }
basename(path: string) { return this.normalize(path).split("/").filter(Boolean).pop() ?? ""; }
getEntry(path: string) { return this.entries.get(this.normalize(path)); }
requireEntry(path: string) { const entry = this.getEntry(path); if (!entry) { throw new Error(`ENOENT: ${path}`); }
return entry; }
requireDir(path: string) { const entry = this.requireEntry(path); if (entry.kind !== "dir") { throw new Error(`ENOTDIR: ${path}`); }
return entry; }
async mkdir(path: string, options?: { recursive?: boolean } | number) { const target = this.normalize(path); if (target === "/") { return; }
const recursive = typeof options === "object" && options !== null && options.recursive; const parent = this.parent(target);
if (!this.entries.has(parent)) { if (!recursive) { throw new Error(`ENOENT: ${parent}`); }
await this.mkdir(parent, { recursive: true }); }
if (this.entries.has(target)) { return; }
this.entries.set(target, { kind: "dir", children: new Set(), mtimeMs: Date.now(), });
this.requireDir(parent).children.add(this.basename(target)); }
async writeFile(path: string, data: string | Uint8Array | ArrayBuffer) { const target = this.normalize(path); await this.mkdir(this.parent(target), { recursive: true });
const bytes = typeof data === "string" ? this.encoder.encode(data) : data instanceof Uint8Array ? data : new Uint8Array(data);
this.entries.set(target, { kind: "file", data: bytes, mtimeMs: Date.now(), });
this.requireDir(this.parent(target)).children.add(this.basename(target)); }
async readFile(path: string, options?: string | { encoding?: string }) { const entry = this.requireEntry(path); if (entry.kind !== "file") { throw new Error(`EISDIR: ${path}`); }
const encoding = typeof options === "string" ? options : options?.encoding; return encoding ? this.decoder.decode(entry.data) : entry.data; }
async readdir(path: string) { return [...this.requireDir(path).children].sort(); }
async unlink(path: string) { const target = this.normalize(path); const entry = this.requireEntry(target); if (entry.kind !== "file") { throw new Error(`EISDIR: ${path}`); }
this.entries.delete(target); this.requireDir(this.parent(target)).children.delete(this.basename(target)); }
async rmdir(path: string) { const target = this.normalize(path); const entry = this.requireDir(target); if (entry.children.size > 0) { throw new Error(`ENOTEMPTY: ${path}`); }
this.entries.delete(target); this.requireDir(this.parent(target)).children.delete(this.basename(target)); }
async stat(path: string) { return new MemoryStats(this.requireEntry(path)); }
async lstat(path: string) { return this.stat(path); }}