From c4cdbdc4c0c5d07f481055e1d88e785255600d79 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Mon, 25 May 2026 11:38:44 -0500 Subject: [PATCH] fix(web-bundles): harden release script per PR review - Verify the zip CLI is on PATH up front with a clear install hint, instead of crashing mid-zip with an opaque execSync error. - Wrap JSON.parse in try/catch; validate the manifest shape (bundles array non-empty, releaseTag present, slug present per entry) before trying to package, so config errors fail with a targeted message. - Catch zip failures per-bundle and surface the failing slug. - Refuse to print the gh release command when zero bundles were packaged (would otherwise mislead the user into creating an empty release). - Derive --title from manifest.releaseTag so the printed command can never drift from the actual tag (was previously hardcoded "Web Bundles v1" while the tag had moved to v1.0.0). - Remove the stale `web-bundles-v1` example from the file header. Addresses augmentcode bot review comments on PR #2424. --- tools/bundle-web-bundles.js | 70 ++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/tools/bundle-web-bundles.js b/tools/bundle-web-bundles.js index ba1d7519c..899dc203d 100644 --- a/tools/bundle-web-bundles.js +++ b/tools/bundle-web-bundles.js @@ -7,10 +7,8 @@ * Usage: * node tools/bundle-web-bundles.js * - * Then upload the resulting zips to a GitHub Release: - * gh release create web-bundles-v1 dist/web-bundles/*.zip \ - * --title "Web Bundles v1" \ - * --notes "BMad web bundles for Gemini Gems and ChatGPT Custom GPTs" + * After running, the script prints the exact `gh release create` command + * (with the correct tag from bundles.json) for you to copy. */ const fs = require('node:fs'); @@ -22,14 +20,42 @@ const BUNDLES_DIR = path.join(REPO_ROOT, 'web-bundles'); const DIST_DIR = path.join(REPO_ROOT, 'dist', 'web-bundles'); const MANIFEST = path.join(BUNDLES_DIR, 'bundles.json'); -function main() { - if (!fs.existsSync(MANIFEST)) { - console.error(`Error: bundles.json not found at ${MANIFEST}`); - process.exit(1); - } +function fail(msg) { + console.error(`[ERROR] ${msg}`); + process.exit(1); +} - const manifest = JSON.parse(fs.readFileSync(MANIFEST, 'utf-8')); - const releaseTag = manifest.releaseTag || 'web-bundles-v1'; +function requireZipCli() { + try { + execSync('zip -v', { stdio: 'ignore' }); + } catch { + fail("'zip' CLI not found on PATH. Install zip (macOS: preinstalled; Debian/Ubuntu: apt install zip; Alpine: apk add zip) and re-run."); + } +} + +function loadManifest() { + if (!fs.existsSync(MANIFEST)) { + fail(`bundles.json not found at ${MANIFEST}`); + } + let manifest; + try { + manifest = JSON.parse(fs.readFileSync(MANIFEST, 'utf-8')); + } catch (error) { + fail(`bundles.json is not valid JSON: ${error.message}`); + } + if (!Array.isArray(manifest.bundles) || manifest.bundles.length === 0) { + fail('bundles.json is missing a non-empty "bundles" array.'); + } + if (typeof manifest.releaseTag !== 'string' || !manifest.releaseTag) { + fail('bundles.json is missing "releaseTag".'); + } + return manifest; +} + +function main() { + requireZipCli(); + const manifest = loadManifest(); + const releaseTag = manifest.releaseTag; fs.mkdirSync(DIST_DIR, { recursive: true }); @@ -37,6 +63,10 @@ function main() { const zipped = []; for (const bundle of manifest.bundles) { + if (!bundle.slug) { + console.warn(` [SKIP] bundle entry missing slug — ${JSON.stringify(bundle).slice(0, 80)}`); + continue; + } const src = path.join(BUNDLES_DIR, bundle.slug); if (!fs.existsSync(src)) { console.warn(` [SKIP] ${bundle.slug} — directory not found`); @@ -46,20 +76,28 @@ function main() { const out = path.join(DIST_DIR, `${bundle.slug}.zip`); if (fs.existsSync(out)) fs.unlinkSync(out); - execSync(`zip -r -X -q "${out}" "${bundle.slug}" -x "*.DS_Store"`, { - cwd: BUNDLES_DIR, - stdio: 'inherit', - }); + try { + execSync(`zip -r -X -q "${out}" "${bundle.slug}" -x "*.DS_Store"`, { + cwd: BUNDLES_DIR, + stdio: 'inherit', + }); + } catch (error) { + fail(`zip failed for ${bundle.slug}: ${error.message}`); + } const size = (fs.statSync(out).size / 1024).toFixed(1); console.log(` [OK] ${bundle.slug}.zip (${size} KB)`); zipped.push(bundle.slug); } + if (zipped.length === 0) { + fail('No bundles were packaged. Check bundles.json slugs against web-bundles/ subdirectories.'); + } + console.log(`\nWrote ${zipped.length} bundles to ${path.relative(REPO_ROOT, DIST_DIR)}/`); console.log('\nNext step — create or update the GitHub Release:\n'); console.log(` gh release create ${releaseTag} dist/web-bundles/*.zip \\`); - console.log(` --title "Web Bundles v1" \\`); + console.log(` --title "${releaseTag}" \\`); console.log(` --notes "BMad web bundles for Gemini Gems and ChatGPT Custom GPTs. See https://bmadcode.com/web-bundles/"\n`); console.log('Or, to refresh an existing release:\n'); console.log(` gh release upload ${releaseTag} dist/web-bundles/*.zip --clobber\n`);