Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 }}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
19 changes: 10 additions & 9 deletions scripts/publish.ts
Original file line number Diff line number Diff line change
@@ -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"]);
70 changes: 43 additions & 27 deletions scripts/release.ts
Original file line number Diff line number Diff line change
@@ -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`);
}
9 changes: 9 additions & 0 deletions scripts/run.ts
Original file line number Diff line number Diff line change
@@ -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)}`);
}
10 changes: 9 additions & 1 deletion src/update-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@
"allowUnreachableCode": false,
"allowUnusedLabels": false
},
"include": ["src", "tests", "*.config.ts", "scripts/check-package.ts"]
"include": ["src", "tests", "scripts", "*.config.ts"]
}
Loading