Files
Aperant/scripts/update-readme.mjs
T
André Mikalsen d958fa65cb fix: harden CI/CD with coverage enforcement, eliminate Python, add 22 test files (#1945)
* feat: harden CI/CD with coverage enforcement, eliminate Python, add 22 test files

- Port scripts/update-readme.py to Node.js (update-readme.mjs) and remove
  Python dependency from release workflow
- Add coverage thresholds to vitest.config.ts with @vitest/coverage-v8
- Run coverage on ubuntu in CI, upload artifacts, add PR coverage comments
- Create E2E workflow (.github/workflows/e2e.yml) with Playwright on ubuntu
- Add test:unit and test:integration scripts for separated test execution
- Add 22 new test files (389 tests) covering previously untested AI layer:
  - 6 builtin tool tests (edit, bash, read, write, glob, grep)
  - 4 orchestration tests (recovery-manager, qa-loop, qa-reports, parallel-executor)
  - 5 auth/client/mcp tests (resolver, types, factory, client, registry)
  - 7 runner tests (changelog, commit-message, ideation, insights, insight-extractor,
    merge-resolver, roadmap)

Total: 206 test files, 4594 tests passing. Zero Python dependencies in CI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve Windows path separator failures in tests

Use path.join() and path.resolve() for cross-platform path construction
in test assertions instead of hardcoded forward slashes. Also mark E2E
workflow as continue-on-error for pre-existing __dirname ESM issue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 19:56:14 +01:00

203 lines
5.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Update README.md version badges and download links.
*
* Usage:
* node scripts/update-readme.mjs <version> [--prerelease]
*
* Examples:
* node scripts/update-readme.mjs 2.8.0 # Stable release
* node scripts/update-readme.mjs 2.8.0-beta.1 --prerelease # Beta release
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { argv, stderr, exit } from 'node:process';
// Semver pattern: X.Y.Z or X.Y.Z-prerelease.N
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[a-zA-Z]+\.\d+)?$/;
/**
* Validate version string matches semver format.
* @param {string} version
* @returns {boolean}
*/
export function validateVersion(version) {
return SEMVER_PATTERN.test(version);
}
/**
* Escape a string for use in a RegExp.
* @param {string} str
* @returns {string}
*/
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Update content between markers with given replacements.
* @param {string} text
* @param {string} startMarker
* @param {string} endMarker
* @param {Array<[string, string]>} replacements - [regexPattern, replacement] pairs
* @returns {string}
*/
export function updateSection(text, startMarker, endMarker, replacements) {
const pattern = new RegExp(
`(${escapeRegExp(startMarker)})(.*?)(${escapeRegExp(endMarker)})`,
's', // dotAll flag — equivalent to re.DOTALL
);
return text.replace(pattern, (_match, g1, section, g3) => {
let updated = section;
for (const [oldPattern, newValue] of replacements) {
updated = updated.replace(new RegExp(oldPattern, 'g'), newValue);
}
return g1 + updated + g3;
});
}
/**
* Update README.md with new version.
* @param {string} version - Version string (e.g., "2.8.0" or "2.8.0-beta.1")
* @param {boolean} isPrerelease - Whether this is a prerelease version
* @returns {boolean} True if changes were made, false otherwise
*/
export function updateReadme(version, isPrerelease) {
// Shields.io escapes hyphens as --
const versionBadge = version.replaceAll('-', '--');
// Read README
const originalContent = readFileSync('README.md', 'utf8');
let content = originalContent;
// Semver pattern: matches X.Y.Z or X.Y.Z-prerelease (e.g., 2.7.2, 2.7.2-beta.10)
// Prerelease MUST contain a dot (beta.10, alpha.1, rc.1) to avoid matching platform suffixes (win32, darwin)
const semver = String.raw`\d+\.\d+\.\d+(?:-[a-zA-Z]+\.[a-zA-Z0-9.]+)?`;
// Shields.io escaped pattern (hyphens as --)
const semverBadge = String.raw`\d+\.\d+\.\d+(?:--[a-zA-Z]+\.[a-zA-Z0-9.]+)?`;
if (isPrerelease) {
console.log(`Updating BETA section to ${version} (badge: ${versionBadge})`);
// Update beta badge
content = content.replace(
new RegExp(`beta-${semverBadge}-orange`, 'g'),
`beta-${versionBadge}-orange`,
);
// Update beta version badge link
content = updateSection(
content,
'<!-- BETA_VERSION_BADGE -->',
'<!-- BETA_VERSION_BADGE_END -->',
[[`tag/v${semver}\\)`, `tag/v${version})`]],
);
// Update beta downloads
content = updateSection(
content,
'<!-- BETA_DOWNLOADS -->',
'<!-- BETA_DOWNLOADS_END -->',
[
[`Auto-Claude-${semver}`, `Auto-Claude-${version}`],
[`download/v${semver}/`, `download/v${version}/`],
],
);
} else {
console.log(`Updating STABLE section to ${version} (badge: ${versionBadge})`);
// Update top version badge
content = updateSection(
content,
'<!-- TOP_VERSION_BADGE -->',
'<!-- TOP_VERSION_BADGE_END -->',
[
[`version-${semverBadge}-blue`, `version-${versionBadge}-blue`],
[`tag/v${semver}\\)`, `tag/v${version})`],
],
);
// Update stable badge
content = content.replace(
new RegExp(`stable-${semverBadge}-blue`, 'g'),
`stable-${versionBadge}-blue`,
);
// Update stable version badge link
content = updateSection(
content,
'<!-- STABLE_VERSION_BADGE -->',
'<!-- STABLE_VERSION_BADGE_END -->',
[[`tag/v${semver}\\)`, `tag/v${version})`]],
);
// Update stable downloads
content = updateSection(
content,
'<!-- STABLE_DOWNLOADS -->',
'<!-- STABLE_DOWNLOADS_END -->',
[
[`Auto-Claude-${semver}`, `Auto-Claude-${version}`],
[`download/v${semver}/`, `download/v${version}/`],
],
);
}
// Check if changes were made
if (content === originalContent) {
console.log('No changes needed');
return false;
}
// Write updated README
writeFileSync('README.md', content, 'utf8');
console.log(`README.md updated for ${version} (prerelease=${isPrerelease})`);
return true;
}
function main() {
const args = argv.slice(2);
// Parse args: <version> [--prerelease]
const versionArg = args.find((a) => !a.startsWith('--'));
const isPrereleaseFlag = args.includes('--prerelease');
if (!versionArg) {
stderr.write('usage: node scripts/update-readme.mjs <version> [--prerelease]\n');
exit(1);
}
// Validate version format
if (!validateVersion(versionArg)) {
stderr.write(`ERROR: Invalid version format: ${versionArg}\n`);
stderr.write('Expected format: X.Y.Z or X.Y.Z-prerelease.N (e.g., 2.8.0 or 2.8.0-beta.1)\n');
exit(1);
}
// Auto-detect prerelease if not explicitly set
const isPrerelease = isPrereleaseFlag || versionArg.includes('-');
try {
updateReadme(versionArg, isPrerelease);
exit(0);
} catch (err) {
if (err.code === 'ENOENT') {
stderr.write('ERROR: README.md not found\n');
} else {
stderr.write(`ERROR: ${err.message}\n`);
}
exit(1);
}
}
// Only run when invoked directly (not when imported by tests)
const isMain =
argv[1] &&
(await import('node:url')).fileURLToPath(import.meta.url) === argv[1];
if (isMain) {
main();
}