diff --git a/deno.json b/deno.json index 985554cf63..ea686947e4 100644 --- a/deno.json +++ b/deno.json @@ -496,7 +496,7 @@ "build:storybook": "npm --prefix storybook run build-storybook", "storybook:check": "deno test --no-lock --config=scripts/test.deno.json --no-check --allow-read scripts/storybook/storybook-workbench.test.ts", "lint": "DENO_NO_PACKAGE_JSON=1 deno lint && deno lint --config=scripts/test.deno.json scripts/test/ scripts/build/dnt-meta-property-safety.ts scripts/build/dnt-meta-property-safety.test.ts scripts/build/dnt-polyfill.ts scripts/build/dnt-polyfill.test.ts scripts/build/npm-package-metadata.test.ts scripts/build/prepare-framework-sources.test.ts && deno lint --config=scripts/codemods/deno.json scripts/codemods/", - "lint:ci": "deno task lint && deno task lint:core-deps && deno task lint:cross-runtime-jsr && deno task lint:dependency-boundaries && deno task lint:module-boundaries && deno task lint:client-bundle && deno task lint:extension-contracts && deno task lint:extension-capabilities && deno task lint:ban-test-only && deno task lint:sanitizer-baseline && deno task lint:skipped-tests && deno task lint:chat-ratchets && deno task lint:chat-composability && deno task lint:rfc-status && deno task lint:esm-sh-codemod && deno task lint:test-typecheck && deno task lint:cwd-relative-test-reads && deno task lint:dnt-meta-properties && deno task storybook:check && deno task docs:api-reference:check && deno task docs:errors:check && deno task docs:public:check && deno test --frozen --config=scripts/test.deno.json --no-check --allow-read --allow-write --allow-run=bash scripts/ci/setup-deno-workflow.test.ts scripts/ci/prepare-rc-build.test.ts scripts/build/generated-artifact-checks.test.ts", + "lint:ci": "deno task lint && deno task lint:core-deps && deno task lint:cross-runtime-jsr && deno task lint:dependency-boundaries && deno task lint:module-boundaries && deno task lint:client-bundle && deno task lint:extension-contracts && deno task lint:extension-capabilities && deno task lint:ban-test-only && deno task lint:sanitizer-baseline && deno task lint:skipped-tests && deno task lint:chat-ratchets && deno task lint:chat-composability && deno task lint:rfc-status && deno task lint:esm-sh-codemod && deno task lint:test-typecheck && deno task lint:cwd-relative-test-reads && deno task lint:dnt-meta-properties && deno task storybook:check && deno task docs:api-reference:check && deno task docs:errors:check && deno task docs:public:check && deno test --frozen --config=scripts/test.deno.json --no-check --allow-read --allow-write --allow-run=bash scripts/ci/setup-deno-workflow.test.ts scripts/ci/prepare-rc-build.test.ts scripts/build/generated-artifact-checks.test.ts scripts/release.test.ts", "fmt": "deno fmt src/ cli/ react/ templates/ && deno fmt --config=scripts/test.deno.json scripts/test/ scripts/build/dnt-meta-property-safety.ts scripts/build/dnt-meta-property-safety.test.ts scripts/build/dnt-polyfill.ts scripts/build/dnt-polyfill.test.ts scripts/build/prepare-framework-sources.test.ts && deno fmt --config=scripts/codemods/deno.json scripts/codemods/", "fmt:check": "deno fmt --check src/ cli/ react/ templates/ && deno fmt --check --config=scripts/test.deno.json scripts/test/ scripts/build/dnt-meta-property-safety.ts scripts/build/dnt-meta-property-safety.test.ts scripts/build/dnt-polyfill.ts scripts/build/dnt-polyfill.test.ts scripts/build/prepare-framework-sources.test.ts && deno fmt --check --config=scripts/codemods/deno.json scripts/codemods/", "typecheck": "deno task generate:manifests:check && deno check src/index.ts cli/main.ts src/server/index.ts src/routing/api/index.ts src/rendering/index.ts src/platform/index.ts src/platform/adapters/index.ts src/build/index.ts src/build/production-build/index.ts src/transforms/index.ts src/config/index.ts src/utils/index.ts src/data/index.ts src/security/index.ts src/middleware/index.ts src/server/handlers/dev/index.ts src/server/handlers/request/api/index.ts src/rendering/cache/index.ts src/rendering/cache/stores/index.ts src/rendering/rsc/actions/index.ts src/html/index.ts src/html/hydration-script-builder/runtime/main.ts src/modules/index.ts src/proxy/main.ts src/react/components/ui/index.ts src/chat/index.ts src/markdown/index.ts src/mdx/index.ts src/fs/index.ts src/oauth/index.ts src/agent/index.ts src/agent/service/route-export.check.ts src/eval/index.ts src/tool/index.ts src/workflow/index.ts src/prompt/index.ts src/resource/index.ts src/runs/index.ts src/mcp/index.ts src/provider/index.ts", diff --git a/scripts/release-version.ts b/scripts/release-version.ts new file mode 100644 index 0000000000..631e36681a --- /dev/null +++ b/scripts/release-version.ts @@ -0,0 +1,22 @@ +/** + * Version rewriting for the release task. + * + * Kept apart from release.ts so it can be tested without pulling in that + * module's runtime dependencies. + */ + +/** + * Replace the version in deno.json source text, leaving everything else byte for + * byte as it was. + * + * Re-serialising the parsed object instead reflows the whole file -- it expands + * inline arrays such as `"dependencies": ["build:npm"]` across several lines -- + * burying the one meaningful line under churn a human then has to revert. + */ +export function bumpDenoJsonVersion(source: string, newVersion: string): string { + const versionField = /("version"\s*:\s*")[^"]*(")/; + if (!versionField.test(source)) { + throw new Error('Could not find a "version" field in deno.json'); + } + return source.replace(versionField, `$1${newVersion}$2`); +} diff --git a/scripts/release.test.ts b/scripts/release.test.ts new file mode 100644 index 0000000000..8c35fae540 --- /dev/null +++ b/scripts/release.test.ts @@ -0,0 +1,84 @@ +import { assertEquals, assertStringIncludes, assertThrows } from "#std/assert"; +import { describe, it } from "#std/testing/bdd"; +import { bumpDenoJsonVersion } from "./release-version.ts"; + +// A deno.json shaped like the real one: an inline array is what re-serialising +// used to reflow, so it has to survive a bump untouched. +const DENO_JSON = `{ + "name": "veryfront", + "version": "0.1.1236", + "tasks": { + "test:node": { + "command": "node ./tests/node/run-tests.mjs", + "dependencies": ["build:npm"] + }, + "test:bun": { + "command": "node ./tests/bun/run-tests.mjs", + "dependencies": ["build:npm"] + } + } +} +`; + +describe("scripts/release", () => { + describe("bumpDenoJsonVersion", () => { + it("changes the version and nothing else", () => { + const bumped = bumpDenoJsonVersion(DENO_JSON, "0.1.1237"); + + assertStringIncludes(bumped, '"version": "0.1.1237"'); + assertEquals( + bumped.split("\n").filter((line, index) => line !== DENO_JSON.split("\n")[index]), + [' "version": "0.1.1237",'], + ); + }); + + it("leaves inline arrays inline", () => { + // The regression: JSON.stringify expanded these across three lines each, + // so a one-line release commit arrived carrying unrelated reformatting. + const bumped = bumpDenoJsonVersion(DENO_JSON, "0.1.1237"); + + assertEquals( + bumped.match(/"dependencies": \["build:npm"\]/g)?.length, + 2, + ); + assertEquals(bumped.includes('"dependencies": [\n'), false); + }); + + it("keeps the file otherwise byte for byte", () => { + const bumped = bumpDenoJsonVersion(DENO_JSON, "0.1.1237"); + + assertEquals( + bumped.replace('"version": "0.1.1237"', '"version": "0.1.1236"'), + DENO_JSON, + ); + }); + + it("refuses a file with no version field", () => { + assertThrows( + () => bumpDenoJsonVersion('{\n "name": "veryfront"\n}\n', "0.1.1237"), + Error, + 'Could not find a "version" field', + ); + }); + }); + + describe("release ordering", () => { + it("regenerates version artifacts before committing", async () => { + // hydration-runtime.generated.ts embeds its own VERSION and cannot be + // reached by the regex pass, so the generate step has to run before the + // commit. Reordering these silently reintroduces a stale-artifact release + // that fails generate:manifests:check on the required typecheck shard. + const source = await Deno.readTextFile(new URL("./release.ts", import.meta.url)); + const generateAt = source.indexOf('"deno", "task", "generate"'); + const commitAt = source.indexOf('"git", "commit"'); + + assertEquals(generateAt > -1, true, "release must run deno task generate"); + assertEquals(commitAt > -1, true, "release must create the commit"); + assertEquals( + generateAt < commitAt, + true, + "deno task generate must run before the release commit", + ); + }); + }); +}); diff --git a/scripts/release.ts b/scripts/release.ts index 955053b018..d344a8eb07 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -8,6 +8,7 @@ */ import { createFileSystem } from "../src/platform/compat/fs.ts"; +import { bumpDenoJsonVersion } from "./release-version.ts"; import { exit, getArgs } from "../src/platform/compat/process.ts"; import { promptUser } from "../cli/utils/index.ts"; @@ -184,6 +185,7 @@ async function updateExampleVersions(newVersion: string) { } } + async function updateTemplates(newVersion: string) { console.log("\nšŸ“ Updating template versions..."); const filesToUpdate = [ @@ -276,11 +278,14 @@ async function runRelease() { // 2. Update deno.json console.log("\nšŸ“ Updating version in deno.json..."); if (!DRY_RUN) { + // Rewrite the version in place rather than re-serialising the parsed + // object. JSON.stringify reflows the whole file -- it expands inline + // arrays such as `"dependencies": ["build:npm"]` across several lines -- + // which buries the one meaningful line under unrelated churn that then + // has to be reverted by hand. + const source = await fs.readTextFile(denoJsonPath); + await fs.writeTextFile(denoJsonPath, bumpDenoJsonVersion(source, newVersion)); denoJson.version = newVersion; - await fs.writeTextFile( - denoJsonPath, - JSON.stringify(denoJson, null, 2) + "\n", - ); } // 2.5 Update examples @@ -295,6 +300,16 @@ async function runRelease() { await runCommand(["deno", "task", "verify:dist"]); } + // 3.5 Regenerate artifacts that embed the version. + // + // hydration-runtime.generated.ts is a prebundled artifact carrying its own + // `var VERSION = "..."`, so a regex bump cannot reach it. Left stale it + // disagrees with version-constant.ts, and `generate:manifests:check` fails + // the required typecheck shard -- blocking the publish this task exists to + // perform. Every release before this ran `deno task generate` by hand. + console.log("\nšŸ”§ Regenerating version-embedding artifacts..."); + await runCommand(["deno", "task", "generate"]); + // 4. Git commit, tag, and push (CI will handle npm publish + binary upload) console.log("\nšŸ“¦ Committing and tagging release..."); await runCommand(["git", "add", "."]);