Skip to content
Closed
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
66 changes: 2 additions & 64 deletions scripts/update-forge.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -23,7 +17,6 @@ const {
getForgeEnvironment,
getLatestForgeRelease,
githubResponse,
prepareForgeOpenApi,
run,
} = await import("./update-forge.ts");
if (originalToken === undefined) {
Expand Down Expand Up @@ -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, [
Expand All @@ -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 }),
Expand Down
164 changes: 34 additions & 130 deletions scripts/update-forge.ts
Original file line number Diff line number Diff line change
@@ -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));
Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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] };
}

/**
Expand All @@ -352,53 +344,6 @@ export function getForgeEnvironment(
);
}

export async function prepareForgeOpenApi(
forgeDir: string,
assetId: number
): Promise<void> {
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<UpdatePullRequest | undefined> {
Expand Down Expand Up @@ -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}\`.
`
);
}
Expand Down Expand Up @@ -639,7 +583,7 @@ async function main(): Promise<void> {
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
Expand Down Expand Up @@ -705,80 +649,41 @@ async function main(): Promise<void> {
}
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);
Expand All @@ -793,8 +698,7 @@ async function main(): Promise<void> {
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).
Expand Down
Loading