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
5 changes: 5 additions & 0 deletions .changeset/c3-exit-code-on-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-cloudflare": patch
---

Fix `create cloudflare` exiting with code `0` even after an unhandled error
11 changes: 11 additions & 0 deletions .changeset/c3-frameworks-update-14235.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"create-cloudflare": patch
---

Update dependencies of "create-cloudflare"

The following dependency versions have been updated:

| Dependency | From | To |
| ------------- | ------ | ------ |
| @tanstack/cli | 0.69.1 | 0.69.2 |
9 changes: 9 additions & 0 deletions .changeset/swift-fox-flies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"create-cloudflare": patch
---

Fix `create cloudflare` aborting with `ERR_PNPM_IGNORED_BUILDS` on pnpm 11

pnpm 11 flipped `strictDepBuilds` to `true` by default, which makes the install fail when dependencies have unapproved build scripts. `wrangler` depends on `workerd` and `esbuild`, and (via miniflare) on `sharp` — all three need their postinstall scripts to produce platform binaries.

C3 now writes or merges in a `pnpm-workspace.yaml` in the generated project that approves exactly those three packages. If other packages trigger this error, C3 also now interactively offers to retry with `pnpm approve-builds <pkg>…`
132 changes: 114 additions & 18 deletions .github/workflows/c3-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,58 @@ permissions:
contents: read
pull-requests: read

# pnpm 11-specific settings - recognised by pnpm 11+ and
# silently ignored by pnpm 10.
#
# - pmOnFail=ignore: pnpm 11's default `download` would silently re-exec as
# the monorepo's pinned pnpm 10.33.0, defeating the matrix override.
# - verifyDepsBeforeRun=false: pnpm 11's default `install` would re-run
# `pnpm install` at the monorepo root before invoking the script, which
# then trips on engines.pnpm.
env:
pnpm_config_pm_on_fail: ignore
pnpm_config_verify_deps_before_run: "false"

jobs:
e2e:
# Runs the non-frameworks C3 E2E tests on all supported operating systems and package managers.
# Runs the non-frameworks C3 E2E tests on all supported operating systems
# and package managers.
timeout-minutes: 45
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.os.name }}-${{ matrix.pm.name }}-${{ matrix.pm.version }}${{ matrix.experimental && '-experimental' || '' }}-${{ matrix.filter }}
cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }}
name: ${{ format('C3 E2E ({0}, {1}{2}) - {3}', matrix.pm.name, matrix.os.description, matrix.experimental && ', experimental' || '', matrix.filter) }}
# `matrix.pm.label` is the display name in job/artifact names. Default
# pm entries use the plain name (e.g. `pnpm`) to keep historical required-
# check names stable; non-default entries (e.g. pnpm 11) suffix the version.
name: ${{ format('C3 E2E ({0}, {1}{2}) - {3}', matrix.pm.label, matrix.os.description, matrix.experimental && ', experimental' || '', matrix.filter) }}
strategy:
fail-fast: false
matrix:
experimental: [false]
filter: ["cli", "workers"]
os: [{ name: ubuntu-latest, description: Linux }]
pm:
- { name: pnpm, version: "10.33.0" }
- { name: npm, version: "0.0.0" }
- { name: pnpm, version: "10.33.0", label: "pnpm" }
- { name: pnpm, version: "11.5.1", label: "pnpm@11.5.1" }
- { name: npm, version: "0.0.0", label: "npm" }
# The yarn tests keep failing on Linux with out of space errors, with no clear reason why. Disabling for now.
# - { name: yarn, version: "1.0.0" }
# - { name: yarn, version: "1.0.0", label: "yarn" }
include:
# Windows and experimental entries stay on the default pnpm to
# preserve historical required-check names. pnpm 11 coverage comes
# from the Linux base entries above.
- os: { name: windows-latest, description: Windows }
pm: { name: pnpm, version: "10.33.0" }
pm: { name: pnpm, version: "10.33.0", label: "pnpm" }
filter: "cli"
- os: { name: windows-latest, description: Windows }
pm: { name: pnpm, version: "10.33.0" }
pm: { name: pnpm, version: "10.33.0", label: "pnpm" }
filter: "workers"
- os: { name: ubuntu-latest, description: Linux }
pm: { name: pnpm, version: "10.33.0" }
pm: { name: pnpm, version: "10.33.0", label: "pnpm" }
experimental: true
filter: "cli"
- os: { name: ubuntu-latest, description: Linux }
pm: { name: pnpm, version: "10.33.0" }
pm: { name: pnpm, version: "10.33.0", label: "pnpm" }
experimental: true
filter: "workers"
runs-on: ${{ matrix.os.name }}
Expand Down Expand Up @@ -82,6 +102,45 @@ jobs:
git config --global user.email wrangler@cloudflare.com
git config --global user.name 'Wrangler Tester'

