mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 02:26:31 +03:00
Add release tooling and bump CLI to 2.1.8
- scripts/release.mjs tags and publishes GitHub releases for the three
independently versioned components (skill, cli, extension). Refuses on
dirty tree, unpushed HEAD, missing changelog entry, or stale build
outputs. Skill release attaches dist/universal.zip; extension release
runs build:extension and attaches dist/extension.zip. Prints a manual
next-step hint for npm publish (CLI) and Chrome Web Store upload.
- package.json: bump CLI to 2.1.8, add release:{skill,cli,ext} scripts.
- public/index.html: add CLI v2.1.8 changelog entry covering the
Windows path fix (#95) and border-radius detector hardening. Adopt
"CLI v" / "Extension v" prefix convention to disambiguate components
in the shared changelog timeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
5f5e2b013d
commit
a923346bcc
+4
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"version": "2.1.7",
|
||||
"version": "2.1.8",
|
||||
"author": "Paul Bakaus",
|
||||
"description": "Design skills, commands, and anti-pattern detection for AI coding agents",
|
||||
"keywords": [
|
||||
@@ -53,6 +53,9 @@
|
||||
"test:live-e2e": "node --test --test-timeout=600000 tests/live-e2e.test.mjs",
|
||||
"prepack": "cp README.md README.repo.md && cp README.npm.md README.md",
|
||||
"postpack": "cp README.repo.md README.md && rm README.repo.md",
|
||||
"release:skill": "node scripts/release.mjs skill",
|
||||
"release:cli": "node scripts/release.mjs cli",
|
||||
"release:ext": "node scripts/release.mjs extension",
|
||||
"screenshot": "bun run scripts/screenshot-antipatterns.js",
|
||||
"og-image": "bun run scripts/generate-og-image.js"
|
||||
},
|
||||
|
||||
@@ -933,6 +933,17 @@
|
||||
</div>
|
||||
|
||||
<div class="changelog-list" data-reveal>
|
||||
<div class="changelog-entry">
|
||||
<div class="changelog-version-header">
|
||||
<span class="changelog-version">CLI v2.1.8</span>
|
||||
<span class="changelog-date">April 28, 2026</span>
|
||||
</div>
|
||||
<ul class="changelog-items">
|
||||
<li><strong>Detector runs on Windows.</strong> CLI path resolution used <code>new URL(...).pathname</code>, which prepends a slash to drive letters (<code>/C:/...</code>) and breaks <code>fs</code> on Windows. Switched to <code>fileURLToPath</code> across the board. Reported in <a href="https://github.com/pbakaus/impeccable/issues/95" target="_blank" rel="noopener">#95</a>, contributed by <a href="https://github.com/voidborne-d" target="_blank" rel="noopener">@voidborne-d</a>.</li>
|
||||
<li><strong>Border-radius detection no longer flickers under jsdom.</strong> The pill-button rule was missing real cases when jsdom returned a percent radius without an explicit width, and over-reporting when CSS shorthand left individual corners unset. The reader now preserves the percent signal and resolves shorthand consistently across the browser and jsdom paths, so CLI scans match what the live overlay sees.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="changelog-entry">
|
||||
<div class="changelog-version-header">
|
||||
<span class="changelog-version">v3.0.4</span>
|
||||
|
||||
Executable
+216
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env node
|
||||
// Tags and publishes a GitHub release for one of three independently versioned
|
||||
// components: skill, cli, extension.
|
||||
//
|
||||
// Usage: node scripts/release.mjs <skill|cli|extension> [--dry-run]
|
||||
//
|
||||
// Refuses on a dirty tree, an unpushed HEAD, or a missing changelog entry.
|
||||
// For the skill component, also reruns `bun run build` and refuses if the
|
||||
// regenerated harness directories drift from what is committed.
|
||||
|
||||
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const COMPONENTS = {
|
||||
skill: {
|
||||
manifest: '.claude-plugin/plugin.json',
|
||||
sibling: '.claude-plugin/marketplace.json',
|
||||
siblingVersion: (m) => m.plugins?.[0]?.version,
|
||||
tagPrefix: 'skill-v',
|
||||
label: 'Skill',
|
||||
changelogLabel: 'v',
|
||||
buildCmd: 'bun run build',
|
||||
artifacts: ['dist/universal.zip'],
|
||||
postReleaseHint: null,
|
||||
},
|
||||
cli: {
|
||||
manifest: 'package.json',
|
||||
tagPrefix: 'cli-v',
|
||||
label: 'CLI',
|
||||
changelogLabel: 'CLI v',
|
||||
buildCmd: null,
|
||||
artifacts: [],
|
||||
postReleaseHint: 'Run `npm publish` next to push the package to the npm registry.',
|
||||
},
|
||||
extension: {
|
||||
manifest: 'extension/manifest.json',
|
||||
tagPrefix: 'ext-v',
|
||||
label: 'Extension',
|
||||
changelogLabel: 'Extension v',
|
||||
buildCmd: 'bun run build:extension',
|
||||
artifacts: ['dist/extension.zip'],
|
||||
postReleaseHint: 'Upload `dist/extension.zip` to the Chrome Web Store dashboard to publish.',
|
||||
},
|
||||
};
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const component = args.find((a) => !a.startsWith('--'));
|
||||
|
||||
if (!component || !COMPONENTS[component]) {
|
||||
console.error('usage: release.mjs <skill|cli|extension> [--dry-run]');
|
||||
process.exit(1);
|
||||
}
|
||||
const cfg = COMPONENTS[component];
|
||||
|
||||
function fail(msg) {
|
||||
console.error(`✗ ${msg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
function ok(msg) {
|
||||
console.log(`✓ ${msg}`);
|
||||
}
|
||||
function step(msg) {
|
||||
console.log(`\n→ ${msg}`);
|
||||
}
|
||||
function run(cmd) {
|
||||
return execSync(cmd, { cwd: repoRoot, encoding: 'utf8' }).trim();
|
||||
}
|
||||
function runMutating(cmd) {
|
||||
if (dryRun) {
|
||||
console.log(` [dry-run] ${cmd}`);
|
||||
return;
|
||||
}
|
||||
execSync(cmd, { cwd: repoRoot, stdio: 'inherit' });
|
||||
}
|
||||
|
||||
step(`Reading version from ${cfg.manifest}`);
|
||||
const manifest = JSON.parse(readFileSync(path.join(repoRoot, cfg.manifest), 'utf8'));
|
||||
const version = manifest.version;
|
||||
if (!version) fail(`No version field in ${cfg.manifest}`);
|
||||
ok(`${cfg.label} ${version}`);
|
||||
|
||||
if (cfg.sibling) {
|
||||
const sibling = JSON.parse(readFileSync(path.join(repoRoot, cfg.sibling), 'utf8'));
|
||||
const siblingVersion = cfg.siblingVersion(sibling);
|
||||
if (siblingVersion !== version) {
|
||||
fail(`${cfg.manifest} (${version}) and ${cfg.sibling} (${siblingVersion}) disagree. Bump both.`);
|
||||
}
|
||||
ok(`${cfg.sibling} agrees`);
|
||||
}
|
||||
|
||||
const tag = `${cfg.tagPrefix}${version}`;
|
||||
|
||||
step('Checking working tree is clean');
|
||||
const status = run('git status --porcelain');
|
||||
if (status) fail(`Working tree is dirty. Commit or stash first:\n${status}`);
|
||||
ok('clean');
|
||||
|
||||
if (cfg.buildCmd) {
|
||||
step(`Rebuilding outputs (${cfg.buildCmd})`);
|
||||
if (dryRun) {
|
||||
console.log(` [dry-run] ${cfg.buildCmd}`);
|
||||
} else {
|
||||
execSync(cfg.buildCmd, { cwd: repoRoot, stdio: 'inherit' });
|
||||
const postBuild = run('git status --porcelain');
|
||||
if (postBuild) {
|
||||
fail(`Build produced uncommitted changes. Run \`${cfg.buildCmd}\`, commit the result, then re-run.\n${postBuild}`);
|
||||
}
|
||||
ok('build outputs match source');
|
||||
}
|
||||
}
|
||||
|
||||
step('Checking HEAD is pushed to origin');
|
||||
const branch = run('git rev-parse --abbrev-ref HEAD');
|
||||
const head = run('git rev-parse HEAD');
|
||||
let remoteHead;
|
||||
try {
|
||||
remoteHead = run(`git rev-parse origin/${branch}`);
|
||||
} catch {
|
||||
fail(`No tracking branch origin/${branch}. Push first.`);
|
||||
}
|
||||
if (head !== remoteHead) fail(`HEAD is ahead of origin/${branch}. Push your commits first.`);
|
||||
ok(`origin/${branch} matches HEAD`);
|
||||
|
||||
step(`Verifying tag ${tag} does not already exist`);
|
||||
let localTagExists = false;
|
||||
try {
|
||||
run(`git rev-parse -q --verify "refs/tags/${tag}"`);
|
||||
localTagExists = true;
|
||||
} catch {}
|
||||
if (localTagExists) fail(`Tag ${tag} already exists locally.`);
|
||||
const remoteTags = run('git ls-remote --tags origin');
|
||||
if (remoteTags.split('\n').some((line) => line.endsWith(`refs/tags/${tag}`))) {
|
||||
fail(`Tag ${tag} already exists on origin.`);
|
||||
}
|
||||
ok('tag is free');
|
||||
|
||||
step(`Extracting changelog entry for "${cfg.changelogLabel}${version}"`);
|
||||
const indexHtml = readFileSync(path.join(repoRoot, 'public/index.html'), 'utf8');
|
||||
const expectedHeader = `<span class="changelog-version">${cfg.changelogLabel}${version}</span>`;
|
||||
const headerIdx = indexHtml.indexOf(expectedHeader);
|
||||
if (headerIdx === -1) {
|
||||
fail(`No changelog entry found for "${cfg.changelogLabel}${version}" in public/index.html. Add one before releasing.`);
|
||||
}
|
||||
const entryStart = indexHtml.lastIndexOf('<div class="changelog-entry"', headerIdx);
|
||||
const ulEnd = indexHtml.indexOf('</ul>', headerIdx);
|
||||
if (entryStart === -1 || ulEnd === -1) fail('Changelog entry markup is malformed.');
|
||||
const entryEnd = indexHtml.indexOf('</div>', ulEnd) + '</div>'.length;
|
||||
const entryHtml = indexHtml.slice(entryStart, entryEnd);
|
||||
|
||||
const notes = htmlToMarkdown(entryHtml);
|
||||
ok('extracted');
|
||||
|
||||
step('Verifying release artifacts exist');
|
||||
for (const artifact of cfg.artifacts) {
|
||||
const abs = path.join(repoRoot, artifact);
|
||||
if (!existsSync(abs)) fail(`Missing artifact: ${artifact}`);
|
||||
ok(artifact);
|
||||
}
|
||||
|
||||
console.log('\n--- Release notes preview ---');
|
||||
console.log(notes);
|
||||
console.log('--- end preview ---\n');
|
||||
|
||||
step(`Creating annotated tag ${tag}`);
|
||||
const tagMessageFile = path.join(repoRoot, '.release-tag-msg.tmp');
|
||||
const releaseNotesFile = path.join(repoRoot, '.release-notes.tmp.md');
|
||||
if (!dryRun) {
|
||||
writeFileSync(tagMessageFile, `${cfg.label} ${version}\n\n${notes}\n`);
|
||||
writeFileSync(releaseNotesFile, notes);
|
||||
}
|
||||
try {
|
||||
runMutating(`git tag -a ${tag} -F "${tagMessageFile}"`);
|
||||
runMutating(`git push origin ${tag}`);
|
||||
|
||||
step(`Creating GitHub release ${tag}`);
|
||||
const artifactArgs = cfg.artifacts.map((a) => `"${a}"`).join(' ');
|
||||
const title = `${cfg.label} ${version}`;
|
||||
runMutating(
|
||||
`gh release create ${tag} --title "${title}" --notes-file "${releaseNotesFile}"${artifactArgs ? ' ' + artifactArgs : ''}`
|
||||
);
|
||||
|
||||
} finally {
|
||||
if (!dryRun) {
|
||||
try { unlinkSync(tagMessageFile); } catch {}
|
||||
try { unlinkSync(releaseNotesFile); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✓ ${cfg.label} ${version} released as ${tag}`);
|
||||
if (cfg.postReleaseHint) {
|
||||
console.log(`\n→ Next step: ${cfg.postReleaseHint}`);
|
||||
}
|
||||
|
||||
function htmlToMarkdown(html) {
|
||||
let md = html;
|
||||
md = md.replace(/<div class="changelog-version-header"[\s\S]*?<\/div>/, '');
|
||||
md = md.replace(/<li>([\s\S]*?)<\/li>/g, (_, inner) => `- ${inner.trim()}\n`);
|
||||
md = md.replace(/<strong>([\s\S]*?)<\/strong>/g, '**$1**');
|
||||
md = md.replace(/<code>([\s\S]*?)<\/code>/g, '`$1`');
|
||||
md = md.replace(/<a\s+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g, '[$2]($1)');
|
||||
md = md.replace(/<\/?(ul|div|span)[^>]*>/g, '');
|
||||
md = md.replace(/×/g, '×');
|
||||
md = md.replace(/&/g, '&');
|
||||
md = md.replace(/</g, '<');
|
||||
md = md.replace(/>/g, '>');
|
||||
md = md.replace(/"/g, '"');
|
||||
md = md.replace(/'/g, "'");
|
||||
md = md.replace(/[ \t]+\n/g, '\n');
|
||||
md = md.replace(/\n{3,}/g, '\n\n');
|
||||
return md.trim();
|
||||
}
|
||||
Reference in New Issue
Block a user