Skip to content

Commit 763e04d

Browse files
fix: align all version manifests to 4.8.1 + guard against drift (#260, #262) (#270)
* fix: align all version manifests to 4.8.0 + guard against drift (#260, #262) The v4.8.0 release shipped with all four plugin manifests still reading 4.7.0, and both package.json files still at the 0.1.0 npm-init default. So Claude/Codex/Gemini reported 4.7.0 as the latest version (#262) and the project advertised three different versions at once (#260). Bump all six version-bearing files to 4.8.0 so they match the release tag: the four plugin manifests, the root package.json, and ponytail-mcp. Add scripts/check-versions.js, wired into CI, so this cannot recur. It asserts every version file shares one pinned X.Y.Z version, and on a release-tag run that the shared version equals the tag. The existing mutual-agreement check in tests/gemini-extension.test.js could not catch this, because all four manifests were stale at 4.7.0 together. Fixes #260 Refs #262 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: target 4.8.1 for a clean superseding release v4.8.0 was already tagged with the stale 4.7.0 manifests. Rather than rewrite a published tag, ship the consistent versions as v4.8.1. The CI guard enforces tag == version on the release run. (#260, #262) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dedc97c commit 763e04d

8 files changed

Lines changed: 86 additions & 6 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ponytail",
3-
"version": "4.7.0",
3+
"version": "4.8.1",
44
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
55
"author": {
66
"name": "Dietrich Gebert",

.codex-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ponytail",
3-
"version": "4.7.0",
3+
"version": "4.8.1",
44
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
55
"author": {
66
"name": "Dietrich Gebert",

.github/plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ponytail",
33
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
4-
"version": "4.7.0",
4+
"version": "4.8.1",
55
"author": {
66
"name": "Dietrich Gebert",
77
"url": "https://github.com/DietrichGebert"

.github/workflows/test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name: test
33
on:
44
push:
55
branches: [main]
6+
tags: ['v*']
67
pull_request:
78

89
jobs:
@@ -25,5 +26,8 @@ jobs:
2526
- name: Check rule copies
2627
run: node scripts/check-rule-copies.js
2728

29+
- name: Check version consistency
30+
run: node scripts/check-versions.js
31+
2832
- name: Run tests
2933
run: npm test

gemini-extension.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ponytail",
3-
"version": "4.7.0",
3+
"version": "4.8.1",
44
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
55
"contextFileName": "AGENTS.md"
66
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ponytail",
3-
"version": "0.1.0",
3+
"version": "4.8.1",
44
"description": "Lazy senior dev mode for AI agents. The best code is the code you never wrote.",
55
"keywords": ["pi-package", "pi", "skills", "ponytail"],
66
"license": "MIT",

ponytail-mcp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ponytail-mcp",
3-
"version": "0.1.0",
3+
"version": "4.8.1",
44
"description": "MCP server that serves Ponytail's lazy-senior-dev instructions as a prompt and a tool.",
55
"private": true,
66
"type": "module",

scripts/check-versions.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env node
2+
// Version-consistency guard. Ponytail declares its version in six files across
3+
// four host ecosystems, and every release bumps all of them by hand.
4+
//
5+
// tests/gemini-extension.test.js already checks the four plugin manifests agree
6+
// with each other, but that can't catch the failure mode that shipped in v4.8.0:
7+
// every manifest stayed stale at 4.7.0 *together* while the release moved on, so
8+
// they "agreed" and the test passed (#260, #262). It also ignores the two
9+
// package.json files. This check closes both gaps:
10+
// 1. every version-bearing file must share one pinned X.Y.Z version, and
11+
// 2. on a release-tag CI run, that shared version must equal the tag.
12+
13+
const fs = require('fs');
14+
const path = require('path');
15+
16+
const root = path.join(__dirname, '..');
17+
const PINNED_SEMVER = /^\d+\.\d+\.\d+$/;
18+
19+
// Every file that declares the project version, and who reads it. Add new host
20+
// manifests here so a future ecosystem can't drift unnoticed.
21+
const VERSION_FILES = [
22+
'.claude-plugin/plugin.json', // Claude Code plugin — what users install
23+
'.codex-plugin/plugin.json', // Codex plugin
24+
'.github/plugin/plugin.json', // Copilot plugin
25+
'gemini-extension.json', // Gemini CLI extension
26+
'package.json', // pi-package / repo root
27+
'ponytail-mcp/package.json', // MCP server (private, internal-only)
28+
];
29+
30+
function readVersion(relPath) {
31+
try {
32+
// Strip a UTF-8 BOM some Windows editors prepend (breaks JSON.parse).
33+
const raw = fs.readFileSync(path.join(root, relPath), 'utf8').replace(/^\uFEFF/, '');
34+
return JSON.parse(raw).version;
35+
} catch (e) {
36+
throw new Error(`${relPath}: ${e.message}`);
37+
}
38+
}
39+
40+
let failed = false;
41+
const versions = VERSION_FILES.map((relPath) => {
42+
const version = readVersion(relPath);
43+
if (typeof version !== 'string' || !PINNED_SEMVER.test(version)) {
44+
console.error(`${relPath}: version must be a pinned X.Y.Z semver, got ${JSON.stringify(version)}`);
45+
failed = true;
46+
}
47+
return [relPath, version];
48+
});
49+
50+
// Every file must declare the same version.
51+
const distinct = [...new Set(versions.map(([, v]) => v))];
52+
if (distinct.length > 1) {
53+
console.error('Version mismatch — every manifest must share one version:');
54+
for (const [relPath, version] of versions) console.error(` ${version}\t${relPath}`);
55+
failed = true;
56+
}
57+
const shared = distinct.length === 1 ? distinct[0] : null;
58+
59+
// On a release-tag push CI sets GITHUB_REF_TYPE=tag and GITHUB_REF_NAME=vX.Y.Z.
60+
// The shared version must equal the tag — this catches tagging a release whose
61+
// version files were never bumped, which mutual agreement alone cannot.
62+
if (shared && process.env.GITHUB_REF_TYPE === 'tag') {
63+
const tag = process.env.GITHUB_REF_NAME || '';
64+
const tagVersion = tag.replace(/^v/, '');
65+
if (PINNED_SEMVER.test(tagVersion) && tagVersion !== shared) {
66+
console.error(`release tag ${tag} does not match version ${shared}; bump the version files before tagging`);
67+
failed = true;
68+
}
69+
}
70+
71+
if (failed) {
72+
console.error('Align the version fields (see issue #260) so every manifest shares one version.');
73+
process.exit(1);
74+
}
75+
76+
console.log(`All ${VERSION_FILES.length} version files pinned at ${shared}.`);

0 commit comments

Comments
 (0)