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
9 changes: 9 additions & 0 deletions .changeset/fix-yarn-classic-init.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"wrangler": patch
---

Fix `wrangler init` failing with Yarn Classic

When using Yarn Classic (v1.x), running `wrangler init` or `wrangler init --from-dash` would fail because Yarn Classic doesn't properly handle version specifiers with special characters like `^` in `yarn create` commands. Yarn would install the package correctly but then fail to find the binary because it would look for a path like `.yarn/bin/create-cloudflare@^2.5.0` instead of `.yarn/bin/create-cloudflare`.

This fix removes the version specifier from the default C3 command entirely. Since C3 has had auto-update behavior for over two years, specifying a version is no longer necessary and removing it resolves the Yarn Classic compatibility issue.
7 changes: 7 additions & 0 deletions .changeset/nasty-laws-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@cloudflare/local-explorer-ui": patch
---

Change the favicon to a Cloudflare logo outline

This is for an experimental WIP project.
7 changes: 7 additions & 0 deletions .changeset/simplify-version-packages-ci-alerts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@cloudflare/devprod-status-bot": patch
---

Simplify Version Packages PR CI failure alerts

The bot now sends an alert for any failing CI job on the Version Packages PR, instead of first fetching the required status checks from GitHub's branch protection API and filtering. This removes unnecessary complexity and ensures all CI failures are reported.
10 changes: 10 additions & 0 deletions .changeset/witty-dolls-win.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@cloudflare/local-explorer-ui": minor
"miniflare": minor
---

Serve the local explorer UI from Miniflare

This bundles the local explorer UI into Miniflare, and if enabled, Miniflare serves the UI at `/cdn-cgi/explorer`.

This is an experimental, WIP feature.
66 changes: 62 additions & 4 deletions fixtures/worker-with-resources/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { resolve } from "path";
import { afterAll, beforeAll, describe, it } from "vitest";
import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived";

const LOCAL_EXPLORER_BASE_PATH = "/cdn-cgi/explorer";
const LOCAL_EXPLORER_API_PATH = `${LOCAL_EXPLORER_BASE_PATH}/api`;