# Put the matrix pnpm version on PATH so scaffolded-project installs
# run it (not the monorepo's pinned pnpm 10.33.0). Without this, the
# matrix only relabels C3's `npm_config_user_agent` and pnpm 11
# regressions go undetected.
#
# `pnpm/action-setup` refuses when both `version` input and the root
# `package.json#packageManager` are set, so we point it at a stub
# `package.json`. We also loosen `engines.pnpm` to avoid pnpm 11
# bailing with ERR_PNPM_UNSUPPORTED_ENGINE at the monorepo root.
- name: Prepare workspace for matrix pnpm install
if: matrix.pm.name == 'pnpm' && steps.changes.outputs.everything_but_markdown == 'true'
shell: bash
run: |
mkdir -p .ci-pnpm-setup-stub
printf '{"name":"ci-stub","private":true}\n' > .ci-pnpm-setup-stub/package.json
node -e "
const fs = require('node:fs');
const pkg = JSON.parse(fs.readFileSync('package.json','utf8'));
if (pkg.engines && pkg.engines.pnpm) pkg.engines.pnpm = '*';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, '\t') + '\n');
"
- name: Install matrix pnpm version for scaffolded installs
if: matrix.pm.name == 'pnpm' && steps.changes.outputs.everything_but_markdown == 'true'
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
with:
version: ${{ matrix.pm.version }}
package_json_file: .ci-pnpm-setup-stub/package.json
# Distinct dest so `addPath` prepends ahead of the monorepo's
# pinned pnpm 10.x from the earlier `~/setup-pnpm`.
dest: ~/matrix-pnpm
- name: Verify pnpm on PATH matches matrix version
if: matrix.pm.name == 'pnpm' && steps.changes.outputs.everything_but_markdown == 'true'
shell: bash
run: |
got="$(pnpm --version)"
want="${{ matrix.pm.version }}"
echo "pnpm on PATH: $got (expected $want)"
[ "$got" = "$want" ] || { echo "ERROR: PATH pnpm is $got, not $want — scaffolded installs would silently run the wrong version."; exit 1; }

- id: run-e2e
if: steps.changes.outputs.everything_but_markdown == 'true'
shell: bash
Expand All @@ -99,36 +158,44 @@ jobs:
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: ${{ !cancelled() && steps.changes.outputs.everything_but_markdown == 'true' }}
with:
name: ${{ format('e2e-logs-{0}-{1}-{2}-{3}', matrix.pm.name, matrix.os.description, matrix.experimental && 'experimental' || 'normal', matrix.filter) }}
name: ${{ format('e2e-logs-{0}-{1}-{2}-{3}', matrix.pm.label, matrix.os.description, matrix.experimental && 'experimental' || 'normal', matrix.filter) }}
path: packages/create-cloudflare/.e2e-logs${{matrix.experimental == 'true' && '-experimental' || ''}}/${{ matrix.pm.name }}/${{ matrix.filter }}
include-hidden-files: true

- name: Upload Turbo Summary
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: ${{ !cancelled() && steps.changes.outputs.everything_but_markdown == 'true' }}
with:
name: ${{ format('turbo-runs-{0}-{1}-{2}-{3}', matrix.pm.name, matrix.os.description, matrix.experimental && 'experimental' || 'normal', matrix.filter) }}
name: ${{ format('turbo-runs-{0}-{1}-{2}-{3}', matrix.pm.label, matrix.os.description, matrix.experimental && 'experimental' || 'normal', matrix.filter) }}
path: .turbo/runs
include-hidden-files: true

