chore: automate release checks
This commit is contained in:
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const pkgPath = path.join(repoRoot, 'package.json');
|
||||
const publicDir = path.join(repoRoot, 'public');
|
||||
if (!fs.existsSync(pkgPath) || !fs.existsSync(publicDir)) process.exit(0);
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
const version = pkg.version;
|
||||
if (!version) {
|
||||
console.error('package.json version missing');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function walkHtmlFiles(dir) {
|
||||
const out = [];
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...walkHtmlFiles(full));
|
||||
else if (entry.isFile() && entry.name.endsWith('.html')) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function isLocalAsset(rawUrl) {
|
||||
return !/^(?:[a-z]+:|\/\/|#)/i.test(rawUrl);
|
||||
}
|
||||
|
||||
function withVersion(rawUrl) {
|
||||
const [baseWithQuery, hash = ''] = rawUrl.split('#');
|
||||
const [pathname, query = ''] = baseWithQuery.split('?');
|
||||
const params = new URLSearchParams(query);
|
||||
params.set('v', version);
|
||||
const qs = params.toString();
|
||||
return pathname + (qs ? '?' + qs : '') + (hash ? '#' + hash : '');
|
||||
}
|
||||
|
||||
const htmlFiles = walkHtmlFiles(publicDir);
|
||||
let changed = 0;
|
||||
for (const file of htmlFiles) {
|
||||
let content = fs.readFileSync(file, 'utf8');
|
||||
const next = content
|
||||
.replace(/(<link\b[^>]*href=["'])([^"']+\.css(?:\?[^"']*)?)(["'][^>]*>)/gi, (m, p1, url, p3) => {
|
||||
return isLocalAsset(url) ? p1 + withVersion(url) + p3 : m;
|
||||
})
|
||||
.replace(/(<script\b(?![^>]*type=["']application\/json["'])[^>]*\bsrc=["'])([^"']+\.js(?:\?[^"']*)?)(["'][^>]*>\s*<\/script>)/gi, (m, p1, url, p3) => {
|
||||
return isLocalAsset(url) ? p1 + withVersion(url) + p3 : m;
|
||||
});
|
||||
if (next !== content) {
|
||||
fs.writeFileSync(file, next);
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed > 0) console.log('Synced versioned assets in ' + changed + ' HTML file(s).');
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const cp = require('child_process');
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const pkgPath = path.join(repoRoot, 'package.json');
|
||||
const publicDir = path.join(repoRoot, 'public');
|
||||
if (!fs.existsSync(pkgPath) || !fs.existsSync(publicDir)) process.exit(0);
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
const version = pkg.version;
|
||||
const failures = [];
|
||||
|
||||
function git(args) {
|
||||
return cp.execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8' }).trim();
|
||||
}
|
||||
|
||||
function walkHtmlFiles(dir) {
|
||||
const out = [];
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...walkHtmlFiles(full));
|
||||
else if (entry.isFile() && entry.name.endsWith('.html')) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function isLocalAsset(rawUrl) {
|
||||
return !/^(?:[a-z]+:|\/\/|#)/i.test(rawUrl);
|
||||
}
|
||||
|
||||
for (const file of walkHtmlFiles(publicDir)) {
|
||||
const rel = path.relative(repoRoot, file);
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
for (const match of content.matchAll(/<link\b[^>]*href=["']([^"']+\.css(?:\?[^"']*)?)["']/gi)) {
|
||||
const url = match[1];
|
||||
if (isLocalAsset(url) && !new URLSearchParams((url.split('?')[1] || '')).get('v')?.includes(version)) {
|
||||
failures.push(rel + ': stylesheet version does not match package.json');
|
||||
}
|
||||
}
|
||||
for (const match of content.matchAll(/<script\b(?![^>]*type=["']application\/json["'])[^>]*\bsrc=["']([^"']+\.js(?:\?[^"']*)?)["']/gi)) {
|
||||
const url = match[1];
|
||||
if (isLocalAsset(url) && !new URLSearchParams((url.split('?')[1] || '')).get('v')?.includes(version)) {
|
||||
failures.push(rel + ': script version does not match package.json');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const staged = git(['diff', '--cached', '--name-only', '--diff-filter=ACMR']).split(/\n+/).filter(Boolean);
|
||||
const codeTouched = staged.some(file => /^(public\/.*\.(html|css|js)|server\.js|package\.json|package-lock\.json)$/.test(file));
|
||||
const projectMapTouched = staged.includes('PROJECT_MAP.md');
|
||||
const changelogTouched = staged.includes('CHANGELOG.md');
|
||||
if (codeTouched && !changelogTouched) failures.push('CHANGELOG.md must be staged when app code, assets, or version files change.');
|
||||
if (codeTouched && fs.existsSync(path.join(repoRoot, 'PROJECT_MAP.md')) && !projectMapTouched) {
|
||||
failures.push('PROJECT_MAP.md must be staged when app code or frontend state/routes change.');
|
||||
}
|
||||
|
||||
const stagedPackageDiff = staged.includes('package.json')
|
||||
? git(['diff', '--cached', '--', 'package.json'])
|
||||
: '';
|
||||
if (stagedPackageDiff.includes('"version"') && fs.existsSync(path.join(repoRoot, 'CHANGELOG.md'))) {
|
||||
const changelog = fs.readFileSync(path.join(repoRoot, 'CHANGELOG.md'), 'utf8');
|
||||
if (!changelog.includes('## [' + version + ']') && !changelog.includes('## [Unreleased]')) {
|
||||
failures.push('CHANGELOG.md must mention the current package.json version or contain an [Unreleased] section.');
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
console.error('Release verification failed:');
|
||||
for (const failure of failures) console.error('- ' + failure);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user