diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index 2b746c9..c96a7b4 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -44,7 +44,8 @@ jobs: echo "" echo "> [!WARNING]" echo "> The manifests still declare \`$VERSION\`, which is already released." - echo "> Bump every manifest before publishing this draft (release spec §2)." + echo "> Run \`deno task bump \` and merge the bump before publishing" + echo "> this draft (release spec §2)." echo "" echo cat /tmp/clean.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73b42ed..71e824c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,8 +36,8 @@ jobs: { echo "> [!CAUTION]" echo "> This release did not build: the manifests do not declare \`${TAG#v}\`." - echo "> Delete this release and its tag, bump every manifest, and release again" - echo "> (release spec §2)." + echo "> Delete this release and its tag, run \`deno task bump ${TAG#v}\`, merge" + echo "> the bump, and release again (release spec §2)." echo cat /tmp/body.md } > /tmp/new.md diff --git a/.reviews/components/ReleaseSpecWarning.md b/.reviews/components/ReleaseSpecWarning.md index 13a14d3..5cdb8a5 100644 --- a/.reviews/components/ReleaseSpecWarning.md +++ b/.reviews/components/ReleaseSpecWarning.md @@ -19,6 +19,7 @@ list when a file is added or removed. - .github/release-drafter.yml - scripts/gen-publish-workflow.md - scripts/build-npm.ts +- scripts/bump-version.ts diff --git a/deno.json b/deno.json index 6b457c8..e291aa5 100644 --- a/deno.json +++ b/deno.json @@ -36,6 +36,7 @@ "xmd": "deno run --allow-all cli/src/cli.ts", "build": "deno compile --node-modules-dir=none --exclude-unused-npm --allow-all --include packages/code-review-agent --output dist/xmd cli/src/cli.ts", "gen:publish-workflow": "deno run --allow-all cli/src/cli.ts run scripts/gen-publish-workflow.md", + "bump": "deno run -A scripts/bump-version.ts", "test": "deno test --allow-all core/tests/ durable-streams/tests/ packages/code-review-agent/tests/", "check": "deno check core/mod.ts", "lint": "npx oxlint -c .oxlintrc.json core/src/ cli/src/ durable-streams/ runtime/ && npx oxfmt --check core/src/ cli/src/ durable-streams/ runtime/ test-support/", diff --git a/scripts/bump-version.ts b/scripts/bump-version.ts new file mode 100644 index 0000000..c2e4373 --- /dev/null +++ b/scripts/bump-version.ts @@ -0,0 +1,64 @@ +/** + * Bump every @executablemd workspace manifest to a new version. + * + * Usage: + * deno task bump + * + * Stamps the `version` field of each member's deno.json and package.json — + * the manifests are the single version source (release spec §2). Commit, + * merge, then publish the draft release as v. + */ + +import { exit, main } from "effection"; +import { readTextFile, writeTextFile } from "@effectionx/fs"; +import { z } from "npm:zod@^4"; + +const SCOPE = "@executablemd/"; + +const RootSchema = z.object({ workspace: z.array(z.string()) }); +const NamedSchema = z.object({ name: z.string() }); + +await main(function* (args) { + const raw = args[0]; + if (!raw) { + console.error("usage: deno task bump "); + yield* exit(1); + return; + } + const version = raw.replace(/^v/, ""); + if (!/^\d+\.\d+\.\d+([-.][0-9A-Za-z.-]+)?$/.test(version)) { + console.error(`"${raw}" is not a semver version`); + yield* exit(1); + return; + } + + const repoRoot = new URL("../", import.meta.url); + const root = RootSchema.parse(JSON.parse(yield* readTextFile(new URL("deno.json", repoRoot)))); + + for (const dir of root.workspace) { + let denoText: string; + try { + denoText = yield* readTextFile(new URL(`${dir}/deno.json`, repoRoot)); + } catch { + continue; + } + const named = NamedSchema.safeParse(JSON.parse(denoText)); + if (!named.success || !named.data.name.startsWith(SCOPE)) { + continue; + } + for (const manifest of ["deno.json", "package.json"]) { + const url = new URL(`${dir}/${manifest}`, repoRoot); + const text = yield* readTextFile(url); + const updated = text.replace(/"version": "[^"]+"/, `"version": "${version}"`); + if (updated === text) { + console.error(`no version field found in ${dir}/${manifest}`); + yield* exit(1); + return; + } + yield* writeTextFile(url, updated); + console.log(`bumped ${dir}/${manifest} -> ${version}`); + } + } + + console.log(`done — commit, merge, then publish the draft release as v${version}`); +}); diff --git a/specs/release-process-spec.md b/specs/release-process-spec.md index e049c9c..2746673 100644 --- a/specs/release-process-spec.md +++ b/specs/release-process-spec.md @@ -55,8 +55,9 @@ are the single source. The npm version derives from the tag, and both workflows refuse a tag the manifests do not declare, so the two cannot diverge. -To cut a release: bump the version in every manifest, merge to `main`, then -create the `vX.Y.Z` tag. +To cut a release: run `deno task bump ` (stamps every manifest), +merge to `main`, then publish the draft release — its tag follows the +manifests (§3). ## 3. Workflows