frameworks-e2e:
# Runs the C3 frameworks E2E tests only in the merge queue, on the release branch, if create-cloudflare has changed, or when explicitly requested via label.
#
# Frameworks that delegate install to their own generator (e.g. `hono`)
# don't route through C3's `ERR_PNPM_IGNORED_BUILDS` recovery path; those
# tests opt out of pnpm 11 via `unsupportedPmRanges` in the test config.
timeout-minutes: 45
concurrency:
group: ${{ github.workflow }}-frameworks${{ matrix.experimental && '-experimental' || '' }}-${{ github.ref }}-${{ matrix.os.name }}-${{ matrix.pm.name }}-${{ matrix.pm.version }}
cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }}
name: ${{ format('C3 E2E ({0}, {1}{2}) - frameworks', matrix.pm.name, matrix.os.description, matrix.experimental && ', experimental' || '') }}
name: ${{ format('C3 E2E ({0}, {1}{2}) - frameworks', matrix.pm.label, matrix.os.description, matrix.experimental && ', experimental' || '') }}
strategy:
fail-fast: false
matrix:
experimental: [false]
os: [{ name: ubuntu-latest, description: Linux }]
os:
- { name: ubuntu-latest, description: Linux }
pm:
- { name: pnpm, version: "10.33.0" }
- { name: npm, version: "0.0.0" }
- { name: pnpm, version: "10.33.0", label: "pnpm" }
- { name: pnpm, version: "11.5.1", label: "pnpm@11.5.1" }
- { name: npm, version: "0.0.0", label: "npm" }
include:
# Experimental stays on the default pnpm to preserve historical
# required-check names; pnpm 11 coverage comes from the base entry.
- os: { name: ubuntu-latest, description: Linux }
pm: { name: pnpm, version: "10.33.0" }
pm: { name: pnpm, version: "10.33.0", label: "pnpm" }
experimental: true
runs-on: ${{ matrix.os.name }}
steps:
Expand Down Expand Up @@ -197,6 +264,35 @@ jobs:
git config --global user.email wrangler@cloudflare.com
git config --global user.name 'Wrangler Tester'

# See the equivalent step in the `e2e` job above for context.
- name: Prepare workspace for matrix pnpm install
if: matrix.pm.name == 'pnpm' && steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true'
shell: bash
run: |
mkdir -p .ci-pnpm-setup-stub
printf '{"name":"ci-stub","private":true}\n' > .ci-pnpm-setup-stub/package.json
node -e "
const fs = require('node:fs');
const pkg = JSON.parse(fs.readFileSync('package.json','utf8'));
if (pkg.engines && pkg.engines.pnpm) pkg.engines.pnpm = '*';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, '\t') + '\n');
"
- name: Install matrix pnpm version for scaffolded installs
if: matrix.pm.name == 'pnpm' && steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true'
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
with:
version: ${{ matrix.pm.version }}
package_json_file: .ci-pnpm-setup-stub/package.json
dest: ~/matrix-pnpm
- name: Verify pnpm on PATH matches matrix version
if: matrix.pm.name == 'pnpm' && steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true'
shell: bash
run: |
got="$(pnpm --version)"
want="${{ matrix.pm.version }}"
echo "pnpm on PATH: $got (expected $want)"
[ "$got" = "$want" ] || { echo "ERROR: PATH pnpm is $got, not $want — scaffolded installs would silently run the wrong version."; exit 1; }

