Skip to content
Draft
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
6 changes: 3 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ pnpm build
├─ cf#task:generate:plugin
│ └─ forge-plugin build generator/index.ts -o generator/cli.wasm
└─ cf#task:generate: tsx packages/cli/generate.ts
├─ fetch pinned Forge OpenAPI release (or preview bundle)
├─ fetch pinned cloudflare/api-schemas openapi.json (or preview bundle)
├─ prepare it (hidden ops, account/zone routes, cf-cli audience)
├─ rm -rf _generated/
├─ forge normalize --leave-old-extensions -
Expand Down Expand Up @@ -798,8 +798,8 @@ The two Forge inputs are updated separately:
- **`FORGE_OPENAPI_VERSION`** in `packages/cli/generate.ts` — the `Update OpenAPI`
workflow (`.github/workflows/update-openapi.yml`) runs `scripts/update-openapi.ts`
hourly and on a `forge-release-published` `repository_dispatch`. It pins the
latest `openapi@<sha>` Forge release, regenerates with the pinned forge
build, writes a `minor` changeset, and opens or refreshes the
latest `cloudflare/api-schemas` `main` commit SHA, regenerates with the pinned
forge build, writes a `minor` changeset, and opens or refreshes the
`automation/update-forge` PR. The existing branch and concurrency group are
retained so the renamed workflow continues managing existing update PRs.

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ depend on generation, including when invoked from this package directory.
Build and type-check tasks also build workspace dependencies first. `pnpm dev`
starts the source entry without generation or bundling. Task caching is disabled.

`pnpm generate` downloads the pinned Forge `openapi.forge.json` release asset
`pnpm generate` downloads the pinned `cloudflare/api-schemas` `openapi.json`
(or reads a local bundle override), prepares it, and runs the forge
binary with the generator plugin (`forge --target wasm://generator/cli.wasm`)
into a cleared `src/commands/_generated/`; a post-step formats the emitted
Expand Down
9 changes: 4 additions & 5 deletions packages/cli/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import { preserveWorkersSecretUpdatePositional } from "./generator/workers-secre
import type { ForgeOpenApiDocument } from "./generator/openapi.js";

