From b757d26d2c7b8ee412f05cba910ce9da7097feea Mon Sep 17 00:00:00 2001 From: Sanchit Rk <0xsanchit@gmail.com> Date: Sun, 18 Oct 2026 13:14:41 +0530 Subject: [PATCH] chore: name release tags @v Bump tags become `@v`, and latest makes a GitHub Release per package released, the CLI's carrying the binaries and marked latest. dev keeps the rolling `devel` prerelease; canary stays npm only. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 20 ++---- CLAUDE.md | 4 +- packages/aikit/bump.config.ts | 6 +- packages/codework/bump.config.ts | 4 +- packages/harness/bump.config.ts | 4 +- packages/plugin/bump.config.ts | 4 +- packages/tui/bump.config.ts | 4 +- scripts/bump.mjs | 2 +- scripts/github.mjs | 101 +++++++++++++++++++++++++++++++ sites/www/public/install | 2 +- 10 files changed, 122 insertions(+), 29 deletions(-) create mode 100644 scripts/github.mjs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index edb40224..373328a9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,8 +3,9 @@ name: Publish # One workflow publishes every channel, since npm trusted publishing names a single workflow file. # - canary / dev: every push to that branch # - latest: run by hand on `main`; publishes each package whose version is not on npm yet -# dev and latest also build the standalone binary and attach it to a GitHub Release: `devel`, one -# prerelease replaced on every dev push, or `cli-v`, marked latest. canary stays npm-only. +# dev and latest also make GitHub Releases (scripts/github.mjs): `devel`, one prerelease with the +# standalone binary replaced on every dev push, or `@v` per package released on +# latest, the CLI's with the binary and marked latest. canary stays npm-only. on: push: branches: [canary, dev] @@ -192,16 +193,5 @@ jobs: path: release merge-multiple: true # codework.sh/install verifies each download against SHA256SUMS. - - run: | - (cd release && sha256sum *.tar.gz > SHA256SUMS) - version="$(node scripts/version.mjs "$CHANNEL" packages/codework)" - if [ "$CHANNEL" = dev ]; then - gh release delete devel --cleanup-tag --yes || true - gh release create devel release/* --prerelease --target "$GITHUB_SHA" \ - --title "CLI $version" --notes "Development build $version, from $GITHUB_SHA." - elif gh release view "cli-v$version" > /dev/null 2>&1; then - echo "cli-v$version is already released" - else - gh release create "cli-v$version" release/* --latest --target "$GITHUB_SHA" \ - --title "CLI $version" --generate-notes - fi + - run: (cd release && sha256sum *.tar.gz > SHA256SUMS) + - run: node scripts/github.mjs "$CHANNEL" release diff --git a/CLAUDE.md b/CLAUDE.md index 3aef3cc8..e5aaf1fa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,7 +84,9 @@ Every package is published by CI from `.github/workflows/publish.yml`, which run - **dev**: every push to `dev`. Versions are `-dev.` (UTC). - **latest**: run the workflow by hand on `main`. It publishes each package whose manifest version is not on npm yet and skips the rest. -Versions are bumped locally with `vp run bump [version]`, which runs `bumpp` with the package's `bump.config.ts` and commits and tags `@codeworksh/@` without pushing. `vp run bump tui` moves the launcher and its three platform packages together. +GitHub Releases (`scripts/github.mjs`): dev replaces the `devel` prerelease with the CLI binaries; latest makes a `@v` release per package not released yet, the CLI's carrying the binaries. canary has none. + +Versions are bumped locally with `vp run bump [version]`, which runs `bumpp` with the package's `bump.config.ts` and commits and tags `@v` without pushing. `vp run bump tui` moves the launcher and its three platform packages together. Flow: diff --git a/packages/aikit/bump.config.ts b/packages/aikit/bump.config.ts index 310a47bb..8ce9aeda 100644 --- a/packages/aikit/bump.config.ts +++ b/packages/aikit/bump.config.ts @@ -1,10 +1,10 @@ import { defineConfig } from "bumpp"; // Release config for @codeworksh/aikit, loaded automatically by `bumpp`. -// - Tags follow the repo's `@codeworksh/@` convention. +// - Tags follow the repo's `@v` convention. // - Pushing stays manual; publishing runs in CI (.github/workflows/publish.yml). export default defineConfig({ - tag: "@codeworksh/aikit@%s", - commit: "release: @codeworksh/aikit@%s", + tag: "aikit@v%s", + commit: "release: aikit@v%s", push: false, }); diff --git a/packages/codework/bump.config.ts b/packages/codework/bump.config.ts index f40fd394..1658c8de 100644 --- a/packages/codework/bump.config.ts +++ b/packages/codework/bump.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "bumpp"; // Release config for @codeworksh/codework, loaded automatically by `bumpp`. // Pushing and publishing stay manual so prerelease validation can happen first. export default defineConfig({ - tag: "@codeworksh/cli@%s", - commit: "release: @codeworksh/cli@%s", + tag: "cli@v%s", + commit: "release: cli@v%s", push: false, }); diff --git a/packages/harness/bump.config.ts b/packages/harness/bump.config.ts index 5ea9e1eb..7f93da3c 100644 --- a/packages/harness/bump.config.ts +++ b/packages/harness/bump.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "bumpp"; // Release config for @codeworksh/harness, loaded automatically by `bumpp`. // Pushing and publishing stay manual so prerelease validation can happen first. export default defineConfig({ - tag: "@codeworksh/harness@%s", - commit: "release: @codeworksh/harness@%s", + tag: "harness@v%s", + commit: "release: harness@v%s", push: false, }); diff --git a/packages/plugin/bump.config.ts b/packages/plugin/bump.config.ts index 02ae0b49..a91ace2b 100644 --- a/packages/plugin/bump.config.ts +++ b/packages/plugin/bump.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "bumpp"; // Release config for @codeworksh/plugin, loaded automatically by `bumpp`. // Pushing and publishing stay manual so prerelease validation can happen first. export default defineConfig({ - tag: "@codeworksh/plugin@%s", - commit: "release: @codeworksh/plugin@%s", + tag: "plugin@v%s", + commit: "release: plugin@v%s", push: false, }); diff --git a/packages/tui/bump.config.ts b/packages/tui/bump.config.ts index 97595af4..a4a7afaa 100644 --- a/packages/tui/bump.config.ts +++ b/packages/tui/bump.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from "bumpp"; // packages ship in lockstep with the launcher, so all four versions move together. export default defineConfig({ files: ["package.json", "npm/*/package.json"], - tag: "@codeworksh/tui@%s", - commit: "release: @codeworksh/tui@%s", + tag: "tui@v%s", + commit: "release: tui@v%s", push: false, }); diff --git a/scripts/bump.mjs b/scripts/bump.mjs index 324fc835..3f30b4ff 100644 --- a/scripts/bump.mjs +++ b/scripts/bump.mjs @@ -1,5 +1,5 @@ // `vp run bump [bumpp args]`: runs bumpp in the package, which reads its bump.config.ts -// to commit and tag `@codeworksh/@` without pushing. +// to commit and tag `@v` without pushing. import { spawnSync } from "node:child_process"; import { findPackage } from "./workspace.mjs"; diff --git a/scripts/github.mjs b/scripts/github.mjs new file mode 100644 index 00000000..c5e3617d --- /dev/null +++ b/scripts/github.mjs @@ -0,0 +1,101 @@ +// GitHub Releases for one publish run: `node scripts/github.mjs `. The +// release job of .github/workflows/publish.yml runs it after npm, so a release never names a +// version npm does not have. canary is npm only. +// +// dev: `devel`, one prerelease replaced on every push, carrying the CLI binaries, so the installer +// has a fixed URL for the newest development build. +// latest: `@v` for each package not released yet; the CLI's carries the binaries +// and is marked latest. The tag is the one `vp run bump` made, or a new one at this commit. +import { spawnSync } from "node:child_process"; +import { globSync } from "node:fs"; +import { basename, dirname, resolve } from "node:path"; + +import { releaseId, releaseVersion } from "./version.mjs"; +import { publishablePackages, repoRoot as root } from "./workspace.mjs"; + +const [channel, assetsDir] = process.argv.slice(2); +if (!["dev", "latest"].includes(channel) || !assetsDir) { + console.error("Usage: node scripts/github.mjs "); + process.exit(1); +} +const assets = globSync("*", { cwd: assetsDir }).map((file) => resolve(assetsDir, file)); +const sha = process.env.GITHUB_SHA; +if (!sha) throw new Error("GITHUB_SHA is not set; GitHub Releases are made in CI"); + +function gh(...args) { + const result = spawnSync("gh", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] }); + if (result.error) throw result.error; + return { ok: result.status === 0, stdout: result.stdout }; +} + +function ghOrThrow(...args) { + const result = gh(...args); + if (!result.ok) throw new Error(`gh ${args.join(" ")} failed`); + return result.stdout; +} + +/** `@codeworksh/cli` at `0.3.0` → `cli@v0.3.0`. */ +const tagName = (name, version) => `${name.replace("@codeworksh/", "")}@v${version}`; + +/** The newest stable tag of the package below `version`, so its notes cover only its own changes. */ +function previousTag(name, version) { + const prefix = tagName(name, ""); + const compare = (a, b) => a.localeCompare(b, "en", { numeric: true }); + const refs = JSON.parse(ghOrThrow("api", `repos/{owner}/{repo}/git/matching-refs/tags/${prefix}`)); + const previous = refs + .map(({ ref }) => ref.slice(`refs/tags/${prefix}`.length)) + .filter((v) => /^\d+\.\d+\.\d+$/.test(v) && compare(v, version) < 0) + .sort(compare) + .at(-1); + return previous && `${prefix}${previous}`; +} + +const cli = publishablePackages().find(({ manifest }) => manifest.name === "@codeworksh/cli").manifest; + +if (channel === "dev") { + const version = releaseVersion(cli.version, channel, releaseId(channel)); + gh("release", "delete", "devel", "--cleanup-tag", "--yes"); + ghOrThrow( + "release", + "create", + "devel", + ...assets, + "--prerelease", + "--target", + sha, + "--title", + `cli@v${version}`, + "--notes", + `Development build ${version}, from ${sha}.`, + ); +} else { + // The platform packages under packages/tui/npm ship with the launcher, which is the one release. + const packages = publishablePackages() + .filter(({ dir }) => basename(dirname(dir)) === "packages") + .map(({ manifest }) => manifest) + // The CLI goes last so it is the newest release, the one `releases/latest` points at. + .sort((a, b) => (a.name === cli.name) - (b.name === cli.name)); + for (const { name, version } of packages) { + const tag = tagName(name, version); + if (gh("release", "view", tag).ok) { + console.error(`${tag} is already released`); + continue; + } + const isCli = name === cli.name; + const start = previousTag(name, version); + ghOrThrow( + "release", + "create", + tag, + ...(isCli ? assets : []), + isCli ? "--latest" : "--latest=false", + "--target", + sha, + "--title", + tag, + "--generate-notes", + ...(start ? ["--notes-start-tag", start] : []), + ); + console.error(`Released ${tag}`); + } +} diff --git a/sites/www/public/install b/sites/www/public/install index 82a25731..dc37b672 100755 --- a/sites/www/public/install +++ b/sites/www/public/install @@ -35,7 +35,7 @@ while [ $# -gt 0 ]; do --dev) release="download/devel" ;; --version) [ $# -ge 2 ] || fail "--version needs a version, such as 0.3.0" - release="download/cli-v${2#v}" + release="download/cli@v${2#v}" shift ;; --no-modify-path) modify_path=0 ;;