- id: run-e2e
if: steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true'
shell: bash
Expand All @@ -214,14 +310,14 @@ jobs:
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: ${{ !cancelled() && steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true' }}
with:
name: ${{ format('e2e-logs-{0}-{1}-{2}-frameworks', matrix.pm.name, matrix.os.description, matrix.experimental && 'experimental' || 'normal') }}
name: ${{ format('e2e-logs-{0}-{1}-{2}-frameworks', matrix.pm.label, matrix.os.description, matrix.experimental && 'experimental' || 'normal') }}
path: packages/create-cloudflare/.e2e-logs${{matrix.experimental == 'true' && '-experimental' || ''}}/${{ matrix.pm.name }}/frameworks
include-hidden-files: true

- name: Upload Turbo Summary
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: ${{ !cancelled() && steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true' }}
with:
name: ${{ format('turbo-runs-{0}-{1}-{2}-frameworks', matrix.pm.name, matrix.os.description, matrix.experimental && 'experimental' || 'normal') }}
name: ${{ format('turbo-runs-{0}-{1}-{2}-frameworks', matrix.pm.label, matrix.os.description, matrix.experimental && 'experimental' || 'normal') }}
path: .turbo/runs
include-hidden-files: true
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"dependencies": {
"@clack/core": "1.2.0",
"@cloudflare/workers-utils": "workspace:*",
"chalk": "5.3.0",
"chalk": "catalog:default",
"ci-info": "catalog:default",
"cross-spawn": "7.0.6",
"log-update": "5.0.1"
Expand Down
26 changes: 25 additions & 1 deletion packages/create-cloudflare/e2e/helpers/framework-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { detectPackageManager } from "helpers/packageManagers";
import { retry } from "helpers/retry";
import * as jsonc from "jsonc-parser";
import semver from "semver";
import { fetch } from "undici";
import { version } from "../../package.json";
import { getFrameworkMap } from "../../src/templates";
Expand All @@ -23,6 +24,7 @@ import {
isExperimental,
runDeployTests,
testPackageManager,
testPackageManagerVersion,
} from "./constants";
import { runC3 } from "./run-c3";
import { kill, spawnWithLogging } from "./spawn";
Expand All @@ -36,6 +38,13 @@ export type FrameworkTestConfig = RunnerConfig & {
nodeCompat: boolean;
unsupportedPms?: string[];
unsupportedOSs?: string[];
/**
* Per–package-manager semver ranges to skip. Keys are pm names ("pnpm",
* "npm", "yarn", "bun"); values are semver ranges. Used on pnpm >=11 for
* frameworks that either run their own install (e.g. Hono) or whose
* recovery prompt fires after the e2e harness has closed stdin.
*/
unsupportedPmRanges?: Partial<Record<string, string>>;
flags?: string[];
extraEnv?: Record<string, string | undefined>;
};
Expand Down Expand Up @@ -417,10 +426,25 @@ export function shouldRunTest(testConfig: FrameworkTestConfig) {
// Skip if the package manager is unsupported
!testConfig.unsupportedPms?.includes(testPackageManager) &&
// Skip if the OS is unsupported
!testConfig.unsupportedOSs?.includes(process.platform)
!testConfig.unsupportedOSs?.includes(process.platform) &&
// Skip if the package-manager version falls inside an unsupported range
!isUnsupportedPmVersion(testConfig)
);
}

function isUnsupportedPmVersion(testConfig: FrameworkTestConfig): boolean {
const range = testConfig.unsupportedPmRanges?.[testPackageManager];
if (!range || !testPackageManagerVersion) {
return false;
}
// Coerce to handle suffixes like "11.5.1-canary"; unparseable → run test.
const coerced = semver.coerce(testPackageManagerVersion);
if (!coerced) {
return false;
}
return semver.satisfies(coerced, range);
}

/**
* Gets the framework config and test info given a `frameworkKey`.
*
Expand Down
41 changes: 41 additions & 0 deletions packages/create-cloudflare/e2e/helpers/run-c3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ export type PromptHandler = {
};
};

/**
* Responder for "opportunistic" prompts that only show up under specific
* conditions (e.g. the pnpm 11 approve-builds confirmation). Fires at most
* once per run, independent of the ordered `promptHandlers` queue.
*/
type BackgroundResponder = {
matcher: RegExp;
keys: string[];
};

export type RunnerConfig = {
promptHandlers?: PromptHandler[];
argv?: string[];
Expand Down Expand Up @@ -86,10 +96,41 @@ export const runC3 = async (
logStream
);

// Responders for prompts that may or may not appear. Note that a responder
// can only write to stdin while it's still open — `handlePrompt` closes
// stdin once the last ordered handler is consumed. Framework tests whose
// recovery prompt fires after that should opt out via `unsupportedPmRanges`.
const backgroundResponders: BackgroundResponder[] = [
{
matcher: /Run `pnpm approve-builds [^`]+` and retry the install\?/,
keys: [keys.enter],
},
];
const handledBackground = new WeakSet<BackgroundResponder>();

const onData = (data: string) => {
handleBackgroundPrompts(data);
handlePrompt(data);
};

const handleBackgroundPrompts = (data: string) => {
const text = stripAnsi(data.toString());
for (const responder of backgroundResponders) {
if (handledBackground.has(responder)) {
continue;
}
if (!responder.matcher.test(text)) {
continue;
}
handledBackground.add(responder);
for (const keystroke of responder.keys) {
if (proc.stdin.writable) {
proc.stdin.write(keystroke);
}
}
}
};

// Clone the prompt handlers so we can consume them destructively
promptHandlers = [...promptHandlers];

Expand Down
Loading
Loading