|
| 1 | +// GitHub Releases for one publish run: `node scripts/github.mjs <dev|latest> <assets dir>`. The |
| 2 | +// release job of .github/workflows/publish.yml runs it after npm, so a release never names a |
| 3 | +// version npm does not have. canary is npm only. |
| 4 | +// |
| 5 | +// dev: `devel`, one prerelease replaced on every push, carrying the CLI binaries, so the installer |
| 6 | +// has a fixed URL for the newest development build. |
| 7 | +// latest: `<pkg>@v<version>` for each package not released yet; the CLI's carries the binaries |
| 8 | +// and is marked latest. The tag is the one `vp run bump` made, or a new one at this commit. |
| 9 | +import { spawnSync } from "node:child_process"; |
| 10 | +import { globSync } from "node:fs"; |
| 11 | +import { basename, dirname, resolve } from "node:path"; |
| 12 | + |
| 13 | +import { releaseId, releaseVersion } from "./version.mjs"; |
| 14 | +import { publishablePackages, repoRoot as root } from "./workspace.mjs"; |
| 15 | + |
| 16 | +const [channel, assetsDir] = process.argv.slice(2); |
| 17 | +if (!["dev", "latest"].includes(channel) || !assetsDir) { |
| 18 | + console.error("Usage: node scripts/github.mjs <dev|latest> <assets dir>"); |
| 19 | + process.exit(1); |
| 20 | +} |
| 21 | +const assets = globSync("*", { cwd: assetsDir }).map((file) => resolve(assetsDir, file)); |
| 22 | +const sha = process.env.GITHUB_SHA; |
| 23 | +if (!sha) throw new Error("GITHUB_SHA is not set; GitHub Releases are made in CI"); |
| 24 | + |
| 25 | +function gh(...args) { |
| 26 | + const result = spawnSync("gh", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] }); |
| 27 | + if (result.error) throw result.error; |
| 28 | + return { ok: result.status === 0, stdout: result.stdout }; |
| 29 | +} |
| 30 | + |
| 31 | +function ghOrThrow(...args) { |
| 32 | + const result = gh(...args); |
| 33 | + if (!result.ok) throw new Error(`gh ${args.join(" ")} failed`); |
| 34 | + return result.stdout; |
| 35 | +} |
| 36 | + |
| 37 | +/** `@codeworksh/cli` at `0.3.0` → `cli@v0.3.0`. */ |
| 38 | +const tagName = (name, version) => `${name.replace("@codeworksh/", "")}@v${version}`; |
| 39 | + |
| 40 | +/** The newest stable tag of the package below `version`, so its notes cover only its own changes. */ |
| 41 | +function previousTag(name, version) { |
| 42 | + const prefix = tagName(name, ""); |
| 43 | + const compare = (a, b) => a.localeCompare(b, "en", { numeric: true }); |
| 44 | + const refs = JSON.parse(ghOrThrow("api", `repos/{owner}/{repo}/git/matching-refs/tags/${prefix}`)); |
| 45 | + const previous = refs |
| 46 | + .map(({ ref }) => ref.slice(`refs/tags/${prefix}`.length)) |
| 47 | + .filter((v) => /^\d+\.\d+\.\d+$/.test(v) && compare(v, version) < 0) |
| 48 | + .sort(compare) |
| 49 | + .at(-1); |
| 50 | + return previous && `${prefix}${previous}`; |
| 51 | +} |
| 52 | + |
| 53 | +const cli = publishablePackages().find(({ manifest }) => manifest.name === "@codeworksh/cli").manifest; |
| 54 | + |
| 55 | +if (channel === "dev") { |
| 56 | + const version = releaseVersion(cli.version, channel, releaseId(channel)); |
| 57 | + gh("release", "delete", "devel", "--cleanup-tag", "--yes"); |
| 58 | + ghOrThrow( |
| 59 | + "release", |
| 60 | + "create", |
| 61 | + "devel", |
| 62 | + ...assets, |
| 63 | + "--prerelease", |
| 64 | + "--target", |
| 65 | + sha, |
| 66 | + "--title", |
| 67 | + `cli@v${version}`, |
| 68 | + "--notes", |
| 69 | + `Development build ${version}, from ${sha}.`, |
| 70 | + ); |
| 71 | +} else { |
| 72 | + // The platform packages under packages/tui/npm ship with the launcher, which is the one release. |
| 73 | + const packages = publishablePackages() |
| 74 | + .filter(({ dir }) => basename(dirname(dir)) === "packages") |
| 75 | + .map(({ manifest }) => manifest) |
| 76 | + // The CLI goes last so it is the newest release, the one `releases/latest` points at. |
| 77 | + .sort((a, b) => (a.name === cli.name) - (b.name === cli.name)); |
| 78 | + for (const { name, version } of packages) { |
| 79 | + const tag = tagName(name, version); |
| 80 | + if (gh("release", "view", tag).ok) { |
| 81 | + console.error(`${tag} is already released`); |
| 82 | + continue; |
| 83 | + } |
| 84 | + const isCli = name === cli.name; |
| 85 | + const start = previousTag(name, version); |
| 86 | + ghOrThrow( |
| 87 | + "release", |
| 88 | + "create", |
| 89 | + tag, |
| 90 | + ...(isCli ? assets : []), |
| 91 | + isCli ? "--latest" : "--latest=false", |
| 92 | + "--target", |
| 93 | + sha, |
| 94 | + "--title", |
| 95 | + tag, |
| 96 | + "--generate-notes", |
| 97 | + ...(start ? ["--notes-start-tag", start] : []), |
| 98 | + ); |
| 99 | + console.error(`Released ${tag}`); |
| 100 | + } |
| 101 | +} |
0 commit comments