Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
317f09f
[miniflare] Add opt-in local Artifacts simulator
leostera Oct 9, 2026
12a486d
[miniflare] Recover Artifacts sidecars and retry Git deletions
leostera Oct 9, 2026
2bd32c8
[miniflare] Stage simulator Git operations and clean interrupted imports
leostera Oct 9, 2026
f3f326d
[miniflare] Guard simulator publication across host processes
leostera Oct 9, 2026
940b585
[miniflare] Retain potentially active repository creation locks
leostera Oct 9, 2026
dca825a
[miniflare] Classify invalid Artifacts import depth as input error
leostera Oct 9, 2026
ef2f445
[miniflare] Stream Git push responses and classify binary files
leostera Oct 9, 2026
559fbc9
[miniflare] Retain review guidance and startup assertions
leostera Oct 9, 2026
877cc03
fix(miniflare): drain Git push reply before syncing refs
leostera Oct 9, 2026
b3c7550
fix(miniflare): synchronize canceled Git pushes
leostera Oct 9, 2026
a69992d
fix(miniflare): address Artifacts lifecycle and review feedback
leostera Oct 12, 2026
c2b198e
fix(miniflare): parse Artifacts admin requests with Zod
leostera Oct 12, 2026
ee4756f
docs(miniflare): describe local Git helper contracts
leostera Oct 12, 2026
193b6d6
[miniflare] Enable local Artifacts across development tools
leostera Oct 9, 2026
0456450
[miniflare] Keep final legacy Artifacts default guidance
leostera Oct 9, 2026
7b41dc9
[miniflare] Publish local Artifacts lifecycle events through Queue RPC
leostera Oct 13, 2026
e2a722f
[miniflare] Keep Artifacts event ingress stateless and name event types
leostera Oct 13, 2026
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
5 changes: 5 additions & 0 deletions .changeset/artifacts-opt-in-simulator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"miniflare": minor
---

Add an opt-in local Artifacts binding simulator to Miniflare. Set `dev.remote: false` on an Artifacts binding to use persistent local repositories and authenticated Git smart HTTP backed by host Git 2.32 or newer. Omitted `dev.remote` retains the existing remote binding behavior.
9 changes: 9 additions & 0 deletions .changeset/local-artifacts-binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"miniflare": minor
"wrangler": minor
"@cloudflare/workers-utils": minor
---

Enable local Artifacts by default across Wrangler, Vite, and Workers Vitest

Artifacts bindings now use the Miniflare simulator and persistent local repositories by default in development. Set `remote: true` on the binding to opt into the deployed resource. Local development does not require Cloudflare credentials, but does require Git 2.32 or newer on the host.
7 changes: 7 additions & 0 deletions .changeset/local-artifacts-event-ingress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"miniflare": minor
---

Add a private, experimental Artifacts event producer in the local Queue plugin

Local repository and token lifecycle operations now send typed version-one Artifacts events over a private JSRPC binding. The receiver does not retain or deliver events until subscription support is added; Git transport and namespace events are not yet produced.
8 changes: 7 additions & 1 deletion packages/miniflare/scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,13 @@ async function buildPackage() {
logLevel: watch ? "info" : "warning",
outdir: outPath,
outbase: pkgRoot,
entryPoints: [indexPath, ...fixtureBuilds],
entryPoints: [
indexPath,
// A separate thread keeps native Git available during synchronous Node
// binding-proxy calls, which block Miniflare's main event loop.
path.join(pkgRoot, "src/plugins/artifacts/git-sidecar.ts"),
...fixtureBuilds,
],
};

