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
12 changes: 12 additions & 0 deletions .changeset/dependabot-update-13993.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"miniflare": patch
"wrangler": patch
---

Update dependencies of "miniflare", "wrangler"

The following dependency versions have been updated:

| Dependency | From | To |
| ---------- | ------------ | ------------ |
| workerd | 1.20260520.1 | 1.20260521.1 |
7 changes: 7 additions & 0 deletions .changeset/include-column-names-d1-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": patch
---

Include column names in D1 SQL export INSERT statements

D1 SQL exports now include column names in INSERT statements (e.g., `INSERT INTO "table" ("col1","col2") VALUES(...)`). This ensures that exported SQL can be successfully imported even when the target table has columns in a different order than the original, which commonly occurs during iterative development when schemas evolve.
40 changes: 40 additions & 0 deletions .changeset/workflows-schedule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
"wrangler": minor
---

Add `schedule` property to Workflow bindings for cron-based triggering

> **Note:** This is a configuration-only change. Scheduled triggering of Workflow instances is not yet available — adding `schedule` to a Workflow binding will not result in scheduled invocations at this time. This change lays the groundwork for an upcoming feature.

Workflow bindings in `wrangler.json` now accept an optional `schedule` field that configures one or more cron expressions to automatically trigger new workflow instances on a schedule.

```jsonc
// wrangler.json
{
"workflows": [
{
"binding": "MY_WORKFLOW",
"name": "my-workflow",
"class_name": "MyWorkflow",
"schedule": "0 9 * * 1",
},
],
}
```

Multiple schedules can be provided as an array:

```jsonc
{
"workflows": [
{
"binding": "MY_WORKFLOW",
"name": "my-workflow",
"class_name": "MyWorkflow",
"schedule": ["0 9 * * 1", "0 17 * * 5"],
},
],
}
```

The schedule is sent to the Workflows control plane on `wrangler deploy`. Configuring `schedule` on a workflow binding that references an external `script_name` is an error — the schedule must be configured on the worker that defines the workflow.
138 changes: 138 additions & 0 deletions .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
name: "Dependabot - auto-merge workerd updates"

# workerd ships a release every weekday, so the workerd-and-workers-types
# Dependabot group produces a steady stream of mechanical PRs that bump
# `workerd`, `@cloudflare/workers-types`, and miniflare's pinned version in
# lockstep (see .github/dependabot.yml). When CI is green these PRs require
# no human review, so we enable GitHub auto-merge on them — required status
# checks remain the gate, and a failing build still parks the PR for a human.
#
# Security model: this workflow effectively bypasses the human-review
# requirement on PRs whose head branch matches the Dependabot naming pattern,
# so we have to be paranoid about exactly what we're auto-merging. Before
# enabling auto-merge we verify that the PR contains exactly the two commits
# we expect (one from Dependabot, one from `miniflare-dependabot-versioning-prs.yml`)
# and that nothing outside the expected fileset has been touched. If any
# subsequent push violates those invariants we actively *disable* auto-merge,
# so a maintainer pushing a follow-up commit cancels rather than rides the
# auto-merge.
#
# DO NOT add `actions/checkout` to this workflow — `pull_request_target`
# grants write-scoped tokens, and checking out PR-controlled code with
# those tokens is the standard pwn vector.

on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]

permissions:
contents: write
pull-requests: write

jobs:
enable-auto-merge:
if: github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-slim
steps:
- name: Fetch Dependabot metadata
id: meta
uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 # v2.3.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Verify PR matches expected workerd-bump shape
id: verify
if: steps.meta.outputs.dependency-group == 'workerd-and-workers-types'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail

# Pull commits and changed files via the GitHub API.
commits_json=$(gh pr view "$PR_NUMBER" --json commits)
files_json=$(gh pr view "$PR_NUMBER" --json files)

fail() {
echo "verified=false" >> "$GITHUB_OUTPUT"
echo "reason=$1" >> "$GITHUB_OUTPUT"
echo "::warning::Refusing to enable auto-merge: $1"
exit 0
}

# --- Commit shape ---------------------------------------------------
# Expected: exactly two commits.
# 1. dependabot[bot] author, with a verified GitHub signature.
# 2. The changeset commit pushed by miniflare-dependabot-versioning-prs.yml,
# authored as `Wrangler automated PR updater <wrangler@cloudflare.com>`.
#
# We can't require a signature on the second commit (it's pushed via
# GH_ACCESS_TOKEN, which doesn't sign), so we lean on path/content
# checks below to constrain what that commit can do.

