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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
node-version: 24
cache: npm
- run: npm ci
- run: npm run check:version-sync
- run: npm run lint
- run: npm run typecheck
- run: npm run test
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
cache: npm
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm run check:version-sync
- run: npm run check
- run: npm pack --dry-run
- run: npm publish
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,18 @@
"scripts": {
"lint": "eslint .",
"typecheck": "tsc -p tsconfig.json",
"check": "npm run lint && npm run typecheck && npm run test && npm run test:integration && npm run test:e2e",
"check:version-sync": "node scripts/check-version-sync.mjs",
"sync:plugin-version": "node scripts/sync-plugin-version.mjs",
"check": "npm run check:version-sync && npm run lint && npm run typecheck && npm run test && npm run test:integration && npm run test:e2e",
"install:codex": "node scripts/installer-cli.mjs install",
"prepack": "npm run check:version-sync",
"setup:git-hooks": "node scripts/setup-git-hooks.mjs",
"test": "node --test tests/*.test.mjs",
"test:integration": "node --test tests/integration/*.test.mjs",
"test:e2e": "node --test tests/e2e/*.test.mjs",
"uninstall:codex": "node scripts/installer-cli.mjs uninstall",
"update:codex": "node scripts/installer-cli.mjs update"
"update:codex": "node scripts/installer-cli.mjs update",
"version": "npm run sync:plugin-version && npm run check:version-sync"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stage plugin manifest in npm version lifecycle

The new version lifecycle script updates .codex-plugin/plugin.json but never stages it, so npm version will create a commit/tag that bumps package.json while leaving the plugin manifest change unstaged. npm's own command docs (.../npm/docs/content/commands/npm-version.md, lifecycle steps 2/4) state files changed by lifecycle scripts must be explicitly added with git add; without that, a normal npm version patch release flow can produce a tagged commit that immediately fails check:version-sync in CI/publish.

Useful? React with 👍 / 👎.

},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand Down
14 changes: 14 additions & 0 deletions scripts/check-version-sync.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env node

import { assertVersionsMatch } from "./lib/version-sync.mjs";

try {
const version = assertVersionsMatch();
process.stdout.write(
`Version sync OK: package.json and .codex-plugin/plugin.json are both ${version}.\n`
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n`);
process.exitCode = 1;
}
69 changes: 69 additions & 0 deletions scripts/lib/version-sync.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const ROOT_DIR = path.resolve(fileURLToPath(new URL("../..", import.meta.url)));

export function resolveVersionSyncPaths(rootDir = ROOT_DIR) {
return {
rootDir,
packageJsonPath: path.join(rootDir, "package.json"),
pluginJsonPath: path.join(rootDir, ".codex-plugin", "plugin.json"),
};
}

function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}

function writeJson(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}

export function readVersionPair(rootDir = ROOT_DIR) {
const { packageJsonPath, pluginJsonPath } = resolveVersionSyncPaths(rootDir);
const packageJson = readJson(packageJsonPath);
const pluginJson = readJson(pluginJsonPath);
return {
packageJson,
pluginJson,
packageVersion: String(packageJson.version ?? ""),
pluginVersion: String(pluginJson.version ?? ""),
};
}

export function assertVersionsMatch(rootDir = ROOT_DIR) {
const { packageVersion, pluginVersion } = readVersionPair(rootDir);
if (!packageVersion || !pluginVersion) {
throw new Error(
"Both package.json and .codex-plugin/plugin.json must declare a version."
);
}
if (packageVersion !== pluginVersion) {
throw new Error(
`Version mismatch: package.json is ${packageVersion} but .codex-plugin/plugin.json is ${pluginVersion}.`
);
}
return packageVersion;
}

export function syncPluginVersionFromPackage(rootDir = ROOT_DIR) {
const { pluginJsonPath } = resolveVersionSyncPaths(rootDir);
const { packageJson, pluginJson, packageVersion, pluginVersion } =
readVersionPair(rootDir);

if (!packageVersion) {
throw new Error("package.json is missing a version.");
}

if (packageVersion === pluginVersion) {
return { changed: false, version: packageVersion };
}

const nextPluginJson = {
...pluginJson,
version: packageJson.version,
};
writeJson(pluginJsonPath, nextPluginJson);
return { changed: true, version: packageVersion };
}
16 changes: 16 additions & 0 deletions scripts/sync-plugin-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env node

import { syncPluginVersionFromPackage } from "./lib/version-sync.mjs";

try {
const result = syncPluginVersionFromPackage();
process.stdout.write(
result.changed
? `Updated .codex-plugin/plugin.json to version ${result.version}.\n`
: `.codex-plugin/plugin.json already matches package.json at ${result.version}.\n`
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n`);
process.exitCode = 1;
}
79 changes: 79 additions & 0 deletions tests/version-sync.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import {
assertVersionsMatch,
readVersionPair,
syncPluginVersionFromPackage,
} from "../scripts/lib/version-sync.mjs";

function writeJson(filePath, value) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}

function createTempRepo({ packageVersion, pluginVersion }) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cc-version-sync-"));
writeJson(path.join(dir, "package.json"), {
name: "cc-plugin-codex",
version: packageVersion,
});
writeJson(path.join(dir, ".codex-plugin", "plugin.json"), {
name: "cc",
version: pluginVersion,
});
return dir;
}

describe("version sync", () => {
it("asserts the live repo versions match", () => {
assert.equal(assertVersionsMatch(), "1.0.0");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove hardcoded repo version from sync test

This test locks the expected live version to 1.0.0, so any legitimate future version bump (for example to 1.0.1) will fail even when package.json and .codex-plugin/plugin.json are correctly synchronized. Because npm run test is part of the required checks, every release version change will require touching this test or CI will break.

Useful? React with 👍 / 👎.

});

it("detects mismatched versions", () => {
const dir = createTempRepo({
packageVersion: "1.2.3",
pluginVersion: "1.2.2",
});
try {
assert.throws(
() => assertVersionsMatch(dir),
/Version mismatch: package\.json is 1\.2\.3 but \.codex-plugin\/plugin\.json is 1\.2\.2\./
);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});

it("syncs plugin.json from package.json", () => {
const dir = createTempRepo({
packageVersion: "2.0.0",
pluginVersion: "1.0.0",
});
try {
const result = syncPluginVersionFromPackage(dir);
assert.deepEqual(result, { changed: true, version: "2.0.0" });
const { packageVersion, pluginVersion } = readVersionPair(dir);
assert.equal(packageVersion, "2.0.0");
assert.equal(pluginVersion, "2.0.0");
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});

it("does nothing when versions already match", () => {
const dir = createTempRepo({
packageVersion: "3.1.4",
pluginVersion: "3.1.4",
});
try {
const result = syncPluginVersionFromPackage(dir);
assert.deepEqual(result, { changed: false, version: "3.1.4" });
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
});