const bundle = process.env.FORGE_OPENAPI_BUNDLE;
// Bump this SHA to generate the commands from a new release.
const FORGE_OPENAPI_VERSION = "ec5b5772f5cf7dfb16c49f3527671edfa37bb666";
// Bump this SHA to generate the commands from a newer cloudflare/api-schemas commit.
const FORGE_OPENAPI_VERSION = "193e683baba9c46b675d91c959c3b9a726b2d184";
const FORGE_OPENAPI_RELEASE = `openapi@${FORGE_OPENAPI_VERSION}`;
const FORGE_OPENAPI_ASSET = "openapi.forge.json";
const FORGE_OPENAPI_ASSET_URL = `https://github.com/cloudflare/forge/releases/download/${FORGE_OPENAPI_RELEASE}/${FORGE_OPENAPI_ASSET}`;
const FORGE_OPENAPI_ASSET = "openapi.json";
const FORGE_OPENAPI_ASSET_URL = `https://raw.githubusercontent.com/cloudflare/api-schemas/${FORGE_OPENAPI_VERSION}/${FORGE_OPENAPI_ASSET}`;
const HTTP_METHODS = [
"get",
"put",
Expand Down Expand Up @@ -84,7 +84,6 @@ function ignoreInvalidHiddenOperations(openapi: ForgeOpenApiDocument): void {
if (!operation || operation["x-forge-hidden"] !== true) {
continue;
}

const responseCodes = Object.keys(operation.responses ?? {});
const hasSuccessResponse = responseCodes.some(
(code) => code === "101" || code === "2XX" || /^2\d{2}$/.test(code)
Expand Down
11 changes: 5 additions & 6 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ pin and adds a patch changeset when generated output changes. Its configuration
lives in `renovate.json5` and `.github/renovate-global.json`.

The OpenAPI workflow runs hourly, manually, or on a `forge-release-published`
repository dispatch. It selects the latest published `openapi@<sha>` release
with an `openapi.forge.json` asset, ignoring package releases, drafts, and
prereleases. It regenerates commands with the existing Forge package pin and
adds a minor changeset. Generation and `pnpm check` must pass before it pushes.
repository dispatch. It resolves the latest commit SHA on `cloudflare/api-schemas`
`main`, regenerates commands with the existing Forge package pin, and adds a
minor changeset. Generation and `pnpm check` must pass before it pushes.

The OpenAPI workflow retains the former `automation/update-forge` branch and
`update-forge` concurrency group so it can continue maintaining existing PRs.
Expand All @@ -26,6 +25,6 @@ other automation refreshes and regenerates its PR against the new `main`.

Run `node scripts/update-openapi.ts --check` to inspect the proposed OpenAPI
update without changing files or PRs. Set `GH_TOKEN` (or `GITHUB_TOKEN`) for
cf's PR and branch operations. Public Forge release requests are anonymous,
and generation and checks run without those credentials. The workflow uses
cf's PR and branch operations. Public `cloudflare/api-schemas` commit lookups
are anonymous, and generation and checks run without those credentials. The workflow uses
`GH_ACCESS_TOKEN` when available so its PR updates trigger normal CI.
40 changes: 12 additions & 28 deletions scripts/update-openapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ await test("Forge latest release reads are anonymous while cf requests can authe
authorization: new Headers(init.headers).get("Authorization"),
});
return Response.json({
tag_name: `openapi@${version}`,
assets: [{ name: "openapi.forge.json", id: 123 }],
sha: version,
});
}
);
Expand All @@ -62,7 +61,7 @@ await test("Forge latest release reads are anonymous while cf requests can authe
});
await githubResponse("/repos/cloudflare/cf/pulls", {}, "cf-write-token");
assert.deepEqual(requests, [
{ path: "/repos/cloudflare/forge/releases/latest", authorization: null },
{ path: "/repos/cloudflare/api-schemas/commits/main", authorization: null },
{
path: "/repos/cloudflare/cf/pulls",
authorization: "Bearer cf-write-token",
Expand All @@ -84,28 +83,14 @@ await test("Forge subprocesses receive no GitHub tokens or injected Git credenti
assert.deepEqual(environment, originalEnvironment);
});

await test("latest releases without the OpenAPI asset are rejected", async (context) => {
for (const assets of [undefined, [], [{ name: "other.json" }]]) {
context.mock.method(globalThis, "fetch", async () =>
Response.json({ tag_name: `openapi@${version}`, assets })
);
await assert.rejects(
getLatestOpenApiRelease(),
/has no openapi\.forge\.json asset/
);
}
});

await test("OpenAPI lookup rejects invalid latest releases without scanning history", async (context) => {
await test("OpenAPI lookup rejects invalid latest commits without scanning history", async (context) => {
for (const response of [
null,
[],
{},
{ tag_name: 123 },
{ tag_name: "v0.2.0" },
{ tag_name: "openapi@invalid" },
{ tag_name: `openapi@${version}`, draft: true },
{ tag_name: `openapi@${version}`, prerelease: true },
{ sha: 123 },
{ sha: "v0.2.0" },
{ sha: "invalid" },
]) {
const requests: string[] = [];
context.mock.method(globalThis, "fetch", async (url: string) => {
Expand All @@ -114,23 +99,23 @@ await test("OpenAPI lookup rejects invalid latest releases without scanning hist
});
await assert.rejects(
getLatestOpenApiRelease(),
/not a published OpenAPI release|does not have a valid OpenAPI tag/
/Latest cloudflare\/api-schemas main commit does not have a valid SHA/
);
assert.deepEqual(requests, ["/repos/cloudflare/forge/releases/latest"]);
assert.deepEqual(requests, ["/repos/cloudflare/api-schemas/commits/main"]);
}
});

await test("a missing latest release is reported without scanning history", async (context) => {
await test("a missing latest commit is reported without scanning history", async (context) => {
const requests: string[] = [];
context.mock.method(globalThis, "fetch", async (url: string) => {
requests.push(new URL(url).pathname);
return Response.json({ message: "Not Found" }, { status: 404 });
});
await assert.rejects(
getLatestOpenApiRelease(),
/GitHub API GET \/repos\/cloudflare\/forge\/releases\/latest failed \(404/
/GitHub API GET \/repos\/cloudflare\/api-schemas\/commits\/main failed \(404/
);
assert.deepEqual(requests, ["/repos/cloudflare/forge/releases/latest"]);
assert.deepEqual(requests, ["/repos/cloudflare/api-schemas/commits/main"]);
});

await test("the renamed updater recognizes old and new bot commits while protecting human edits", async (context) => {
Expand Down Expand Up @@ -321,8 +306,7 @@ await test("release reads retry failures while consuming the response body", asy
);
}
return Response.json({
tag_name: `openapi@${version}`,
assets: [{ name: "openapi.forge.json", id: 123 }],
sha: version,
});
});
const result = getLatestOpenApiRelease();
Expand Down
36 changes: 9 additions & 27 deletions scripts/update-openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import { x } from "tinyexec";
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(SCRIPT_DIR, "..");
const GENERATOR_PATH = join(REPO_ROOT, "packages/cli/generate.ts");
const FORGE_REPOSITORY = "cloudflare/forge";
const OPENAPI_ASSET = "openapi.forge.json";
const OPENAPI_REPOSITORY = "cloudflare/api-schemas";
const OPENAPI_VERSION_PATTERN =
/^const FORGE_OPENAPI_VERSION = "([0-9a-f]{40})";$/m;
const UPDATE_COMMIT_PATTERN =
Expand Down Expand Up @@ -293,40 +292,23 @@ export async function getLatestOpenApiRelease(): Promise<{
tag: string;
version: string;
}> {
// Forge must keep its latest release marker on the current OpenAPI spec.
const release = await githubRequest(
`/repos/${FORGE_REPOSITORY}/releases/latest`,
const commit = await githubRequest(
`/repos/${OPENAPI_REPOSITORY}/commits/main`,
{},
undefined,
"application/vnd.github+json",
readJson
);
if (
!isObject(release) ||
release.draft ||
release.prerelease ||
typeof release.tag_name !== "string"
!isObject(commit) ||
typeof commit.sha !== "string" ||
!/^[0-9a-f]{40}$/.test(commit.sha)
) {
throw new Error("Latest Forge release is not a published OpenAPI release");
}
const match = release.tag_name.match(/^openapi@([0-9a-f]{40})$/);
if (!match?.[1]) {
throw new Error(
`Latest Forge release ${release.tag_name} does not have a valid OpenAPI tag`
);
}
// generate.ts downloads this asset, so never pin a release without it.
const hasAsset =
Array.isArray(release.assets) &&
release.assets.some(
(item) => isObject(item) && item.name === OPENAPI_ASSET
);
if (!hasAsset) {
throw new Error(
`Forge release ${release.tag_name} has no ${OPENAPI_ASSET} asset`
"Latest cloudflare/api-schemas main commit does not have a valid SHA"
);
}
return { tag: release.tag_name, version: match[1] };
return { tag: `openapi@${commit.sha}`, version: commit.sha };
}

/**
Expand Down Expand Up @@ -675,7 +657,7 @@ async function main(): Promise<void> {

const shortVersion = version.slice(0, 12);
const title = `chore: update OpenAPI to ${shortVersion}`;
const releaseUrl = `${GITHUB_SERVER_URL}/${FORGE_REPOSITORY}/releases/tag/${encodeURIComponent(tag)}`;
const releaseUrl = `${GITHUB_SERVER_URL}/${OPENAPI_REPOSITORY}/commit/${version}`;
const body = `Updates cf to [\`${tag}\`](${releaseUrl}).

- pins OpenAPI revision \`${version}\`
Expand Down
Loading