From 71f1469778fb3a40d395cfca5bfaee43324a9bd9 Mon Sep 17 00:00:00 2001 From: Donald Silveira Date: Wed, 21 Oct 2026 13:20:18 -0300 Subject: [PATCH] Typecheck the release scripts and release from tags release.ts and publish.ts used Bun-only APIs, so they were excluded from typecheck and had never run since being rewritten. They are now plain Node TypeScript, run with node, and included in tsconfig, oxlint and knip. A preview run of the workflow found releases broken regardless: the script pushed the version bump to main using GH_PAT, which has expired, and main's ruleset only lets admins bypass its PR requirement. Releases now come from tags instead: the release job computes the next version from the latest v* tag and creates the tag and GitHub release through the API, and the publish job stamps that version into package.json before building. Nothing is pushed to main, so the workflow's GITHUB_TOKEN is enough and GH_PAT is no longer used. package.json keeps a 0.0.0-development placeholder, which also skips the update notice. Preview mode runs every check plus an npm publish dry run of the next version. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 12 +++--- package.json | 2 +- scripts/publish.ts | 19 +++++----- scripts/release.ts | 70 +++++++++++++++++++++-------------- scripts/run.ts | 9 +++++ src/update-check.ts | 10 ++++- tsconfig.json | 2 +- 7 files changed, 80 insertions(+), 44 deletions(-) create mode 100644 scripts/run.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3818d08..6f3b657 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,7 +13,7 @@ on: - minor - major preview: - description: "Preview mode (skip release and publish)" + description: "Preview: run every check and an npm publish dry run, create nothing" required: false type: boolean default: false @@ -27,20 +27,20 @@ jobs: version: ${{ steps.release.outputs.version }} tag: ${{ steps.release.outputs.tag }} steps: + # Full history, so the latest version tag can be found. - uses: actions/checkout@v5 with: fetch-depth: 0 - token: ${{ secrets.GH_PAT }} - uses: ./.github/actions/setup - name: Release id: release - run: bun run scripts/release.ts + run: node scripts/release.ts env: BUMP: ${{ inputs.bump }} PREVIEW: ${{ inputs.preview }} - GH_TOKEN: ${{ secrets.GH_PAT }} + GH_TOKEN: ${{ github.token }} publish: needs: release @@ -57,4 +57,6 @@ jobs: - uses: ./.github/actions/setup - name: Publish to npm - run: bun run scripts/publish.ts + run: node scripts/publish.ts + env: + VERSION: ${{ needs.release.outputs.version }} diff --git a/package.json b/package.json index 35a30e3..ef9d686 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@donnes/syncode", - "version": "1.1.9", + "version": "0.0.0-development", "private": false, "description": "Sync AI coding agent instructions, skills and configs (Claude Code, Codex, OpenCode, Gemini CLI, Cursor, Copilot) across machines.", "keywords": [ diff --git a/scripts/publish.ts b/scripts/publish.ts index 80e85a1..a8e18fe 100644 --- a/scripts/publish.ts +++ b/scripts/publish.ts @@ -1,12 +1,13 @@ -#!/usr/bin/env bun -// Publishes to npm with trusted publishing (GitHub OIDC), so no npm token is -// stored in the repo. Requires npm >= 11.5.1 and the package's trusted -// publisher set to this repo's "Release & Publish" workflow on npmjs.com. +// Publishes the tagged version to npm with trusted publishing (GitHub OIDC), +// so no npm token is stored in the repo. Requires npm >= 11.5.1 and the +// package's trusted publisher set to this repo's publish.yml on npmjs.com. -import { $ } from "bun"; +import { run } from "./run.ts"; -await $`bun run build`; -await $`npm publish --access public --provenance`; +const version = process.env.VERSION; +if (!version) throw new Error("Set VERSION to the version being published."); -const pkg = await Bun.file("package.json").json(); -console.log(`Published ${pkg.name}@${pkg.version}`); +// The repo's package.json carries a placeholder; the tag is the version. +run("npm", ["version", version, "--no-git-tag-version"]); +run("bun", ["run", "build"]); +run("npm", ["publish", "--access", "public", "--provenance"]); diff --git a/scripts/release.ts b/scripts/release.ts index 3947c92..7e8f997 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -1,42 +1,58 @@ -#!/usr/bin/env bun -// Bumps the version, tags it and creates a GitHub release with generated -// notes. Run by the "Release & Publish" workflow; set PREVIEW=true to only -// print the next version. +// Tags the next version and creates its GitHub release with generated notes. +// Versions come from git tags, not commits: nothing is pushed to main (which +// requires reviewed PRs), so the workflow's own GITHUB_TOKEN is enough. The +// publish job stamps the version into package.json at build time. +// +// With PREVIEW=true it runs every check and an npm publish dry run of the +// next version, but creates nothing. -import { writeFile } from "node:fs/promises"; -import { $ } from "bun"; +import { spawnSync } from "node:child_process"; +import * as fs from "node:fs"; +import { run } from "./run.ts"; -const bump = process.env.BUMP; -if (bump !== "patch" && bump !== "minor" && bump !== "major") { - console.error("Set BUMP to patch, minor or major."); - process.exit(1); +type Bump = "patch" | "minor" | "major"; + +function isBump(value: string | undefined): value is Bump { + return value === "patch" || value === "minor" || value === "major"; +} + +function output(command: string, args: string[]): string { + const result = spawnSync(command, args, { encoding: "utf8" }); + if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} failed: ${result.stderr}`); + return result.stdout.trim(); } + +const bump = process.env.BUMP; +if (!isBump(bump)) throw new Error("Set BUMP to patch, minor or major."); const preview = process.env.PREVIEW === "true"; -const pkg = await Bun.file("package.json").json(); -const [major = 0, minor = 0, patch = 0] = String(pkg.version).split(".").map(Number); +const latest = output("git", ["describe", "--tags", "--abbrev=0", "--match", "v[0-9]*"]); +const match = /^v(\d+)\.(\d+)\.(\d+)$/.exec(latest); +if (!match) throw new Error(`Unexpected latest tag: ${latest}`); +const [major, minor, patch] = [Number(match[1]), Number(match[2]), Number(match[3])]; const version = { major: `${major + 1}.0.0`, minor: `${major}.${minor + 1}.0`, patch: `${major}.${minor}.${patch + 1}`, }[bump]; const tag = `v${version}`; -console.log(`Bumping ${pkg.version} -> ${version}`); - -await $`bun run check`; - -if (!preview) { - pkg.version = version; - await writeFile("package.json", `${JSON.stringify(pkg, null, "\t")}\n`); - await $`git config user.name "github-actions[bot]"`; - await $`git config user.email "github-actions[bot]@users.noreply.github.com"`; - await $`git add package.json`; - await $`git commit -m ${`release: ${tag}`}`; - await $`git tag ${tag}`; - await $`git push origin HEAD --tags --no-verify`; - await $`gh release create ${tag} --title ${tag} --generate-notes`; +console.log(`${preview ? "Preview: " : ""}${latest} -> ${tag}`); + +run("bun", ["run", "check"]); + +if (preview) { + const original = fs.readFileSync("package.json", "utf8"); + try { + run("npm", ["version", version, "--no-git-tag-version"]); + run("npm", ["publish", "--dry-run", "--access", "public"]); + } finally { + fs.writeFileSync("package.json", original); + } +} else { + const target = process.env.GITHUB_SHA ?? output("git", ["rev-parse", "HEAD"]); + run("gh", ["release", "create", tag, "--target", target, "--title", tag, "--generate-notes"]); } if (process.env.GITHUB_OUTPUT) { - await writeFile(process.env.GITHUB_OUTPUT, `version=${version}\ntag=${tag}\n`, { flag: "a" }); + fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\ntag=${tag}\n`); } diff --git a/scripts/run.ts b/scripts/run.ts new file mode 100644 index 0000000..386214f --- /dev/null +++ b/scripts/run.ts @@ -0,0 +1,9 @@ +import { spawnSync } from "node:child_process"; + +/** Runs a command with inherited output, failing the script if it fails. */ +export function run(command: string, args: string[]) { + console.log(`$ ${[command, ...args].join(" ")}`); + const result = spawnSync(command, args, { stdio: "inherit" }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error(`${command} exited with ${String(result.status)}`); +} diff --git a/src/update-check.ts b/src/update-check.ts index f209104..e77bc39 100644 --- a/src/update-check.ts +++ b/src/update-check.ts @@ -53,7 +53,15 @@ function isOutdated(current: string, latest: string): boolean { * request timeout, and stays quiet outside interactive terminals. */ export function checkForUpdates(packageName: string, current: string) { - if (!canPrompt() || process.env.CI || process.env.SYNCODE_NO_UPDATE_CHECK) return; + // Local builds carry package.json's 0.0.0-development placeholder. + if ( + current.includes("-") || + !canPrompt() || + process.env.CI || + process.env.SYNCODE_NO_UPDATE_CHECK + ) { + return; + } const cache = readCache(); if (cache && isOutdated(current, cache.latest)) { console.log( diff --git a/tsconfig.json b/tsconfig.json index 1dc9030..65210a3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,5 +22,5 @@ "allowUnreachableCode": false, "allowUnusedLabels": false }, - "include": ["src", "tests", "*.config.ts", "scripts/check-package.ts"] + "include": ["src", "tests", "scripts", "*.config.ts"] }