Compare commits

..
1 Commits
Author SHA1 Message Date
DietrichGebertandClaude Opus 4.8 763e04deee 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>
2026-06-23 18:28:53 +02:00
8 changed files with 86 additions and 6 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ponytail", "name": "ponytail",
"version": "4.7.0", "version": "4.8.1",
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.", "description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
"author": { "author": {
"name": "Dietrich Gebert", "name": "Dietrich Gebert",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ponytail", "name": "ponytail",
"version": "4.7.0", "version": "4.8.1",
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.", "description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
"author": { "author": {
"name": "Dietrich Gebert", "name": "Dietrich Gebert",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "ponytail", "name": "ponytail",
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.", "description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
"version": "4.7.0", "version": "4.8.1",
"author": { "author": {
"name": "Dietrich Gebert", "name": "Dietrich Gebert",
"url": "https://github.com/DietrichGebert" "url": "https://github.com/DietrichGebert"
+4
View File
@@ -3,6 +3,7 @@ name: test
on: on:
push: push:
branches: [main] branches: [main]
tags: ['v*']
pull_request: pull_request:
jobs: jobs:
@@ -25,5 +26,8 @@ jobs:
- name: Check rule copies - name: Check rule copies
run: node scripts/check-rule-copies.js run: node scripts/check-rule-copies.js
- name: Check version consistency
run: node scripts/check-versions.js
- name: Run tests - name: Run tests
run: npm test run: npm test
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ponytail", "name": "ponytail",
"version": "4.7.0", "version": "4.8.1",
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.", "description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
"contextFileName": "AGENTS.md" "contextFileName": "AGENTS.md"
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ponytail", "name": "ponytail",
"version": "0.1.0", "version": "4.8.1",
"description": "Lazy senior dev mode for AI agents. The best code is the code you never wrote.", "description": "Lazy senior dev mode for AI agents. The best code is the code you never wrote.",
"keywords": ["pi-package", "pi", "skills", "ponytail"], "keywords": ["pi-package", "pi", "skills", "ponytail"],
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ponytail-mcp", "name": "ponytail-mcp",
"version": "0.1.0", "version": "4.8.1",
"description": "MCP server that serves Ponytail's lazy-senior-dev instructions as a prompt and a tool.", "description": "MCP server that serves Ponytail's lazy-senior-dev instructions as a prompt and a tool.",
"private": true, "private": true,
"type": "module", "type": "module",
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env node
// Version-consistency guard. Ponytail declares its version in six files across
// four host ecosystems, and every release bumps all of them by hand.
//
// tests/gemini-extension.test.js already checks the four plugin manifests agree
// with each other, but that can't catch the failure mode that shipped in v4.8.0:
// every manifest stayed stale at 4.7.0 *together* while the release moved on, so
// they "agreed" and the test passed (#260, #262). It also ignores the two
// package.json files. This check closes both gaps:
// 1. every version-bearing file must share one pinned X.Y.Z version, and
// 2. on a release-tag CI run, that shared version must equal the tag.
const fs = require('fs');
const path = require('path');
const root = path.join(__dirname, '..');
const PINNED_SEMVER = /^\d+\.\d+\.\d+$/;
// Every file that declares the project version, and who reads it. Add new host
// manifests here so a future ecosystem can't drift unnoticed.
const VERSION_FILES = [
'.claude-plugin/plugin.json', // Claude Code plugin — what users install
'.codex-plugin/plugin.json', // Codex plugin
'.github/plugin/plugin.json', // Copilot plugin
'gemini-extension.json', // Gemini CLI extension
'package.json', // pi-package / repo root
'ponytail-mcp/package.json', // MCP server (private, internal-only)
];
function readVersion(relPath) {
try {
// Strip a UTF-8 BOM some Windows editors prepend (breaks JSON.parse).
const raw = fs.readFileSync(path.join(root, relPath), 'utf8').replace(/^\uFEFF/, '');
return JSON.parse(raw).version;
} catch (e) {
throw new Error(`${relPath}: ${e.message}`);
}
}
let failed = false;
const versions = VERSION_FILES.map((relPath) => {
const version = readVersion(relPath);
if (typeof version !== 'string' || !PINNED_SEMVER.test(version)) {
console.error(`${relPath}: version must be a pinned X.Y.Z semver, got ${JSON.stringify(version)}`);
failed = true;
}
return [relPath, version];
});
// Every file must declare the same version.
const distinct = [...new Set(versions.map(([, v]) => v))];
if (distinct.length > 1) {
console.error('Version mismatch — every manifest must share one version:');
for (const [relPath, version] of versions) console.error(` ${version}\t${relPath}`);
failed = true;
}
const shared = distinct.length === 1 ? distinct[0] : null;
// On a release-tag push CI sets GITHUB_REF_TYPE=tag and GITHUB_REF_NAME=vX.Y.Z.
// The shared version must equal the tag — this catches tagging a release whose
// version files were never bumped, which mutual agreement alone cannot.
if (shared && process.env.GITHUB_REF_TYPE === 'tag') {
const tag = process.env.GITHUB_REF_NAME || '';
const tagVersion = tag.replace(/^v/, '');
if (PINNED_SEMVER.test(tagVersion) && tagVersion !== shared) {
console.error(`release tag ${tag} does not match version ${shared}; bump the version files before tagging`);
failed = true;
}
}
if (failed) {
console.error('Align the version fields (see issue #260) so every manifest shares one version.');
process.exit(1);
}
console.log(`All ${VERSION_FILES.length} version files pinned at ${shared}.`);