commit_count=$(echo "$commits_json" | jq '.commits | length')
if [ "$commit_count" -ne 2 ]; then
fail "expected exactly 2 commits, found $commit_count"
fi

first_author=$(echo "$commits_json" | jq -r '.commits[0].authors[0].login')
first_oid=$(echo "$commits_json" | jq -r '.commits[0].oid')
if [ "$first_author" != "dependabot[bot]" ]; then
fail "first commit author is '$first_author', expected 'dependabot[bot]'"
fi

# `gh pr view --json commits` doesn't expose signature info, so look
# it up via the REST commit endpoint.
first_verified=$(gh api "repos/${{ github.repository }}/commits/$first_oid" --jq '.commit.verification.verified')
if [ "$first_verified" != "true" ]; then
fail "first commit (Dependabot) does not have a verified signature"
fi

second_email=$(echo "$commits_json" | jq -r '.commits[1].authors[0].email // ""')
second_message=$(echo "$commits_json" | jq -r '.commits[1].messageHeadline // ""')
if [ "$second_email" != "wrangler@cloudflare.com" ]; then
fail "second commit author email is '$second_email', expected 'wrangler@cloudflare.com'"
fi
if ! echo "$second_message" | grep -qE '^Update dependencies of '; then
fail "second commit message '$second_message' does not match expected changeset commit shape"
fi

# --- Changed files allowlist ---------------------------------------
# The only paths a workerd bump should touch:
# - .changeset/dependabot-update-*.md (added by the changeset job)
# - packages/*/package.json (workerd, workers-types pins)
# - pnpm-lock.yaml
# - pnpm-workspace.yaml (catalog entries)
allowed='^(\.changeset/dependabot-update-.*\.md|packages/[^/]+/package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml)$'

unexpected=$(echo "$files_json" | jq -r '.files[].path' | grep -vE "$allowed" || true)
if [ -n "$unexpected" ]; then
fail "PR touches unexpected files:$(echo "$unexpected" | sed 's/^/ /' | tr '\n' ',')"
fi

# Make sure the changeset is actually present — the changeset job
# may not have run yet, in which case we bail and wait for the
# `synchronize` event from its push.
if ! echo "$files_json" | jq -e '.files[] | select(.path | startswith(".changeset/dependabot-update-"))' > /dev/null; then
fail "changeset file not yet present; waiting for changeset job to push it"
fi

echo "verified=true" >> "$GITHUB_OUTPUT"

- name: Enable auto-merge
if: steps.meta.outputs.dependency-group == 'workerd-and-workers-types' && steps.verify.outputs.verified == 'true'
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Disable auto-merge if verification failed
# If a previous run enabled auto-merge but a later push broke the
# invariants, actively cancel auto-merge so the bad commit can't ride.
# `gh pr merge --disable-auto` is a no-op (and exits 0) if auto-merge
# was never enabled, so this is safe to always run on the failure path.
if: always() && steps.meta.outputs.dependency-group == 'workerd-and-workers-types' && steps.verify.outputs.verified != 'true'
run: gh pr merge --disable-auto "$PR_URL" || true
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion packages/miniflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"@cspotcode/source-map-support": "0.8.1",
"sharp": "^0.34.5",
"undici": "catalog:default",
"workerd": "1.20260520.1",
"workerd": "1.20260521.1",
"ws": "catalog:default",
"youch": "4.1.0-beta.10"
},
Expand Down
48 changes: 46 additions & 2 deletions packages/miniflare/src/workers/d1/dumpSql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@
// as possible, with any deviations noted.
import type { SqlStorage } from "@cloudflare/workers-types/experimental";

/** Stats tracking for dumpSql. Will be mutated in place if provided. */
export interface DumpSqlStats {
rows_read: number;
rows_written: number;
// Stats for tracking INSERT statement sizes (column names are always included)
/** Number of INSERT statements over 100KB (current size, with column names) */
inserts_over_100kb_with_column_names: number;
/** Number of INSERT statements that would be over 100KB without column names (for backward comparison) */
inserts_already_over_100kb: number;
/** Total number of INSERT statements generated */
total_inserts: number;
/** Maximum INSERT statement size without column names (hypothetical, for backward comparison) */
max_insert_size: number;
/** Maximum INSERT statement size (current size, with column names) */
max_insert_size_with_column_names: number;
}