describe("local explorer", () => {
describe("with X_LOCAL_EXPLORER=true", () => {
let ip: string;
Expand All @@ -20,11 +23,11 @@ describe("local explorer", () => {
await stop?.();
});

it("returns local explorer API response for /cdn-cgi/explorer/api", async ({
it(`returns local explorer API response for ${LOCAL_EXPLORER_API_PATH}`, async ({
expect,
}) => {
const response = await fetch(
`http://${ip}:${port}/cdn-cgi/explorer/api/storage/kv/namespaces`
`http://${ip}:${port}${LOCAL_EXPLORER_API_PATH}/storage/kv/namespaces`
);
expect(response.headers.get("Content-Type")).toBe("application/json");
const json = await response.json();
Expand Down Expand Up @@ -56,6 +59,59 @@ describe("local explorer", () => {
const text = await response.text();
expect(text).toBe("Hello World!");
});

it(`serves UI index.html at ${LOCAL_EXPLORER_BASE_PATH}`, async ({
expect,
}) => {
const response = await fetch(
`http://${ip}:${port}${LOCAL_EXPLORER_BASE_PATH}`
);
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe(
"text/html; charset=utf-8"
);
const text = await response.text();
expect(text).toContain("<!doctype html>");
expect(text).toContain("Cloudflare Local Explorer");
});

it(`serves UI assets at ${LOCAL_EXPLORER_BASE_PATH}/assets/*`, async ({
expect,
}) => {
// First get index.html to find the actual asset paths
const indexResponse = await fetch(
`http://${ip}:${port}${LOCAL_EXPLORER_BASE_PATH}`
);
const html = await indexResponse.text();

// Extract JS asset path from the HTML
// The HTML looks like: <script type="module" crossorigin src="/cdn-cgi/explorer/assets/index-xxx.js">
const jsMatch = html.match(/assets\/index-[^"]+\.js/);
expect(jsMatch).not.toBeNull();
const jsPath = jsMatch![0];

// Fetch the JS asset
const jsResponse = await fetch(
`http://${ip}:${port}${LOCAL_EXPLORER_BASE_PATH}/${jsPath}`
);
expect(jsResponse.status).toBe(200);
expect(jsResponse.headers.get("Content-Type")).toMatch(
/^application\/javascript/
);
});

it("serves UI with SPA fallback for unknown routes", async ({ expect }) => {
// Request a route that doesn't exist as a file but should be handled by the SPA
const response = await fetch(
`http://${ip}:${port}${LOCAL_EXPLORER_BASE_PATH}/kv/some-namespace`
);
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe(
"text/html; charset=utf-8"
);
const text = await response.text();
expect(text).toContain("<!doctype html>");
});
});

describe("without X_LOCAL_EXPLORER (default)", () => {
Expand All @@ -74,10 +130,12 @@ describe("local explorer", () => {
await stop?.();
});

it("returns worker response for /cdn-cgi/explorer/api", async ({
it("returns worker response for LOCAL_EXPLORER_API_PATH", async ({
expect,
}) => {
const response = await fetch(`http://${ip}:${port}/cdn-cgi/explorer/api`);
const response = await fetch(
`http://${ip}:${port}${LOCAL_EXPLORER_API_PATH}`
);
const text = await response.text();
expect(text).toBe("Hello World!");
});
Expand Down
112 changes: 4 additions & 108 deletions packages/devprod-status-bot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,46 +52,6 @@ async function isWranglerTeamMember(
}
}

type RequiredStatusChecksResult =
| { success: true; checks: string[] }
| { success: false; error: string };

async function getRequiredStatusChecks(
apiToken: string,
owner: string,
repo: string,
branch: string
): Promise<RequiredStatusChecksResult> {
try {
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/branches/${branch}/protection/required_status_checks`,
{
headers: {
"User-Agent": "Cloudflare ANT Status bot",
Authorization: `Bearer ${apiToken}`,
Accept: "application/vnd.github+json",
},
}
);
if (!response.ok) {
console.error("Failed to fetch required status checks:", response.status);
return { success: false, error: `HTTP ${response.status}` };
}
const data = await response.json<{
// `contexts` is deprecated in favor of `checks` array
contexts?: string[];
checks?: { context: string; app_id?: number | null }[];
}>();
return {
success: true,
checks: data.checks?.map((c) => c.context) || data.contexts || [],
};
} catch (error) {
console.error("Error fetching required status checks:", error);
return { success: false, error: String(error) };
}
}

async function checkForSecurityIssue(
ai: Ai,
apiToken: string,
Expand Down Expand Up @@ -603,42 +563,6 @@ async function sendVersionPackagesCIFailureAlert(
);
}

async function sendGitHubAPIFailureAlert(
webhookUrl: string,
error: string,
additionalInfo: string
) {
return sendMessage(
webhookUrl,
{
cardsV2: [
{
cardId: "github-api-failure",
card: {
header: {
title: "⚠️ GitHub API Request Failed",
subtitle: additionalInfo,
},
sections: [
{
collapsible: false,
widgets: [
{
textParagraph: {
text: `<b>Error:</b> ${error}\n\nThis may affect the bot's ability to filter alerts correctly. Please investigate.`,
},
},
],
},
],
},
},
],
},
"-github-api-failure"
);
}

async function sendUpcomingMeetingMessage(webhookUrl: string, ai: Ai) {
const message = await getBotMessage(
ai,
Expand Down Expand Up @@ -818,41 +742,13 @@ export default {
maybeRepositoryAdvisory
);
}
// Notifies when a required CI check fails on the Version Packages PR
// Notifies when any CI check fails on the Version Packages PR
const maybeVersionPackagesFailure = isVersionPackagesPRCheckRun(body);
if (maybeVersionPackagesFailure) {
// Fetch required status checks from branch protection
const requiredChecksResult = await getRequiredStatusChecks(
env.GITHUB_PAT,
"cloudflare",
"workers-sdk",
"main"
await sendVersionPackagesCIFailureAlert(
env.ALERTS_WEBHOOK,
maybeVersionPackagesFailure
);

if (!requiredChecksResult.success) {
// Alert about the API failure
await sendGitHubAPIFailureAlert(
env.ALERTS_WEBHOOK,
requiredChecksResult.error,
"Fetching required status checks for Version Packages PR"
);
// Still send the CI failure alert since we can't filter
await sendVersionPackagesCIFailureAlert(
env.ALERTS_WEBHOOK,
maybeVersionPackagesFailure
);
} else if (
requiredChecksResult.checks.length === 0 ||
requiredChecksResult.checks.includes(
maybeVersionPackagesFailure.checkRun.name
)
) {
// Only alert if this check is a required check (or if there are no required checks)
await sendVersionPackagesCIFailureAlert(
env.ALERTS_WEBHOOK,
maybeVersionPackagesFailure
);
}
}
}

Expand Down
5 changes: 4 additions & 1 deletion packages/local-explorer-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
},
"dependencies": {
"@base-ui/react": "^1.1.0",
"@phosphor-icons/react": "^2.1.10",
"@tailwindcss/vite": "^4.0.15",
"@tanstack/react-router": "^1.158.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
"react-dom": "^19.2.0",
"tailwindcss": "^4.0.15"
},
"devDependencies": {
"@cloudflare/eslint-config-shared": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/local-explorer-ui/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion packages/local-explorer-ui/src/api/client-config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { LOCAL_EXPLORER_API_PATH } from "../constants";
import type { CreateClientConfig } from "./generated/client.gen";

export const createClientConfig: CreateClientConfig = (config) => ({
...config,
baseUrl: "/cdn-cgi/explorer/api",
baseUrl: LOCAL_EXPLORER_API_PATH,
throwOnError: true,
});
3 changes: 0 additions & 3 deletions packages/local-explorer-ui/src/assets/icons/check.svg

This file was deleted.

4 changes: 0 additions & 4 deletions packages/local-explorer-ui/src/assets/icons/copy.svg

This file was deleted.

5 changes: 0 additions & 5 deletions packages/local-explorer-ui/src/assets/icons/dots.svg

This file was deleted.

16 changes: 9 additions & 7 deletions packages/local-explorer-ui/src/components/AddKVForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,28 +52,30 @@ export function AddKVForm({ onAdd, clearSignal = 0 }: AddKVFormProps) {
const isKeyInvalid = !!validateKey(key);

return (
<form className="add-entry-form" onSubmit={handleSubmit}>
<div className="kv-field">
<form className="flex gap-2 mb-4 items-start" onSubmit={handleSubmit}>
<div className="flex flex-col w-[200px] shrink-0">
<label className="sr-only" htmlFor="add-key">
Key
</label>
<input
id="add-key"
className={`kv-input kv-input--add${keyError ? " kv-input--invalid" : ""}`}
className={`w-full font-mono bg-bg text-text py-2 px-3 text-sm border border-border rounded-md focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(255,72,1,0.15)] disabled:bg-bg-secondary disabled:text-text-secondary ${keyError ? "border-danger focus:shadow-[0_0_0_3px_rgba(251,44,54,0.15)]" : ""}`}
placeholder="Key"
value={key}
onChange={handleKeyChange}
disabled={saving}
/>
{keyError && <span className="field-error">{keyError}</span>}
{keyError && (
<span className="text-danger text-xs mt-1">{keyError}</span>
)}
</div>
<div className="kv-field">
<div className="flex flex-col flex-1 min-w-[200px]">
<label className="sr-only" htmlFor="add-value">
Value
</label>
<textarea
id="add-value"
className="kv-input kv-input--add kv-input--textarea"
className="w-full font-mono bg-bg text-text py-2 px-3 text-sm border border-border rounded-md focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(255,72,1,0.15)] disabled:bg-bg-secondary disabled:text-text-secondary max-h-[200px] resize-none overflow-y-auto [field-sizing:content]"
placeholder="Value"
value={value}
onChange={(e) => setValue(e.target.value)}
Expand All @@ -82,7 +84,7 @@ export function AddKVForm({ onAdd, clearSignal = 0 }: AddKVFormProps) {
</div>
<Button
type="submit"
className="btn btn-primary"
className="btn shrink-0 inline-flex items-center justify-center py-2 px-4 text-sm font-medium border-none rounded-md cursor-pointer transition-[background-color,transform] active:translate-y-px bg-primary text-bg-tertiary hover:bg-primary-hover"
disabled={saving || isKeyInvalid}
focusableWhenDisabled
>
Expand Down
7 changes: 3 additions & 4 deletions packages/local-explorer-ui/src/components/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Button } from "@base-ui/react/button";
import { CheckIcon, CopyIcon } from "@phosphor-icons/react";
import { useState } from "react";
import CheckIcon from "../assets/icons/check.svg?react";
import CopyIcon from "../assets/icons/copy.svg?react";

interface CopyButtonProps {
text: string;
Expand All @@ -18,11 +17,11 @@ export function CopyButton({ text }: CopyButtonProps) {

return (
<Button
className={`copy-btn ${copied ? "copied" : ""}`}
className={`copy-btn flex items-center justify-center w-6 h-6 p-0 border-none rounded bg-transparent text-text-secondary cursor-pointer opacity-0 transition-[opacity,background-color,color] shrink-0 hover:bg-border hover:text-text ${copied ? "opacity-100 text-success" : ""}`}
onClick={handleCopy}
aria-label={copied ? "Copied" : "Copy to clipboard"}
>
{copied ? <CheckIcon /> : <CopyIcon />}
{copied ? <CheckIcon size={14} weight="bold" /> : <CopyIcon size={14} />}
</Button>
);
}
Loading
Loading