if (watch) {
Expand Down
6 changes: 5 additions & 1 deletion packages/miniflare/src/config/v4-convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,11 @@ function addProductBindings(
env[name] = {
type: "artifacts",
namespace: binding.namespace,
dev: { remote: isRemote(binding.remoteProxyConnectionString) },
// Preserve omitted legacy settings so the Artifacts plugin applies
// the same local default as a V5 binding.
...(binding.remoteProxyConnectionString === undefined
? {}
: { dev: { remote: isRemote(binding.remoteProxyConnectionString) } }),
};
}
for (const name of Object.keys(worker.workerLoaders ?? {})) {
Expand Down
23 changes: 21 additions & 2 deletions packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import {
WORKER_BINDING_SERVICE_LOOPBACK,
WORKFLOWS_PLUGIN_NAME,
} from "./plugins";
import { ArtifactsController } from "./plugins/artifacts/controller";
import { RPC_PROXY_SERVICE_NAME } from "./plugins/assets/constants";
import { BROWSER_VERSION } from "./plugins/browser-rendering/browser-version";
import { closeBrowserProcess } from "./plugins/browser-rendering/process";
Expand Down Expand Up @@ -888,6 +889,7 @@ export class Miniflare {
#maybeInspectorProxyController?: InspectorProxyController;
#previousRuntimeInspectorPort?: number;

#artifactsController = new ArtifactsController();
#containerPrivilegesCache = new ContainerPrivilegesCache();
#hyperdriveProxyController: HyperdriveProxyController =
new HyperdriveProxyController();
Expand Down Expand Up @@ -2326,6 +2328,7 @@ export class Miniflare {
queueConsumers,
containerPrivilegesCache: this.#containerPrivilegesCache,
hyperdriveProxyController: this.#hyperdriveProxyController,
artifactsController: this.#artifactsController,
};
for (const [key, plugin] of this.#mergedPluginEntries) {
const pluginServicesExtensions = await plugin.getServices({
Expand All @@ -2341,6 +2344,11 @@ export class Miniflare {
} else {
pluginServices = pluginServicesExtensions.services;
extensions.push(...pluginServicesExtensions.extensions);
for (const socket of pluginServicesExtensions.sockets ?? []) {
if (!sockets.some((existing) => existing.name === socket.name)) {
sockets.push(socket);
}
}
}

for (const service of pluginServices) {
Expand Down Expand Up @@ -2672,11 +2680,13 @@ export class Miniflare {

async #assembleAndUpdateConfig(reusePorts = false) {
this.#hyperdriveProxyController.beginUpdate();
this.#artifactsController.beginUpdate();
try {
await this.#assembleAndUpdateConfigInternal(reusePorts);
} finally {
// If assembly or runtime startup failed, keep the old listeners and
// close only the new candidates. A successful update already committed.
// A failed update retains listeners used by the previous runtime and
// closes only candidates started during this attempt.
await this.#artifactsController.abortUpdate();
this.#hyperdriveProxyController.abortUpdate();
}
}
Expand Down Expand Up @@ -2800,6 +2810,7 @@ export class Miniflare {
}
}
this.#hyperdriveProxyController.commitUpdate(activeExternalAddresses);
await this.#artifactsController.commitUpdate();
// Note: `updateConfig()` doesn't resolve until ports for all required
// sockets have been recorded. At this point, `maybeSocketPorts` contains
// all of `requiredSockets` as keys.
Expand Down Expand Up @@ -3915,6 +3926,14 @@ export class Miniflare {
}

const runtimeCleanupOutcome = await runtimeDisposeOutcome;
try {
await this.#artifactsController.dispose();
} catch (error) {
if (!independentCleanupFailed) {
independentCleanupFailed = true;
independentCleanupError = error;
}
}
this.#devRegistry.unregisterWorkers();
try {
await Promise.all(
Expand Down
77 changes: 77 additions & 0 deletions packages/miniflare/src/plugins/artifacts/LOCAL_DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Develop with local Artifacts

## Prerequisite: Git

This first version of the local Artifacts emulator requires Git 2.32 or newer installed globally on your development machine, with `git` available on the PATH of the process starting Wrangler, Vite, or Workers Vitest. Run `git --version` in that environment before starting the dev server or tests. If Git is missing, cannot run, or is older than 2.32, local Artifacts fails at startup with installation or upgrade instructions instead of waiting for a repository operation. Restart your dev tool after installing Git so it picks up the updated PATH.

Git 2.32 provides `GIT_CONFIG_GLOBAL`, which the emulator uses to isolate host Git settings. Use an up-to-date, security-patched release.

A binding with `"remote": true` does not start the local emulator and does not need local Git. Production Artifacts also does not require Git on your machine. We plan to open-source a JavaScript Git engine in the future; it is not part of this initial local release, which uses native Git.

## Configure a binding

For a Wrangler project, add an Artifacts binding to `wrangler.jsonc`:

```jsonc
{
"name": "example-worker",
"main": "src/index.ts",
"compatibility_date": "2026-09-03",
"artifacts": [{ "binding": "REPOS", "namespace": "examples" }],
}
```

To try this unreleased feature from the Workers SDK checkout, install its dependencies and build Miniflare and Wrangler, then start the checkout's CLI from the app directory:

```sh
# From the workers-sdk root:
pnpm install --frozen-lockfile
pnpm --filter miniflare build
pnpm --filter wrangler build

# From your app directory, using the absolute path to this checkout:
node /path/to/workers-sdk/packages/wrangler/bin/wrangler.js dev --port=0 --inspector-port=0
```

Open the URL on Wrangler's `Ready on` line. With `@cloudflare/vite-plugin`, use the same Wrangler config with `cloudflare()` in your Vite config and start the Vite dev server. In Workers Vitest, point `cloudflareTest({ wrangler: { configPath: "./wrangler.jsonc" } })` at this config. These paths share the Miniflare implementation. To force local behavior explicitly, add `"remote": false` to the binding. The absence of `remote` is also local; this differs from older development behavior that implicitly used a remote Artifacts resource. Do not rely on production repositories being available locally.

A Worker can create and inspect a local repository:

```ts
const created = await env.REPOS.create("demo");
const repo = await env.REPOS.get("demo");
const listing = await env.REPOS.list();
const info = await repo.info();
// created.remote is a loopback HTTP Git URL; created.token is a secret.
```

To use Git smart HTTP, use the returned `remote` URL and a repository token. If `git clone` prompts for a username and password, enter any username (for example, `git`) and use `created.token` as the password. This is an Artifacts repository token, **not** a GitHub or Cloudflare account password. A configured Git credential helper may remember a password entered at the prompt. To avoid that, supply a bearer token for one Git command without storing it in source or in a Git remote URL:

```sh
read -s REPO_TOKEN # Paste the token returned by create(); input is hidden.
printf '\n'
# Replace the URL with the `remote` value returned by create() or info().
GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraHeader \
GIT_CONFIG_VALUE_0="Authorization: Bearer $REPO_TOKEN" \
git clone 'http://127.0.0.1:<port>/git/examples/demo.git'
unset REPO_TOKEN
```

After pushing `hello.txt` to `main`, read it in the Worker with `await repo.readFile({ ref: "main", path: "hello.txt" })` (a `Blob`, or `null` when absent). The initial create token permits writes. Its plaintext is returned only once; if you lose it, use `repo.createToken("read")` for cloning or `repo.createToken("write")` for pushing and save the returned `plaintext` securely. Revoke tokens that are no longer needed. Avoid printing tokens or committing them to config. A plain request without a token cannot clone a private local repository. Git's URL uses an ephemeral loopback port; obtain the current URL after restarting the dev server instead of hardcoding it.

## Storage and network boundaries

Local repositories and metadata persist under the dev tool's Miniflare resource persistence path (for example, Wrangler's `.wrangler/state/` when persistence is enabled). Use the same project root and persistence settings to keep local data across reloads and restarts. Disabling persistence (for example, Vite's `persistState: false`) makes the data temporary. Local and deployed Artifacts are separate: local create, push, and delete operations do not update remote resources. To opt in to a remote binding, set `"remote": true` on that Artifacts binding and configure the credentials required by your development tool. Remote mode uses a Cloudflare proxy, not the local Git store; do not assume local data migrates to it.

To reset local state, stop the dev server first, back up any repositories you need, then delete the `artifacts/` directory inside that dev tool's resource persistence path. Older draft persisted Artifacts layouts are not automatically migrated and may require this backup-and-reset procedure. Do not delete unrelated binding data in the same persistence directory.

A local binding does not contact Cloudflare for repository operations. **Explicit imports from HTTPS Git sources are different:** they make outbound network requests from your development machine to the supplied host, including private-network hosts. Import does not follow HTTP redirects; supply the final HTTPS Git URL so credentials cannot be redirected to another destination. Imports can require authentication and may fail due to TLS certificates, connectivity, or upstream Git permissions. For a private Git CA, set `GIT_SSL_CAINFO` to the trusted CA file in the dev tool's host environment before starting the dev server. Do not disable certificate verification. After a successful import, URL credentials are omitted from the local repository metadata and Git config. In-flight process-crash recovery is not yet validated; treat partial local storage after an interrupted import as sensitive if the URL contained credentials. Treat the import URL as a secret while calling `import()` and avoid logging it. Avoid importing untrusted or private URLs in examples or tests. No remote resources are contacted by the local-only examples above.

## Troubleshooting and limits

- If startup says local Artifacts requires Git, install Git globally, check `git --version` in the dev tool's environment, and restart the tool with `git` on PATH.
- If startup rejects an older storage layout, back it up and reset only the Artifacts persistence directory; a restart alone will not convert it.
- If Git asks for a username and password, use any username and the repository token as the password. If Git reports unauthorized, check the repository, scope, expiry, and revocation; do not use a Cloudflare or GitHub account password.
- If startup reports `Address already in use`, the dev or inspector port may be occupied. Pass `--port=0 --inspector-port=0` and use the printed `Ready on` URL.
- If a Git URL stops responding after restart, get the new `remote` from the binding; the loopback listener is tied to the dev process.
- Local development is not a replica of production placement or infrastructure. The local Git endpoint is HTTP on loopback only, and local state and tokens do not transfer to a remote namespace. Miniflare Artifacts, Wrangler dev, and Vite local fixtures have passed Windows CI; Workers Vitest's Artifacts fixture has not been verified on Windows.
Loading
Loading