diff --git a/scripts/update-forge.test.ts b/scripts/update-forge.test.ts index f76f956757..4395e83dcc 100644 --- a/scripts/update-forge.test.ts +++ b/scripts/update-forge.test.ts @@ -1,14 +1,8 @@ /* oxlint-disable turbo/no-undeclared-env-vars -- standalone automation tests */ import assert from "node:assert/strict"; -import { - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - writeFileSync, -} from "node:fs"; +import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; +import { join } from "node:path"; import { test } from "node:test"; import { setImmediate } from "node:timers/promises"; import type { TestContext } from "node:test"; @@ -23,7 +17,6 @@ const { getForgeEnvironment, getLatestForgeRelease, githubResponse, - prepareForgeOpenApi, run, } = await import("./update-forge.ts"); if (originalToken === undefined) { @@ -65,7 +58,6 @@ await test("Forge release reads are anonymous while cf requests can authenticate assert.deepEqual(await getLatestForgeRelease(), { tag: `openapi@${version}`, version, - assetId: 123, }); await githubResponse("/repos/cloudflare/cf/pulls", {}, "cf-write-token"); assert.deepEqual(requests, [ @@ -91,60 +83,6 @@ await test("Forge subprocesses receive no GitHub tokens or injected Git credenti assert.deepEqual(environment, originalEnvironment); }); -await test("OpenAPI assets download anonymously and compatibility fixes preserve the source", async (context) => { - const forgeDir = mkdtempSync(join(tmpdir(), "cf-forge-auth-test-")); - context.after(() => rmSync(forgeDir, { recursive: true, force: true })); - const compatibilityPath = join( - forgeDir, - "packages/forge/shared/fern-openapi-compat.ts" - ); - mkdirSync(dirname(compatibilityPath), { recursive: true }); - writeFileSync(join(forgeDir, "package.json"), '{"type":"module"}'); - writeFileSync( - compatibilityPath, - ` -export function applyFernCompatibilityFixes(source) { - source.paths["/compatibility-fix"] = {}; - return { fixes: 1 }; -} -` - ); - - const source = { openapi: "3.0.0", paths: {} }; - let downloaded = false; - context.mock.method( - globalThis, - "fetch", - async (url: string, init: RequestInit) => { - assert.equal( - new URL(url).pathname, - "/repos/cloudflare/forge/releases/assets/123" - ); - const headers = new Headers(init.headers); - assert.equal(headers.get("Authorization"), null); - assert.equal(headers.get("Accept"), "application/octet-stream"); - downloaded = true; - return Response.json(source); - } - ); - - await prepareForgeOpenApi(forgeDir, 123); - assert.equal(downloaded, true); - assert.deepEqual( - JSON.parse(readFileSync(join(forgeDir, "openapi.json"), "utf8")), - source - ); - assert.deepEqual( - JSON.parse( - readFileSync( - join(forgeDir, "packages/cloudflare-fern-config/fern/openapi.json"), - "utf8" - ) - ), - { openapi: "3.0.0", paths: { "/compatibility-fix": {} } } - ); -}); - function transportError(code = "UND_ERR_SOCKET"): TypeError { return new TypeError("fetch failed", { cause: Object.assign(new Error("other side closed"), { code }), diff --git a/scripts/update-forge.ts b/scripts/update-forge.ts index 8e7074fc2f..3e9fbdb82f 100644 --- a/scripts/update-forge.ts +++ b/scripts/update-forge.ts @@ -1,15 +1,8 @@ #!/usr/bin/env node -import { - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - writeFileSync, -} from "node:fs"; -import { tmpdir } from "node:os"; +import { readFileSync, writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import timers from "node:timers/promises"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { fileURLToPath } from "node:url"; import { x } from "tinyexec"; const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); @@ -298,7 +291,6 @@ function extractOpenApiVersion(source: string, label: string): string { export async function getLatestForgeRelease(): Promise<{ tag: string; version: string; - assetId: number; }> { const release = await githubRequest( `/repos/${FORGE_REPOSITORY}/releases/latest`, @@ -328,7 +320,7 @@ export async function getLatestForgeRelease(): Promise<{ ); } - return { tag: release.tag_name, version: match[1], assetId: asset.id }; + return { tag: release.tag_name, version: match[1] }; } /** @@ -352,53 +344,6 @@ export function getForgeEnvironment( ); } -export async function prepareForgeOpenApi( - forgeDir: string, - assetId: number -): Promise { - const source = await githubRequest( - `/repos/${FORGE_REPOSITORY}/releases/assets/${assetId}`, - {}, - undefined, - "application/octet-stream", - readJson - ); - if (!isObject(source) || !isObject(source.paths)) { - throw new Error(`Forge release asset ${OPENAPI_ASSET} is not OpenAPI JSON`); - } - - const compatibilityModulePath = join( - forgeDir, - "packages/forge/shared/fern-openapi-compat.ts" - ); - const compatibilityModule = (await import( - pathToFileURL(compatibilityModulePath).href - )) as { - applyFernCompatibilityFixes?: (openapi: object) => unknown; - }; - const applyFernCompatibilityFixes = - compatibilityModule.applyFernCompatibilityFixes; - if (typeof applyFernCompatibilityFixes !== "function") { - throw new Error( - `Forge checkout has no applyFernCompatibilityFixes export at ${compatibilityModulePath}` - ); - } - - const fernSource = structuredClone(source); - const fixes = applyFernCompatibilityFixes(fernSource); - const rootSpecPath = join(forgeDir, "openapi.json"); - const fernSpecPath = join( - forgeDir, - "packages/cloudflare-fern-config/fern/openapi.json" - ); - mkdirSync(dirname(fernSpecPath), { recursive: true }); - writeFileSync(rootSpecPath, `${JSON.stringify(source, null, 2)}\n`); - writeFileSync(fernSpecPath, `${JSON.stringify(fernSource, null, 2)}\n`); - console.log( - `Prepared Forge OpenAPI build inputs (${JSON.stringify(fixes)}).` - ); -} - async function getOpenUpdatePullRequest( repository: string ): Promise { @@ -514,8 +459,7 @@ function writeChangeset(version: string): void { "cf": minor --- -Update the generated command surface and vendored Forge packages for -Forge OpenAPI release \`${version}\`. +Update the generated command surface for Forge OpenAPI release \`${version}\`. ` ); } @@ -639,7 +583,7 @@ async function main(): Promise { const checkOnly = args.has("--check"); const repository = await inferRepository(); logStep("Checking the latest Forge release"); - const { tag, version, assetId } = await getLatestForgeRelease(); + const { tag, version } = await getLatestForgeRelease(); const current = extractOpenApiVersion( readFileSync(GENERATOR_PATH, "utf8"), GENERATOR_PATH @@ -705,80 +649,41 @@ async function main(): Promise { } await assertCleanWorktree(); - const tempRoot = mkdtempSync( - join(process.env.RUNNER_TEMP ?? tmpdir(), "cf-update-forge-") - ); - const forgeDir = join(tempRoot, "forge"); - let forgeSourceSha: string; + // Only the OpenAPI pin moves. The commands are generated by the plugin in + // packages/cli/generator, which the vendored forge binary runs; updating + // that binary is a manual `pnpm sync:forge` (see scripts/sync-forge.ts). const forgeEnvironment = getForgeEnvironment(); const failedChecks: string[] = []; - try { - logStep(`Cloning Forge release ${tag}`); - await run( - "git", - [ - "clone", - "--depth", - "1", - "--branch", - tag, - "--single-branch", - `https://github.com/${FORGE_REPOSITORY}.git`, - forgeDir, - ], - { env: forgeEnvironment } - ); - forgeSourceSha = await output( - "git", - ["rev-parse", "HEAD"], - forgeDir, - forgeEnvironment - ); - - logStep("Installing the Forge workspace"); - await run("pnpm", ["--dir", forgeDir, "install", "--frozen-lockfile"], { - env: forgeEnvironment, - }); + logStep("Pinning the OpenAPI revision"); + updateOpenApiVersion(version); + writeChangeset(version); - logStep("Preparing Forge OpenAPI build inputs"); - await prepareForgeOpenApi(forgeDir, assetId); - - // The commands are generated by the plugin in packages/cli/generator, - // which the vendored forge binary runs (see scripts/sync-forge.ts), - // so only the OpenAPI pin moves here. - logStep("Pinning the OpenAPI revision"); - updateOpenApiVersion(version); - - writeChangeset(version); - logStep("Regenerating the command surface"); - let generated = true; - try { - await run("pnpm", ["generate"], { env: forgeEnvironment }); - } catch { - generated = false; - failedChecks.push("pnpm generate"); - console.warn("Generation failed; continuing to open the update PR."); - } + logStep("Regenerating the command surface"); + let generated = true; + try { + await run("pnpm", ["generate"], { env: forgeEnvironment }); + } catch { + generated = false; + failedChecks.push("pnpm generate"); + console.warn("Generation failed; continuing to open the update PR."); + } - logStep("Validating the generated update"); + logStep("Validating the generated update"); + try { + await run("git", ["diff", "--check"], { env: forgeEnvironment }); + } catch { + failedChecks.push("git diff --check"); + console.warn("Diff validation failed; continuing to open the update PR."); + } + if (generated) { try { - await run("git", ["diff", "--check"], { env: forgeEnvironment }); + await run("pnpm", ["check"], { env: forgeEnvironment }); } catch { - failedChecks.push("git diff --check"); - console.warn("Diff validation failed; continuing to open the update PR."); - } - if (generated) { - try { - await run("pnpm", ["check"], { env: forgeEnvironment }); - } catch { - failedChecks.push("pnpm check"); - console.warn( - "Repository checks failed; continuing to open the update PR." - ); - } + failedChecks.push("pnpm check"); + console.warn( + "Repository checks failed; continuing to open the update PR." + ); } - } finally { - rmSync(tempRoot, { recursive: true, force: true }); } const shortVersion = version.slice(0, 12); @@ -793,8 +698,7 @@ async function main(): Promise { const body = `Updates cf to [\`${tag}\`](${releaseUrl}). - pins OpenAPI revision \`${version}\` -- matches Forge source \`${forgeSourceSha}\` from the release tag -- attempts to regenerate the committed command surface +- attempts to regenerate the committed command surface with the vendored Forge binary ${validationNote} This PR is maintained automatically by [the Update Forge workflow](${GITHUB_SERVER_URL}/${repository}/actions/workflows/update-forge.yml).