From d2deb04489e552462833c44472bc1cc6a7c9a146 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Sun, 12 Apr 2026 02:53:19 -0700 Subject: [PATCH] fix: harden manual release path --- scripts/release.sh | 8 ++++-- tests/scripts/release.test.js | 53 +++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 tests/scripts/release.test.js diff --git a/scripts/release.sh b/scripts/release.sh index c75f5747..dcaefbde 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -44,8 +44,8 @@ if [[ "$CURRENT_BRANCH" != "main" ]]; then exit 1 fi -# Check working tree is clean -if ! git diff --quiet || ! git diff --cached --quiet; then +# Check working tree is clean, including untracked files +if [[ -n "$(git status --porcelain --untracked-files=all)" ]]; then echo "Error: Working tree is not clean. Commit or stash changes first." exit 1 fi @@ -193,6 +193,10 @@ update_version "$OPENCODE_PACKAGE_JSON" "s|\"version\": *\"[^\"]*\"|\"version\": update_package_lock_version "$OPENCODE_PACKAGE_LOCK_JSON" update_readme_version_row +# Verify the bumped release surface is still internally consistent before +# writing a release commit, tag, or push. +node tests/plugin-manifest.test.js + # Stage, commit, tag, and push git add "$ROOT_PACKAGE_JSON" "$PACKAGE_LOCK_JSON" "$ROOT_AGENTS_MD" "$AGENT_YAML" "$VERSION_FILE" "$PLUGIN_JSON" "$MARKETPLACE_JSON" "$CODEX_MARKETPLACE_JSON" "$CODEX_PLUGIN_JSON" "$OPENCODE_PACKAGE_JSON" "$OPENCODE_PACKAGE_LOCK_JSON" "$README_FILE" git commit -m "chore: bump plugin version to $VERSION" diff --git a/tests/scripts/release.test.js b/tests/scripts/release.test.js new file mode 100644 index 00000000..8cda7eb2 --- /dev/null +++ b/tests/scripts/release.test.js @@ -0,0 +1,53 @@ +/** + * Source-level tests for scripts/release.sh + */ + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'release.sh'); +const source = fs.readFileSync(scriptPath, 'utf8'); + +function test(name, fn) { + try { + fn(); + console.log(` ✓ ${name}`); + return true; + } catch (error) { + console.log(` ✗ ${name}`); + console.log(` Error: ${error.message}`); + return false; + } +} + +function runTests() { + console.log('\n=== Testing release.sh ===\n'); + + let passed = 0; + let failed = 0; + + if (test('release script rejects untracked files when checking cleanliness', () => { + assert.ok( + source.includes('git status --porcelain --untracked-files=all'), + 'release.sh should use git status --porcelain --untracked-files=all for cleanliness checks' + ); + })) passed++; else failed++; + + if (test('release script reruns release metadata sync validation before commit/tag', () => { + const syncCheckIndex = source.indexOf('node tests/plugin-manifest.test.js'); + const commitIndex = source.indexOf('git commit -m "chore: bump plugin version to $VERSION"'); + + assert.ok(syncCheckIndex >= 0, 'release.sh should run plugin-manifest.test.js'); + assert.ok(commitIndex >= 0, 'release.sh should create the release commit'); + assert.ok( + syncCheckIndex < commitIndex, + 'plugin-manifest.test.js should run before the release commit is created' + ); + })) passed++; else failed++; + + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); + process.exit(failed > 0 ? 1 : 0); +} + +runTests();