export function* dumpSql(
db: SqlStorage,
options?: {
Expand All @@ -13,7 +30,7 @@ export function* dumpSql(
tables?: string[];
},
/** Optional stats tracking. Will be mutated in place if provided */
stats?: { rows_read: number; rows_written: number }
stats?: DumpSqlStats
) {
// WARNING: the caller in D1 assumes non-empty exports, so think carefully before removing this initial yield.
yield `PRAGMA defer_foreign_keys=TRUE;`;
Expand Down Expand Up @@ -83,6 +100,9 @@ export function* dumpSql(

const select = `SELECT ${columns.map((c) => escapeId(c.name)).join(", ")} FROM ${escapeId(table)};`;
const rows_cursor = db.exec(select);
const columnNames = columns.map((c) => escapeId(c.name)).join(",");
// The column names portion is: " (" + columnNames + ")" = 3 + columnNames.length
const columnNamesOverhead = 3 + columnNames.length;
for (const dataRow of rows_cursor.raw()) {
const formattedCells = dataRow.map((cell: unknown, i: number) => {
const colType = columns[i].type;
Expand All @@ -109,7 +129,31 @@ export function* dumpSql(
}
});

yield `INSERT INTO ${escapeId(table)} VALUES(${formattedCells.join(",")});`;
const insertStmt = `INSERT INTO ${escapeId(table)} (${columnNames}) VALUES(${formattedCells.join(",")});`;

// Track stats for INSERT statement sizes
if (stats) {
const currentSize = insertStmt.length;
// Calculate what the size would be without column names (for comparison)
const sizeWithoutColumnNames = currentSize - columnNamesOverhead;
const LIMIT = 100 * 1024; // 100KB

stats.total_inserts++;
if (sizeWithoutColumnNames > LIMIT) {
stats.inserts_already_over_100kb++;
}
if (currentSize > LIMIT) {
stats.inserts_over_100kb_with_column_names++;
}
if (sizeWithoutColumnNames > stats.max_insert_size) {
stats.max_insert_size = sizeWithoutColumnNames;
}
if (currentSize > stats.max_insert_size_with_column_names) {
stats.max_insert_size_with_column_names = currentSize;
}
}

yield insertStmt;
}
if (stats) {
stats.rows_read += rows_cursor.rowsRead;
Expand Down
2 changes: 2 additions & 0 deletions packages/workers-utils/src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,8 @@ export type WorkflowBinding = {
/** Maximum number of steps a Workflow instance can execute */
steps?: number;
};
/** Optional cron schedule(s) for automatically triggering workflow instances */
schedule?: string | string[];
};

/**
Expand Down
38 changes: 38 additions & 0 deletions packages/workers-utils/src/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2699,6 +2699,43 @@ const validateWorkflowBinding: ValidatorFn = (diagnostics, field, value) => {
isValid = false;
}

if (hasProperty(value, "schedule") && value.schedule !== undefined) {
if (typeof value.schedule === "string") {
if (value.schedule.length === 0) {
diagnostics.errors.push(
`"${field}" bindings "schedule" field must not be an empty string.`
);
isValid = false;
}
} else if (Array.isArray(value.schedule)) {
if (value.schedule.length === 0) {
diagnostics.errors.push(
`"${field}" bindings "schedule" field must not be an empty array.`
);
isValid = false;
} else if (!value.schedule.every((s: unknown) => typeof s === "string")) {
diagnostics.errors.push(
`"${field}" bindings should, optionally, have a string or array of strings "schedule" field but got ${JSON.stringify(
value
)}.`
);
isValid = false;
} else if (value.schedule.some((s: unknown) => s === "")) {
diagnostics.errors.push(
`"${field}" bindings "schedule" field must not contain empty strings.`
);
isValid = false;
}
} else {
diagnostics.errors.push(
`"${field}" bindings should, optionally, have a string or array of strings "schedule" field but got ${JSON.stringify(
value
)}.`
);
isValid = false;
}
}

if (hasProperty(value, "limits") && value.limits !== undefined) {
if (
typeof value.limits !== "object" ||
Expand Down Expand Up @@ -2747,6 +2784,7 @@ const validateWorkflowBinding: ValidatorFn = (diagnostics, field, value) => {
"script_name",
"remote",
"limits",
"schedule",
]);

return isValid;
Expand Down
1 change: 1 addition & 0 deletions packages/workers-utils/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export interface CfWorkflow {
limits?: {
steps?: number;
};
schedule?: string | string[];
}

export interface CfQueue {
Expand Down
Loading
Loading