mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
* Guard plugin/skill version drift in the build (issue #274) The Claude Code marketplace installs from the committed ./plugin subtree, so a version disagreement between the hand-edited manifests and the generated subtree ships stale content under a wrong version. This is the class of bug reported in #274: a version bump that doesn't regenerate ./plugin (e.g. PR #252, where root plugin.json was 3.7.0 while plugin/.claude-plugin/plugin.json was still 3.6.0) merges a drift window onto main, and marketplace/Cowork installs pull the stale subtree. Add a build-time validator that treats root .claude-plugin/plugin.json as the source of truth and fails the build if any of these disagree: - .claude-plugin/marketplace.json plugins[0].version (hand-edited; the post-merge sync workflow never bumps versions, so it can't repair a mismatch here) - plugin/.claude-plugin/plugin.json version (generated subtree) - plugin/skills/impeccable/SKILL.md frontmatter version (bundled skill) It only fires on an inconsistent bump; PRs that don't touch versions keep every file in agreement and stay silent. The pure comparison lives in scripts/lib/validate-plugin-versions.js with direct unit coverage; build.js owns the logging and the non-zero exit. Documents the regenerate-on-bump step in CLAUDE.md's Versioning section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Harden version-drift collector against malformed/incomplete manifests Address Greptile review on #278: - Wrap every file read/parse in a sentinel helper (extractFromFile) so a half-edited manifest — the exact state during a version bump — yields a clean "could not parse (...)" diagnostic naming the file instead of a raw JSON.parse stack trace out of build(). - Report a present-but-malformed root plugin.json, or one missing its `version` field, as an explicit error. Previously `undefined` version short-circuited the build wrapper's `source == null` guard and passed silently. collectPluginVersions now returns an `errors` array; build.js fails on errors + mismatches combined, and only the genuinely-absent root manifest is a no-op skip. Adds 4 unit tests: malformed checked manifest, malformed root, missing version field, and the absent-root no-errors case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Make SKILL.md frontmatter version read CRLF-tolerant Address Cursor Bugbot review on #278: readSkillFrontmatterVersion only matched `\n` delimiters, while the shared parseFrontmatter in scripts/lib/utils.js accepts `\r?\n`. A bundled SKILL.md saved with CRLF line endings would parse to a null version and trip a false mismatch against root plugin.json even when the version line is correct. Match the shared parser's `\r?\n` tolerance and drop the `$` anchor on the version line (it would not match before a `\r`). Adds CRLF coverage for both readSkillFrontmatterVersion and collectPluginVersions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Re-trigger CI (no file change) CI did not fire for 5cda9f6b; force a fresh run on the current tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c0d50e36da
commit
793feda5a0
+37
-1
@@ -23,6 +23,7 @@ import { generateApiData } from './lib/api-data.js';
|
||||
import { createTransformer, PROVIDERS } from './lib/transformers/index.js';
|
||||
import { hooksJsonFor, buildClaudePluginHooksManifest } from './lib/transformers/hooks.js';
|
||||
import { createAllZips } from './lib/zip.js';
|
||||
import { collectPluginVersions } from './lib/validate-plugin-versions.js';
|
||||
import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs';
|
||||
// Sub-page generation is now handled by Astro content collections.
|
||||
|
||||
@@ -112,6 +113,37 @@ function generateCounts(rootDir, skills, buildDir) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guard against plugin/skill version drift (issue #274). The pure comparison
|
||||
* lives in ./lib/validate-plugin-versions.js (so it's unit-tested directly);
|
||||
* this wrapper owns the console output and the error count the build gates on.
|
||||
*/
|
||||
function validatePluginVersions(rootDir) {
|
||||
const { source, mismatches, errors } = collectPluginVersions(rootDir);
|
||||
// No root manifest at all → nothing to check (source null with no errors).
|
||||
if (source == null && errors.length === 0) return 0;
|
||||
|
||||
for (const { relPath, reason } of errors) {
|
||||
console.error(` ❌ ${relPath}: ${reason}`);
|
||||
}
|
||||
for (const { relPath, found, expected } of mismatches) {
|
||||
console.error(
|
||||
` ❌ ${relPath}: version "${found}" disagrees with .claude-plugin/plugin.json "${expected}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const total = errors.length + mismatches.length;
|
||||
if (total > 0) {
|
||||
console.error(
|
||||
`\n❌ ${total} plugin/skill version problem(s). Bump every version together and run ` +
|
||||
`\`bun run build:release\` to regenerate the ./plugin subtree (issue #274).`,
|
||||
);
|
||||
} else {
|
||||
console.log(`✓ Plugin/skill versions agree: ${source}`);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function validateSkillFrontmatter(skills) {
|
||||
let errors = 0;
|
||||
|
||||
@@ -766,6 +798,10 @@ async function build() {
|
||||
// Generate authoritative counts and validate references
|
||||
const countErrors = generateCounts(ROOT_DIR, skills, buildDir);
|
||||
|
||||
// Guard plugin/skill version drift: marketplace + ./plugin subtree must
|
||||
// match root plugin.json so marketplace installs never ship a stale version.
|
||||
const versionErrors = validatePluginVersions(ROOT_DIR);
|
||||
|
||||
// Verify every hand-authored HTML page carries the shared site header
|
||||
const headerErrors = validateSiteHeader(ROOT_DIR);
|
||||
|
||||
@@ -779,7 +815,7 @@ async function build() {
|
||||
// that has no technical reading. Hardening repetition is intentionally allowed.
|
||||
const skillProseErrors = validateSkillProse(ROOT_DIR);
|
||||
|
||||
if (countErrors > 0 || headerErrors > 0 || themeErrors > 0 || proseErrors > 0 || skillProseErrors > 0) {
|
||||
if (countErrors > 0 || versionErrors > 0 || headerErrors > 0 || themeErrors > 0 || proseErrors > 0 || skillProseErrors > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user