diff --git a/docs/architecture.md b/docs/architecture.md index 1f007e4849..6e19bc2e33 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 - @@ -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@` 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. diff --git a/packages/cli/AGENTS.md b/packages/cli/AGENTS.md index e1f80c7f30..691873d706 100644 --- a/packages/cli/AGENTS.md +++ b/packages/cli/AGENTS.md @@ -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 diff --git a/packages/cli/generate.ts b/packages/cli/generate.ts index 0b85d19714..4f8285ae90 100644 --- a/packages/cli/generate.ts +++ b/packages/cli/generate.ts @@ -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", @@ -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) diff --git a/scripts/README.md b/scripts/README.md index 52343009d5..c1f91bad20 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -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@` 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. @@ -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. diff --git a/scripts/update-openapi.test.ts b/scripts/update-openapi.test.ts index 379a474b49..cee98d1615 100644 --- a/scripts/update-openapi.test.ts +++ b/scripts/update-openapi.test.ts @@ -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, }); } ); @@ -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", @@ -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) => { @@ -114,13 +99,13 @@ 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); @@ -128,9 +113,9 @@ await test("a missing latest release is reported without scanning history", asyn }); 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) => { @@ -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(); diff --git a/scripts/update-openapi.ts b/scripts/update-openapi.ts index edd2605b9b..12f36223ec 100644 --- a/scripts/update-openapi.ts +++ b/scripts/update-openapi.ts @@ -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 = @@ -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 }; } /** @@ -675,7 +657,7 @@ async function main(): Promise { 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}\`