diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 18d9382..74e1637 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "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.", "author": { "name": "Dietrich Gebert", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index dad3120..f6b1b4c 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "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.", "author": { "name": "Dietrich Gebert", diff --git a/.github/plugin/plugin.json b/.github/plugin/plugin.json index c57f448..516b137 100644 --- a/.github/plugin/plugin.json +++ b/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ponytail", "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": { "name": "Dietrich Gebert", "url": "https://github.com/DietrichGebert" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c8fc566..4bb17c0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,7 @@ name: test on: push: branches: [main] + tags: ['v*'] pull_request: jobs: @@ -25,5 +26,8 @@ jobs: - name: Check rule copies run: node scripts/check-rule-copies.js + - name: Check version consistency + run: node scripts/check-versions.js + - name: Run tests run: npm test diff --git a/gemini-extension.json b/gemini-extension.json index a8c1393..0238ca0 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "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.", "contextFileName": "AGENTS.md" } diff --git a/package.json b/package.json index 74587f1..314d149 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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.", "keywords": ["pi-package", "pi", "skills", "ponytail"], "license": "MIT", diff --git a/ponytail-mcp/package.json b/ponytail-mcp/package.json index 96c1d82..7f98e12 100644 --- a/ponytail-mcp/package.json +++ b/ponytail-mcp/package.json @@ -1,6 +1,6 @@ { "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.", "private": true, "type": "module", diff --git a/scripts/check-versions.js b/scripts/check-versions.js new file mode 100644 index 0000000..e7006fb --- /dev/null +++ b/scripts/check-versions.js @@ -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}.`);