From 28edd197c210344fe0fad45a768a944dc737aa45 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Sun, 12 Apr 2026 22:33:32 -0700 Subject: [PATCH] fix: harden release surface version and packaging sync (#1388) * fix: keep ecc release surfaces version-synced * fix: keep lockfile release version in sync * fix: remove release version drift from locks and tests * fix: keep root release metadata version-synced * fix: keep codex marketplace metadata version-synced * fix: gate release workflows on full metadata sync * fix: ship all versioned release metadata * fix: harden manual release path * fix: keep localized release docs version-synced * fix: sync install architecture version examples * test: cover shipped plugin metadata in npm pack * fix: verify final npm payload in release script * fix: ship opencode lockfile in npm package * docs: sync localized release highlights * fix: stabilize windows ci portability * fix: tighten release script version sync * fix: prefer repo-relative hook file paths * fix: make npm pack test shell-safe on windows --- .agents/plugins/marketplace.json | 1 + .github/workflows/release.yml | 11 +- .github/workflows/reusable-release.yml | 15 ++ README.zh-CN.md | 11 ++ docs/pt-BR/README.md | 9 ++ docs/tr/README.md | 9 ++ package.json | 6 +- scripts/release.sh | 183 +++++++++++++++++++++++-- tests/lib/install-state.test.js | 11 +- tests/lib/resolve-ecc-root.test.js | 13 +- tests/plugin-manifest.test.js | 136 ++++++++++++++++++ tests/scripts/build-opencode.test.js | 36 +++++ tests/scripts/release.test.js | 71 ++++++++++ 13 files changed, 487 insertions(+), 25 deletions(-) create mode 100644 tests/scripts/release.test.js diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 14abfa12..b088fe52 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -6,6 +6,7 @@ "plugins": [ { "name": "ecc", + "version": "1.10.0", "source": { "source": "local", "path": "../.." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cea5f6ca..1de1da7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,18 +38,21 @@ jobs: env: REF_NAME: ${{ github.ref_name }} - - name: Verify plugin.json version matches tag + - name: Verify package version matches tag env: TAG_NAME: ${{ github.ref_name }} run: | TAG_VERSION="${TAG_NAME#v}" - PLUGIN_VERSION=$(grep -oE '"version": *"[^"]*"' .claude-plugin/plugin.json | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') - if [ "$TAG_VERSION" != "$PLUGIN_VERSION" ]; then - echo "::error::Tag version ($TAG_VERSION) does not match plugin.json version ($PLUGIN_VERSION)" + PACKAGE_VERSION=$(node -p "require('./package.json').version") + if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then + echo "::error::Tag version ($TAG_VERSION) does not match package.json version ($PACKAGE_VERSION)" echo "Run: ./scripts/release.sh $TAG_VERSION" exit 1 fi + - name: Verify release metadata stays in sync + run: node tests/plugin-manifest.test.js + - name: Generate release highlights id: highlights env: diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml index 9fd37991..e6012659 100644 --- a/.github/workflows/reusable-release.yml +++ b/.github/workflows/reusable-release.yml @@ -47,6 +47,21 @@ jobs: exit 1 fi + - name: Verify package version matches tag + env: + INPUT_TAG: ${{ inputs.tag }} + run: | + TAG_VERSION="${INPUT_TAG#v}" + PACKAGE_VERSION=$(node -p "require('./package.json').version") + if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then + echo "::error::Tag version ($TAG_VERSION) does not match package.json version ($PACKAGE_VERSION)" + echo "Run: ./scripts/release.sh $TAG_VERSION" + exit 1 + fi + + - name: Verify release metadata stays in sync + run: node tests/plugin-manifest.test.js + - name: Generate release highlights env: TAG_NAME: ${{ inputs.tag }} diff --git a/README.zh-CN.md b/README.zh-CN.md index 1c1d333e..5f9f545b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -78,6 +78,17 @@ --- +## 最新动态 + +### v1.10.0 — 表面同步、运营工作流与 ECC 2.0 Alpha(2026年4月) + +- **公共表面已与真实仓库同步** —— 元数据、目录数量、插件清单以及安装文档现在都与实际开源表面保持一致。 +- **运营与外向型工作流扩展** —— `brand-voice`、`social-graph-ranker`、`customer-billing-ops`、`google-workspace-ops` 等运营型 skill 已纳入同一系统。 +- **媒体与发布工具补齐** —— `manim-video`、`remotion-video-creation` 以及社媒发布能力让技术讲解和发布流程直接在同一仓库内完成。 +- **框架与产品表面继续扩展** —— `nestjs-patterns`、更完整的 Codex/OpenCode 安装表面,以及跨 harness 打包改进,让仓库不再局限于 Claude Code。 +- **ECC 2.0 alpha 已进入仓库** —— `ecc2/` 下的 Rust 控制层现已可在本地构建,并提供 `dashboard`、`start`、`sessions`、`status`、`stop`、`resume` 与 `daemon` 命令。 +- **生态加固持续推进** —— AgentShield、ECC Tools 成本控制、计费门户工作与网站刷新仍围绕核心插件持续交付。 + ## 快速开始 在 2 分钟内快速上手: diff --git a/docs/pt-BR/README.md b/docs/pt-BR/README.md index 8298a2ba..9e1c9145 100644 --- a/docs/pt-BR/README.md +++ b/docs/pt-BR/README.md @@ -80,6 +80,15 @@ Este repositório contém apenas o código. Os guias explicam tudo. ## O Que Há de Novo +### v1.10.0 — Sincronização de Superfície, Fluxos Operacionais e ECC 2.0 Alpha (Abr 2026) + +- **Superfície pública sincronizada com o repositório real** — metadados, contagens de catálogo, manifests de plugin e documentação de instalação agora refletem a superfície OSS que realmente é entregue. +- **Expansão dos fluxos operacionais e externos** — `brand-voice`, `social-graph-ranker`, `customer-billing-ops`, `google-workspace-ops` e skills relacionadas fortalecem a trilha operacional dentro do mesmo sistema. +- **Ferramentas de mídia e lançamento** — `manim-video`, `remotion-video-creation` e os fluxos de publicação social colocam explicadores técnicos e lançamento no mesmo repositório. +- **Crescimento de framework e superfície de produto** — `nestjs-patterns`, superfícies de instalação mais ricas para Codex/OpenCode e melhorias de empacotamento cross-harness ampliam o uso além do Claude Code. +- **ECC 2.0 alpha já está no repositório** — o plano de controle em Rust dentro de `ecc2/` já compila localmente e expõe `dashboard`, `start`, `sessions`, `status`, `stop`, `resume` e `daemon`. +- **Fortalecimento do ecossistema** — AgentShield, controles de custo do ECC Tools, trabalho no portal de billing e a renovação do site continuam sendo entregues ao redor do plugin principal. + ### v1.9.0 — Instalação Seletiva e Expansão de Idiomas (Mar 2026) - **Arquitetura de instalação seletiva** — Pipeline de instalação baseado em manifesto com `install-plan.js` e `install-apply.js` para instalação de componentes direcionada. O state store rastreia o que está instalado e habilita atualizações incrementais. diff --git a/docs/tr/README.md b/docs/tr/README.md index 6264bdc3..812e172e 100644 --- a/docs/tr/README.md +++ b/docs/tr/README.md @@ -79,6 +79,15 @@ Bu repository yalnızca ham kodu içerir. Rehberler her şeyi açıklıyor. ## Yenilikler +### v1.10.0 — Surface Sync, Operatör İş Akışları ve ECC 2.0 Alpha (Nis 2026) + +- **Public surface canlı repo ile senkronlandı** — metadata, katalog sayıları, plugin manifest'leri ve kurulum odaklı dokümanlar artık gerçek OSS yüzeyiyle eşleşiyor. +- **Operatör ve dışa dönük iş akışları büyüdü** — `brand-voice`, `social-graph-ranker`, `customer-billing-ops`, `google-workspace-ops` ve ilgili operatör skill'leri aynı sistem içinde tamamlandı. +- **Medya ve lansman araçları** — `manim-video`, `remotion-video-creation` ve sosyal yayın yüzeyleri teknik anlatım ve duyuru akışlarını aynı repo içine taşıdı. +- **Framework ve ürün yüzeyi genişledi** — `nestjs-patterns`, daha zengin Codex/OpenCode kurulum yüzeyleri ve çapraz harness paketleme iyileştirmeleri repo'yu Claude Code dışına da taşıdı. +- **ECC 2.0 alpha repoda** — `ecc2/` altındaki Rust kontrol katmanı artık yerelde derleniyor ve `dashboard`, `start`, `sessions`, `status`, `stop`, `resume` ve `daemon` komutlarını sunuyor. +- **Ekosistem sağlamlaştırma** — AgentShield, ECC Tools maliyet kontrolleri, billing portal işleri ve web yüzeyi çekirdek plugin etrafında birlikte gelişmeye devam ediyor. + ### v1.9.0 — Seçici Kurulum & Dil Genişlemesi (Mar 2026) - **Seçici kurulum mimarisi** — `install-plan.js` ve `install-apply.js` ile manifest-tabanlı kurulum pipeline'ı, hedefli component kurulumu için. State store neyin kurulu olduğunu takip eder ve artımlı güncellemelere olanak sağlar. diff --git a/package.json b/package.json index 1a569589..4266e884 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ ".opencode/index.ts", ".opencode/opencode.json", ".opencode/package.json", + ".opencode/package-lock.json", ".opencode/tsconfig.json", ".opencode/MIGRATION.md", ".opencode/README.md", @@ -90,14 +91,17 @@ "scripts/uninstall.js", "skills/", "AGENTS.md", + "agent.yaml", ".claude-plugin/plugin.json", + ".claude-plugin/marketplace.json", ".claude-plugin/README.md", ".codex-plugin/plugin.json", ".codex-plugin/README.md", ".mcp.json", "install.sh", "install.ps1", - "llms.txt" + "llms.txt", + "VERSION" ], "bin": { "ecc": "scripts/ecc.js", diff --git a/scripts/release.sh b/scripts/release.sh index f9c1ebb7..d2694623 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -6,9 +6,21 @@ set -euo pipefail VERSION="${1:-}" ROOT_PACKAGE_JSON="package.json" +PACKAGE_LOCK_JSON="package-lock.json" +ROOT_AGENTS_MD="AGENTS.md" +TR_AGENTS_MD="docs/tr/AGENTS.md" +ZH_CN_AGENTS_MD="docs/zh-CN/AGENTS.md" +AGENT_YAML="agent.yaml" +VERSION_FILE="VERSION" PLUGIN_JSON=".claude-plugin/plugin.json" MARKETPLACE_JSON=".claude-plugin/marketplace.json" +CODEX_MARKETPLACE_JSON=".agents/plugins/marketplace.json" +CODEX_PLUGIN_JSON=".codex-plugin/plugin.json" OPENCODE_PACKAGE_JSON=".opencode/package.json" +OPENCODE_PACKAGE_LOCK_JSON=".opencode/package-lock.json" +README_FILE="README.md" +ZH_CN_README_FILE="docs/zh-CN/README.md" +SELECTIVE_INSTALL_ARCHITECTURE_DOC="docs/SELECTIVE-INSTALL-ARCHITECTURE.md" # Function to show usage usage() { @@ -36,14 +48,14 @@ 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 # Verify versioned manifests exist -for FILE in "$ROOT_PACKAGE_JSON" "$PLUGIN_JSON" "$MARKETPLACE_JSON" "$OPENCODE_PACKAGE_JSON"; do +for FILE in "$ROOT_PACKAGE_JSON" "$PACKAGE_LOCK_JSON" "$ROOT_AGENTS_MD" "$TR_AGENTS_MD" "$ZH_CN_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" "$ZH_CN_README_FILE" "$SELECTIVE_INSTALL_ARCHITECTURE_DOC"; do if [[ ! -f "$FILE" ]]; then echo "Error: $FILE not found" exit 1 @@ -58,13 +70,6 @@ if [[ -z "$OLD_VERSION" ]]; then fi echo "Bumping version: $OLD_VERSION -> $VERSION" -# Build and verify the packaged OpenCode payload before mutating any manifest -# versions or creating a tag. This keeps a broken npm artifact from being -# released via the manual script path. -echo "Verifying OpenCode build and npm pack payload..." -node scripts/build-opencode.js -node tests/scripts/build-opencode.test.js - update_version() { local file="$1" local pattern="$2" @@ -75,14 +80,170 @@ update_version() { fi } +update_package_lock_version() { + node -e ' + const fs = require("fs"); + const file = process.argv[1]; + const version = process.argv[2]; + const lock = JSON.parse(fs.readFileSync(file, "utf8")); + if (!lock || typeof lock !== "object") { + console.error(`Error: ${file} does not contain a JSON object`); + process.exit(1); + } + lock.version = version; + if (!lock.packages || typeof lock.packages !== "object" || Array.isArray(lock.packages)) { + console.error(`Error: ${file} is missing lock.packages`); + process.exit(1); + } + if (!lock.packages[""] || typeof lock.packages[""] !== "object" || Array.isArray(lock.packages[""])) { + console.error(`Error: ${file} is missing lock.packages[\"\"]`); + process.exit(1); + } + lock.packages[""].version = version; + fs.writeFileSync(file, `${JSON.stringify(lock, null, 2)}\n`); + ' "$1" "$VERSION" +} + +update_readme_version_row() { + local file="$1" + local label="$2" + local first_col="$3" + local second_col="$4" + local third_col="$5" + node -e ' + const fs = require("fs"); + const file = process.argv[1]; + const version = process.argv[2]; + const label = process.argv[3]; + const firstCol = process.argv[4]; + const secondCol = process.argv[5]; + const thirdCol = process.argv[6]; + const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const current = fs.readFileSync(file, "utf8"); + const updated = current.replace( + new RegExp( + `^\\| \\*\\*${escape(label)}\\*\\* \\| ${escape(firstCol)} \\| ${escape(secondCol)} \\| ${escape(thirdCol)} \\| [0-9]+\\.[0-9]+\\.[0-9]+ \\|$`, + "m" + ), + `| **${label}** | ${firstCol} | ${secondCol} | ${thirdCol} | ${version} |` + ); + if (updated === current) { + console.error(`Error: could not update README version row in ${file}`); + process.exit(1); + } + fs.writeFileSync(file, updated); + ' "$file" "$VERSION" "$label" "$first_col" "$second_col" "$third_col" +} + +update_selective_install_repo_version() { + local file="$1" + node -e ' + const fs = require("fs"); + const file = process.argv[1]; + const version = process.argv[2]; + const current = fs.readFileSync(file, "utf8"); + const updated = current.replace( + /("repoVersion":\s*")[0-9][0-9.]*(")/, + `$1${version}$2` + ); + if (updated === current) { + console.error(`Error: could not update repoVersion example in ${file}`); + process.exit(1); + } + fs.writeFileSync(file, updated); + ' "$file" "$VERSION" +} + +update_agents_version() { + local file="$1" + local label="$2" + node -e ' + const fs = require("fs"); + const file = process.argv[1]; + const version = process.argv[2]; + const label = process.argv[3]; + const current = fs.readFileSync(file, "utf8"); + const updated = current.replace( + new RegExp(`^\\*\\*${label}:\\*\\* [0-9][0-9.]*$`, "m"), + `**${label}:** ${version}` + ); + if (updated === current) { + console.error(`Error: could not update AGENTS version line in ${file}`); + process.exit(1); + } + fs.writeFileSync(file, updated); + ' "$file" "$VERSION" "$label" +} + +update_agent_yaml_version() { + node -e ' + const fs = require("fs"); + const file = process.argv[1]; + const version = process.argv[2]; + const current = fs.readFileSync(file, "utf8"); + const updated = current.replace( + /^version:\s*[0-9][0-9.]*$/m, + `version: ${version}` + ); + if (updated === current) { + console.error(`Error: could not update agent.yaml version line in ${file}`); + process.exit(1); + } + fs.writeFileSync(file, updated); + ' "$AGENT_YAML" "$VERSION" +} + +update_version_file() { + printf '%s\n' "$VERSION" > "$VERSION_FILE" +} + +update_codex_marketplace_version() { + node -e ' + const fs = require("fs"); + const file = process.argv[1]; + const version = process.argv[2]; + const marketplace = JSON.parse(fs.readFileSync(file, "utf8")); + if (!marketplace || typeof marketplace !== "object" || !Array.isArray(marketplace.plugins)) { + console.error(`Error: ${file} does not contain a marketplace plugins array`); + process.exit(1); + } + const plugin = marketplace.plugins.find(entry => entry && entry.name === "ecc"); + if (!plugin || typeof plugin !== "object") { + console.error(`Error: could not find ecc plugin entry in ${file}`); + process.exit(1); + } + plugin.version = version; + fs.writeFileSync(file, `${JSON.stringify(marketplace, null, 2)}\n`); + ' "$CODEX_MARKETPLACE_JSON" "$VERSION" +} + # Update all shipped package/plugin manifests update_version "$ROOT_PACKAGE_JSON" "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|" +update_package_lock_version "$PACKAGE_LOCK_JSON" +update_agents_version "$ROOT_AGENTS_MD" "Version" +update_agents_version "$TR_AGENTS_MD" "Sürüm" +update_agents_version "$ZH_CN_AGENTS_MD" "版本" +update_agent_yaml_version +update_version_file update_version "$PLUGIN_JSON" "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|" update_version "$MARKETPLACE_JSON" "0,/\"version\": *\"[^\"]*\"/s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|" +update_codex_marketplace_version +update_version "$CODEX_PLUGIN_JSON" "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|" update_version "$OPENCODE_PACKAGE_JSON" "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|" +update_package_lock_version "$OPENCODE_PACKAGE_LOCK_JSON" +update_readme_version_row "$README_FILE" "Version" "Plugin" "Plugin" "Reference config" +update_readme_version_row "$ZH_CN_README_FILE" "版本" "插件" "插件" "参考配置" +update_selective_install_repo_version "$SELECTIVE_INSTALL_ARCHITECTURE_DOC" + +# Verify the bumped release surface is still internally consistent before +# writing a release commit, tag, or push. +echo "Verifying OpenCode build and npm pack payload..." +node scripts/build-opencode.js +node tests/scripts/build-opencode.test.js +node tests/plugin-manifest.test.js # Stage, commit, tag, and push -git add "$ROOT_PACKAGE_JSON" "$PLUGIN_JSON" "$MARKETPLACE_JSON" "$OPENCODE_PACKAGE_JSON" +git add "$ROOT_PACKAGE_JSON" "$PACKAGE_LOCK_JSON" "$ROOT_AGENTS_MD" "$TR_AGENTS_MD" "$ZH_CN_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" "$ZH_CN_README_FILE" "$SELECTIVE_INSTALL_ARCHITECTURE_DOC" git commit -m "chore: bump plugin version to $VERSION" git tag "v$VERSION" git push origin main "v$VERSION" diff --git a/tests/lib/install-state.test.js b/tests/lib/install-state.test.js index 1e21018e..01011f6a 100644 --- a/tests/lib/install-state.test.js +++ b/tests/lib/install-state.test.js @@ -6,6 +6,9 @@ const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); +const CURRENT_PACKAGE_VERSION = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8') +).version; const { createInstallState, @@ -66,7 +69,7 @@ function runTests() { }, ], source: { - repoVersion: '1.10.0', + repoVersion: CURRENT_PACKAGE_VERSION, repoCommit: 'abc123', manifestVersion: 1, }, @@ -100,7 +103,7 @@ function runTests() { }, operations: [], source: { - repoVersion: '1.10.0', + repoVersion: CURRENT_PACKAGE_VERSION, repoCommit: 'abc123', manifestVersion: 1, }, @@ -154,7 +157,7 @@ function runTests() { }, operations: [operation], source: { - repoVersion: '1.10.0', + repoVersion: CURRENT_PACKAGE_VERSION, repoCommit: 'abc123', manifestVersion: 1, }, @@ -208,7 +211,7 @@ function runTests() { skippedModules: [], }, source: { - repoVersion: '1.10.0', + repoVersion: CURRENT_PACKAGE_VERSION, repoCommit: 'abc123', manifestVersion: 1, }, diff --git a/tests/lib/resolve-ecc-root.test.js b/tests/lib/resolve-ecc-root.test.js index 628e53a9..2d74b2d4 100644 --- a/tests/lib/resolve-ecc-root.test.js +++ b/tests/lib/resolve-ecc-root.test.js @@ -13,6 +13,9 @@ const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); +const CURRENT_PACKAGE_VERSION = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8') +).version; const { resolveEccRoot, INLINE_RESOLVE } = require('../../scripts/lib/resolve-ecc-root'); @@ -181,7 +184,7 @@ function runTests() { const homeDir = createTempDir(); try { const expected = setupLegacyPluginInstall(homeDir, ['marketplace', 'ecc']); - setupPluginCache(homeDir, 'ecc', 'affaan-m', '1.10.0'); + setupPluginCache(homeDir, 'ecc', 'affaan-m', CURRENT_PACKAGE_VERSION); const result = resolveEccRoot({ envRoot: '', homeDir }); assert.strictEqual(result, expected); } finally { @@ -193,7 +196,7 @@ function runTests() { if (test('discovers plugin root from cache directory', () => { const homeDir = createTempDir(); try { - const expected = setupPluginCache(homeDir, 'ecc', 'affaan-m', '1.10.0'); + const expected = setupPluginCache(homeDir, 'ecc', 'affaan-m', CURRENT_PACKAGE_VERSION); const result = resolveEccRoot({ envRoot: '', homeDir }); assert.strictEqual(result, expected); } finally { @@ -205,7 +208,7 @@ function runTests() { const homeDir = createTempDir(); try { const claudeDir = setupStandardInstall(homeDir); - setupPluginCache(homeDir, 'ecc', 'affaan-m', '1.10.0'); + setupPluginCache(homeDir, 'ecc', 'affaan-m', CURRENT_PACKAGE_VERSION); const result = resolveEccRoot({ envRoot: '', homeDir }); assert.strictEqual(result, claudeDir, 'Standard install should take precedence over plugin cache'); @@ -218,7 +221,7 @@ function runTests() { const homeDir = createTempDir(); try { setupPluginCache(homeDir, 'everything-claude-code', 'legacy-org', '1.7.0'); - const expected = setupPluginCache(homeDir, 'ecc', 'affaan-m', '1.10.0'); + const expected = setupPluginCache(homeDir, 'ecc', 'affaan-m', CURRENT_PACKAGE_VERSION); const result = resolveEccRoot({ envRoot: '', homeDir }); // Should find one of them (either is valid) assert.ok( @@ -311,7 +314,7 @@ function runTests() { if (test('INLINE_RESOLVE discovers plugin cache when env var is unset', () => { const homeDir = createTempDir(); try { - const expected = setupPluginCache(homeDir, 'ecc', 'affaan-m', '1.10.0'); + const expected = setupPluginCache(homeDir, 'ecc', 'affaan-m', CURRENT_PACKAGE_VERSION); const { execFileSync } = require('child_process'); const result = execFileSync('node', [ '-e', `console.log(${INLINE_RESOLVE})`, diff --git a/tests/plugin-manifest.test.js b/tests/plugin-manifest.test.js index 0fa99946..b779f382 100644 --- a/tests/plugin-manifest.test.js +++ b/tests/plugin-manifest.test.js @@ -20,6 +20,20 @@ const path = require('path'); const repoRoot = path.resolve(__dirname, '..'); const repoRootWithSep = `${repoRoot}${path.sep}`; +const packageJsonPath = path.join(repoRoot, 'package.json'); +const packageLockPath = path.join(repoRoot, 'package-lock.json'); +const rootAgentsPath = path.join(repoRoot, 'AGENTS.md'); +const trAgentsPath = path.join(repoRoot, 'docs', 'tr', 'AGENTS.md'); +const zhCnAgentsPath = path.join(repoRoot, 'docs', 'zh-CN', 'AGENTS.md'); +const ptBrReadmePath = path.join(repoRoot, 'docs', 'pt-BR', 'README.md'); +const trReadmePath = path.join(repoRoot, 'docs', 'tr', 'README.md'); +const rootZhCnReadmePath = path.join(repoRoot, 'README.zh-CN.md'); +const agentYamlPath = path.join(repoRoot, 'agent.yaml'); +const versionFilePath = path.join(repoRoot, 'VERSION'); +const zhCnReadmePath = path.join(repoRoot, 'docs', 'zh-CN', 'README.md'); +const selectiveInstallArchitecturePath = path.join(repoRoot, 'docs', 'SELECTIVE-INSTALL-ARCHITECTURE.md'); +const opencodePackageJsonPath = path.join(repoRoot, '.opencode', 'package.json'); +const opencodePackageLockPath = path.join(repoRoot, '.opencode', 'package-lock.json'); let passed = 0; let failed = 0; @@ -64,6 +78,86 @@ function assertSafeRepoRelativePath(relativePath, label) { ); } +const rootPackage = loadJsonObject(packageJsonPath, 'package.json'); +const packageLock = loadJsonObject(packageLockPath, 'package-lock.json'); +const opencodePackageLock = loadJsonObject(opencodePackageLockPath, '.opencode/package-lock.json'); +const expectedVersion = rootPackage.version; + +test('package.json has version field', () => { + assert.ok(expectedVersion, 'Expected package.json version field'); +}); + +test('package-lock.json root version matches package.json', () => { + assert.strictEqual(packageLock.version, expectedVersion); + assert.ok(packageLock.packages && packageLock.packages[''], 'Expected package-lock root package entry'); + assert.strictEqual(packageLock.packages[''].version, expectedVersion); +}); + +test('AGENTS.md version line matches package.json', () => { + const agentsSource = fs.readFileSync(rootAgentsPath, 'utf8'); + const match = agentsSource.match(/^\*\*Version:\*\* ([0-9]+\.[0-9]+\.[0-9]+)$/m); + assert.ok(match, 'Expected AGENTS.md to declare a top-level version line'); + assert.strictEqual(match[1], expectedVersion); +}); + +test('docs/tr/AGENTS.md version line matches package.json', () => { + const agentsSource = fs.readFileSync(trAgentsPath, 'utf8'); + const match = agentsSource.match(/^\*\*Sürüm:\*\* ([0-9]+\.[0-9]+\.[0-9]+)$/m); + assert.ok(match, 'Expected docs/tr/AGENTS.md to declare a top-level version line'); + assert.strictEqual(match[1], expectedVersion); +}); + +test('docs/zh-CN/AGENTS.md version line matches package.json', () => { + const agentsSource = fs.readFileSync(zhCnAgentsPath, 'utf8'); + const match = agentsSource.match(/^\*\*版本:\*\* ([0-9]+\.[0-9]+\.[0-9]+)$/m); + assert.ok(match, 'Expected docs/zh-CN/AGENTS.md to declare a top-level version line'); + assert.strictEqual(match[1], expectedVersion); +}); + +test('agent.yaml version matches package.json', () => { + const agentYamlSource = fs.readFileSync(agentYamlPath, 'utf8'); + const match = agentYamlSource.match(/^version:\s*([0-9]+\.[0-9]+\.[0-9]+)$/m); + assert.ok(match, 'Expected agent.yaml to declare a top-level version field'); + assert.strictEqual(match[1], expectedVersion); +}); + +test('VERSION file matches package.json', () => { + const versionFile = fs.readFileSync(versionFilePath, 'utf8').trim(); + assert.ok(versionFile, 'Expected VERSION file to be non-empty'); + assert.strictEqual(versionFile, expectedVersion); +}); + +test('docs/SELECTIVE-INSTALL-ARCHITECTURE.md repoVersion example matches package.json', () => { + const source = fs.readFileSync(selectiveInstallArchitecturePath, 'utf8'); + const match = source.match(/"repoVersion":\s*"([0-9]+\.[0-9]+\.[0-9]+)"/); + assert.ok(match, 'Expected docs/SELECTIVE-INSTALL-ARCHITECTURE.md to declare a repoVersion example'); + assert.strictEqual(match[1], expectedVersion); +}); + +test('docs/pt-BR/README.md latest release heading matches package.json', () => { + const source = fs.readFileSync(ptBrReadmePath, 'utf8'); + assert.ok( + source.includes(`### v${expectedVersion} `), + 'Expected docs/pt-BR/README.md to advertise the current release heading', + ); +}); + +test('docs/tr/README.md latest release heading matches package.json', () => { + const source = fs.readFileSync(trReadmePath, 'utf8'); + assert.ok( + source.includes(`### v${expectedVersion} `), + 'Expected docs/tr/README.md to advertise the current release heading', + ); +}); + +test('README.zh-CN.md latest release heading matches package.json', () => { + const source = fs.readFileSync(rootZhCnReadmePath, 'utf8'); + assert.ok( + source.includes(`### v${expectedVersion} `), + 'Expected README.zh-CN.md to advertise the current release heading', + ); +}); + // ── Claude plugin manifest ──────────────────────────────────────────────────── console.log('\n=== .claude-plugin/plugin.json ===\n'); @@ -80,6 +174,10 @@ test('claude plugin.json has version field', () => { assert.ok(claudePlugin.version, 'Expected version field'); }); +test('claude plugin.json version matches package.json', () => { + assert.strictEqual(claudePlugin.version, expectedVersion); +}); + test('claude plugin.json uses short plugin slug', () => { assert.strictEqual(claudePlugin.name, 'ecc'); }); @@ -156,6 +254,10 @@ test('claude marketplace.json has plugins array with a short ecc plugin entry', assert.strictEqual(claudeMarketplace.plugins[0].name, 'ecc'); }); +test('claude marketplace.json plugin version matches package.json', () => { + assert.strictEqual(claudeMarketplace.plugins[0].version, expectedVersion); +}); + // ── Codex plugin manifest ───────────────────────────────────────────────────── // Per official docs: https://platform.openai.com/docs/codex/plugins // - .codex-plugin/plugin.json is the required manifest @@ -183,6 +285,10 @@ test('codex plugin.json has version field', () => { assert.ok(codexPlugin.version, 'Expected version field'); }); +test('codex plugin.json version matches package.json', () => { + assert.strictEqual(codexPlugin.version, expectedVersion); +}); + test('codex plugin.json skills is a string (not array) per official spec', () => { assert.strictEqual( typeof codexPlugin.skills, @@ -268,6 +374,7 @@ test('marketplace.json exists at .agents/plugins/', () => { }); const marketplace = loadJsonObject(marketplacePath, '.agents/plugins/marketplace.json'); +const opencodePackage = loadJsonObject(opencodePackageJsonPath, '.opencode/package.json'); test('marketplace.json has name field', () => { assert.ok(marketplace.name, 'Expected name field'); @@ -284,6 +391,7 @@ test('marketplace.json has plugins array with at least one entry', () => { test('marketplace.json plugin entries have required fields', () => { for (const plugin of marketplace.plugins) { assert.ok(plugin.name, `Plugin entry missing name`); + assert.ok(plugin.version, `Plugin "${plugin.name}" missing version`); assert.ok(plugin.source && plugin.source.source, `Plugin "${plugin.name}" missing source.source`); assert.ok(plugin.policy && plugin.policy.installation, `Plugin "${plugin.name}" missing policy.installation`); assert.ok(plugin.category, `Plugin "${plugin.name}" missing category`); @@ -294,6 +402,10 @@ test('marketplace.json plugin entry uses short plugin slug', () => { assert.strictEqual(marketplace.plugins[0].name, 'ecc'); }); +test('marketplace.json plugin version matches package.json', () => { + assert.strictEqual(marketplace.plugins[0].version, expectedVersion); +}); + test('marketplace local plugin path resolves to the repo-root Codex bundle', () => { for (const plugin of marketplace.plugins) { if (!plugin.source || plugin.source.source !== 'local') { @@ -317,6 +429,30 @@ test('marketplace local plugin path resolves to the repo-root Codex bundle', () } }); +test('.opencode/package.json version matches package.json', () => { + assert.strictEqual(opencodePackage.version, expectedVersion); +}); + +test('.opencode/package-lock.json root version matches package.json', () => { + assert.strictEqual(opencodePackageLock.version, expectedVersion); + assert.ok(opencodePackageLock.packages && opencodePackageLock.packages[''], 'Expected .opencode/package-lock root package entry'); + assert.strictEqual(opencodePackageLock.packages[''].version, expectedVersion); +}); + +test('README version row matches package.json', () => { + const readme = fs.readFileSync(path.join(repoRoot, 'README.md'), 'utf8'); + const match = readme.match(/^\| \*\*Version\*\* \| Plugin \| Plugin \| Reference config \| ([0-9][0-9.]*) \|$/m); + assert.ok(match, 'Expected README version summary row'); + assert.strictEqual(match[1], expectedVersion); +}); + +test('docs/zh-CN/README.md version row matches package.json', () => { + const readme = fs.readFileSync(zhCnReadmePath, 'utf8'); + const match = readme.match(/^\| \*\*版本\*\* \| 插件 \| 插件 \| 参考配置 \| ([0-9][0-9.]*) \|$/m); + assert.ok(match, 'Expected docs/zh-CN/README.md version summary row'); + assert.strictEqual(match[1], expectedVersion); +}); + // ── Summary ─────────────────────────────────────────────────────────────────── console.log(`\nPassed: ${passed}`); console.log(`Failed: ${failed}`); diff --git a/tests/scripts/build-opencode.test.js b/tests/scripts/build-opencode.test.js index 253d16e7..5d2b5d7b 100644 --- a/tests/scripts/build-opencode.test.js +++ b/tests/scripts/build-opencode.test.js @@ -68,6 +68,42 @@ function main() { packagedPaths.has(".opencode/dist/tools/index.js"), "npm pack should include compiled OpenCode tool output" ) + assert.ok( + packagedPaths.has(".claude-plugin/marketplace.json"), + "npm pack should include .claude-plugin/marketplace.json" + ) + assert.ok( + packagedPaths.has(".claude-plugin/plugin.json"), + "npm pack should include .claude-plugin/plugin.json" + ) + assert.ok( + packagedPaths.has(".codex-plugin/plugin.json"), + "npm pack should include .codex-plugin/plugin.json" + ) + assert.ok( + packagedPaths.has(".agents/plugins/marketplace.json"), + "npm pack should include .agents/plugins/marketplace.json" + ) + assert.ok( + packagedPaths.has(".opencode/package.json"), + "npm pack should include .opencode/package.json" + ) + assert.ok( + packagedPaths.has(".opencode/package-lock.json"), + "npm pack should include .opencode/package-lock.json" + ) + assert.ok( + packagedPaths.has("agent.yaml"), + "npm pack should include agent.yaml" + ) + assert.ok( + packagedPaths.has("AGENTS.md"), + "npm pack should include AGENTS.md" + ) + assert.ok( + packagedPaths.has("VERSION"), + "npm pack should include VERSION" + ) }], ] diff --git a/tests/scripts/release.test.js b/tests/scripts/release.test.js new file mode 100644 index 00000000..299f5352 --- /dev/null +++ b/tests/scripts/release.test.js @@ -0,0 +1,71 @@ +/** + * 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.lastIndexOf('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++; + + if (test('release script verifies npm pack payload after version updates and before commit/tag', () => { + const updateIndex = source.indexOf('update_version "$ROOT_PACKAGE_JSON"'); + const packCheckIndex = source.indexOf('node tests/scripts/build-opencode.test.js'); + const commitIndex = source.indexOf('git commit -m "chore: bump plugin version to $VERSION"'); + + assert.ok(updateIndex >= 0, 'release.sh should update package version fields'); + assert.ok(packCheckIndex >= 0, 'release.sh should run build-opencode.test.js'); + assert.ok(commitIndex >= 0, 'release.sh should create the release commit'); + assert.ok( + updateIndex < packCheckIndex, + 'build-opencode.test.js should run after versioned files are updated' + ); + assert.ok( + packCheckIndex < commitIndex, + 'build-opencode.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();