Compare commits

..
Author SHA1 Message Date
Emeriko 9a27039e32 fix: use --tags (not --tag) for clawhub skill publish 2026-06-23 23:04:34 +02:00
DietrichGebertandClaude Opus 4.8 88be9caee7 feat: add publish-openclaw-skills.js to push skills to ClawHub (#273)
ClawHub does not sync from GitHub. Each OpenClaw skill is pushed with the
clawhub CLI at its own version, so the published copies can drift from the
repo the same way the plugin manifests did (#260). This adds a one-pass
publisher that pushes every generated .openclaw/skills/ skill at the
package.json version, with --dry-run to preview, and documents it in the
README next to the build step.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 20:05:34 +02:00
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
10 changed files with 162 additions and 7 deletions
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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"
+4
View File
@@ -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
+1 -1
View File
@@ -232,7 +232,7 @@ node scripts/check-rule-copies.js
npm test
```
The OpenClaw skill package (`.openclaw/skills/`) is generated from `skills/`; rerun `node scripts/build-openclaw-skills.js` after changing a skill, the test suite fails if it is stale.
The OpenClaw skill package (`.openclaw/skills/`) is generated from `skills/`; rerun `node scripts/build-openclaw-skills.js` after changing a skill, the test suite fails if it is stale. To publish the skills to ClawHub, run `clawhub login` once, then `node scripts/publish-openclaw-skills.js` (it publishes all six at the `package.json` version; pass `--dry-run` to preview).
The correctness benchmark spawns Python for email and CSV checks; `python3` is tried before `python`. CSV checks need `pandas` installed locally.
+1 -1
View File
@@ -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"
}
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+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}.`);
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env node
// Publish the generated OpenClaw skills (.openclaw/skills/) to ClawHub.
//
// ClawHub does not sync from GitHub: each skill is pushed explicitly with the
// clawhub CLI and carries its own version. This publishes every generated skill
// in one pass, versioned from the repo's package.json so ClawHub tracks the repo
// instead of drifting (the same drift that hit the plugin manifests in #260).
//
// Prereqs:
// - `clawhub login` once (registry auth persists)
// - skills must be current: run `node scripts/build-openclaw-skills.js` first
// if you changed a skill (CI fails if the committed copies are stale)
//
// Usage:
// node scripts/publish-openclaw-skills.js # publish all as latest
// node scripts/publish-openclaw-skills.js --dry-run # preview, upload nothing
// (any extra args are passed through to `clawhub skill publish`)
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const root = path.join(__dirname, '..');
const skillsDir = path.join(root, '.openclaw', 'skills');
const version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version;
// Every generated skill dir with a SKILL.md is publishable. Reading the dir
// (instead of a hardcoded list) covers whatever build-openclaw-skills emits,
// with nothing to keep in sync.
const slugs = fs.readdirSync(skillsDir, { withFileTypes: true })
.filter((e) => e.isDirectory() && fs.existsSync(path.join(skillsDir, e.name, 'SKILL.md')))
.map((e) => e.name)
.sort();
if (slugs.length === 0) {
console.error(`No skills under ${path.relative(root, skillsDir)}; run build-openclaw-skills.js first.`);
process.exit(1);
}
// "ponytail-review" -> "Ponytail Review"
const displayName = (slug) =>
slug.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
// Minimal quoting that satisfies both POSIX sh and cmd.exe: only display names
// (which contain a space) need wrapping; slugs, versions, paths, and flags don't.
const quote = (a) => (/[^\w./-]/.test(a) ? `"${a}"` : a);
const passthrough = process.argv.slice(2);
const extra = passthrough.length ? ` (${passthrough.join(' ')})` : '';
console.log(`Publishing ${slugs.length} skills to ClawHub at version ${version}${extra}:`);
for (const slug of slugs) {
const args = [
'clawhub', 'skill', 'publish', `.openclaw/skills/${slug}`,
'--slug', slug,
'--name', displayName(slug),
'--version', version,
'--tags', 'latest',
...passthrough,
];
const cmdline = args.map(quote).join(' ');
console.log(`\n$ ${cmdline}`);
const res = spawnSync(cmdline, { stdio: 'inherit', cwd: root, shell: true });
if (res.status !== 0) {
console.error(
`\nPublish failed for "${slug}" (exit ${res.status}). ` +
`Check that the clawhub CLI is installed and you have run \`clawhub login\`, then re-run. ` +
`Skills already published in this run are unaffected.`,
);
process.exit(res.status || 1);
}
}
console.log(`\nDone. Published ${slugs.length} skills at ${version}.`);