mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-29 13:33:31 +08:00
feat(ecc2): finalize rc1 release surface
This commit is contained in:
120
tests/docs/ecc2-release-surface.test.js
Normal file
120
tests/docs/ecc2-release-surface.test.js
Normal file
@@ -0,0 +1,120 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||
const releaseDir = path.join(repoRoot, 'docs', 'releases', '2.0.0-rc.1');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
passed++;
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
function walkMarkdown(rootPath) {
|
||||
const files = [];
|
||||
for (const entry of fs.readdirSync(rootPath, { withFileTypes: true })) {
|
||||
const nextPath = path.join(rootPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...walkMarkdown(nextPath));
|
||||
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
files.push(nextPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
console.log('\n=== Testing ECC 2.0 release surface ===\n');
|
||||
|
||||
const expectedReleaseFiles = [
|
||||
'release-notes.md',
|
||||
'x-thread.md',
|
||||
'linkedin-post.md',
|
||||
'article-outline.md',
|
||||
'launch-checklist.md',
|
||||
'telegram-handoff.md',
|
||||
'demo-prompts.md',
|
||||
];
|
||||
|
||||
test('release candidate directory includes the public launch pack', () => {
|
||||
for (const fileName of expectedReleaseFiles) {
|
||||
assert.ok(fs.existsSync(path.join(releaseDir, fileName)), `Missing ${fileName}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('README links to Hermes setup and rc.1 release notes', () => {
|
||||
const readme = read('README.md');
|
||||
assert.ok(readme.includes('docs/HERMES-SETUP.md'), 'README must link to Hermes setup');
|
||||
assert.ok(readme.includes('docs/releases/2.0.0-rc.1/release-notes.md'), 'README must link to rc.1 release notes');
|
||||
});
|
||||
|
||||
test('cross-harness architecture doc exists and names core harnesses', () => {
|
||||
const source = read('docs/architecture/cross-harness.md');
|
||||
for (const harness of ['Claude Code', 'Codex', 'OpenCode', 'Cursor', 'Gemini', 'Hermes']) {
|
||||
assert.ok(source.includes(harness), `Expected cross-harness doc to mention ${harness}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('Hermes import skill exists and declares sanitization rules', () => {
|
||||
const source = read('skills/hermes-imports/SKILL.md');
|
||||
assert.ok(source.includes('name: hermes-imports'));
|
||||
assert.ok(source.includes('Sanitization Checklist'));
|
||||
assert.ok(source.includes('Do not ship raw workspace exports'));
|
||||
});
|
||||
|
||||
test('release docs do not contain private local workspace paths', () => {
|
||||
const offenders = [];
|
||||
for (const filePath of walkMarkdown(releaseDir)) {
|
||||
const source = fs.readFileSync(filePath, 'utf8');
|
||||
if (source.includes('/Users/') || source.includes('/.hermes/')) {
|
||||
offenders.push(path.relative(repoRoot, filePath));
|
||||
}
|
||||
}
|
||||
assert.deepStrictEqual(offenders, []);
|
||||
});
|
||||
|
||||
test('release docs do not contain unresolved public-link placeholders', () => {
|
||||
const offenders = [];
|
||||
for (const filePath of walkMarkdown(releaseDir)) {
|
||||
const source = fs.readFileSync(filePath, 'utf8');
|
||||
if (source.includes('<repo-link>')) {
|
||||
offenders.push(path.relative(repoRoot, filePath));
|
||||
}
|
||||
}
|
||||
assert.deepStrictEqual(offenders, []);
|
||||
});
|
||||
|
||||
test('Hermes setup uses release-candidate wording for the rc.1 surface', () => {
|
||||
const source = read('docs/HERMES-SETUP.md');
|
||||
assert.ok(source.includes('Public Release Candidate Scope'));
|
||||
assert.ok(source.includes('ECC v2.0.0-rc.1 documents the Hermes surface'));
|
||||
assert.ok(!source.includes('Public Preview Scope'));
|
||||
});
|
||||
|
||||
test('release docs preserve the ECC/Hermes boundary', () => {
|
||||
const releaseNotes = read('docs/releases/2.0.0-rc.1/release-notes.md');
|
||||
assert.ok(releaseNotes.includes('ECC is the reusable substrate'));
|
||||
assert.ok(releaseNotes.includes('Hermes as the operator shell'));
|
||||
});
|
||||
|
||||
if (failed > 0) {
|
||||
console.log(`\nFailed: ${failed}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
@@ -35,6 +35,7 @@ const selectiveInstallArchitecturePath = path.join(repoRoot, 'docs', 'SELECTIVE-
|
||||
const opencodePackageJsonPath = path.join(repoRoot, '.opencode', 'package.json');
|
||||
const opencodePackageLockPath = path.join(repoRoot, '.opencode', 'package-lock.json');
|
||||
const opencodeHooksPluginPath = path.join(repoRoot, '.opencode', 'plugins', 'ecc-hooks.ts');
|
||||
const semverPattern = '[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?';
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
@@ -118,28 +119,28 @@ test('package-lock.json root version matches package.json', () => {
|
||||
|
||||
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);
|
||||
const match = agentsSource.match(new RegExp(`^\\*\\*Version:\\*\\* (${semverPattern})$`, '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);
|
||||
const match = agentsSource.match(new RegExp(`^\\*\\*Sürüm:\\*\\* (${semverPattern})$`, '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);
|
||||
const match = agentsSource.match(new RegExp(`^\\*\\*版本:\\*\\* (${semverPattern})$`, '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);
|
||||
const match = agentYamlSource.match(new RegExp(`^version:\\s*(${semverPattern})$`, 'm'));
|
||||
assert.ok(match, 'Expected agent.yaml to declare a top-level version field');
|
||||
assert.strictEqual(match[1], expectedVersion);
|
||||
});
|
||||
@@ -152,14 +153,14 @@ test('VERSION file matches package.json', () => {
|
||||
|
||||
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]+)"/);
|
||||
const match = source.match(new RegExp(`"repoVersion":\\s*"(${semverPattern})"`));
|
||||
assert.ok(match, 'Expected docs/SELECTIVE-INSTALL-ARCHITECTURE.md to declare a repoVersion example');
|
||||
assert.strictEqual(match[1], expectedVersion);
|
||||
});
|
||||
|
||||
test('.opencode/plugins/ecc-hooks.ts active plugin banner matches package.json', () => {
|
||||
const source = fs.readFileSync(opencodeHooksPluginPath, 'utf8');
|
||||
const match = source.match(/## Active Plugin: Everything Claude Code v([0-9]+\.[0-9]+\.[0-9]+)/);
|
||||
const match = source.match(new RegExp(`## Active Plugin: Everything Claude Code v(${semverPattern})`));
|
||||
assert.ok(match, 'Expected .opencode/plugins/ecc-hooks.ts to declare an active plugin banner');
|
||||
assert.strictEqual(match[1], expectedVersion);
|
||||
});
|
||||
@@ -445,7 +446,7 @@ test('.opencode/package-lock.json root version matches package.json', () => {
|
||||
|
||||
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);
|
||||
const match = readme.match(new RegExp(`^\\| \\*\\*Version\\*\\* \\| Plugin \\| Plugin \\| Reference config \\| (${semverPattern}) \\|$`, 'm'));
|
||||
assert.ok(match, 'Expected README version summary row');
|
||||
assert.strictEqual(match[1], expectedVersion);
|
||||
});
|
||||
@@ -497,7 +498,7 @@ test('user-facing docs do not use the legacy non-URL marketplace add form', () =
|
||||
|
||||
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);
|
||||
const match = readme.match(new RegExp(`^\\| \\*\\*版本\\*\\* \\| 插件 \\| 插件 \\| 参考配置 \\| (${semverPattern}) \\|$`, 'm'));
|
||||
assert.ok(match, 'Expected docs/zh-CN/README.md version summary row');
|
||||
assert.strictEqual(match[1], expectedVersion);
|
||||
});
|
||||
|
||||
@@ -50,6 +50,18 @@ for (const workflow of [
|
||||
assert.match(content, /npm publish --access public --provenance/);
|
||||
assert.match(content, /NODE_AUTH_TOKEN:\s*\$\{\{\s*secrets\.NPM_TOKEN\s*\}\}/);
|
||||
});
|
||||
|
||||
test(`${workflow} creates the GitHub Release before publishing to npm`, () => {
|
||||
const releaseIndex = content.indexOf('name: Create GitHub Release');
|
||||
const publishIndex = content.indexOf('name: Publish npm package');
|
||||
|
||||
assert.ok(releaseIndex >= 0, `${workflow} should create a GitHub Release`);
|
||||
assert.ok(publishIndex >= 0, `${workflow} should publish the npm package`);
|
||||
assert.ok(
|
||||
releaseIndex < publishIndex,
|
||||
`${workflow} should not publish to npm until GitHub Release creation has succeeded`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (failed > 0) {
|
||||
|
||||
@@ -8,6 +8,19 @@ const path = require('path');
|
||||
|
||||
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'release.sh');
|
||||
const source = fs.readFileSync(scriptPath, 'utf8');
|
||||
const releaseWorkflowPath = path.join(__dirname, '..', '..', '.github', 'workflows', 'release.yml');
|
||||
const reusableReleaseWorkflowPath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'.github',
|
||||
'workflows',
|
||||
'reusable-release.yml'
|
||||
);
|
||||
const ciWorkflowPath = path.join(__dirname, '..', '..', '.github', 'workflows', 'ci.yml');
|
||||
const releaseWorkflowSource = fs.readFileSync(releaseWorkflowPath, 'utf8');
|
||||
const reusableReleaseWorkflowSource = fs.readFileSync(reusableReleaseWorkflowPath, 'utf8');
|
||||
const ciWorkflowSource = fs.readFileSync(ciWorkflowPath, 'utf8');
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
@@ -64,6 +77,71 @@ function runTests() {
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('release script supports prerelease semver and release heading sync', () => {
|
||||
assert.ok(
|
||||
source.includes('2.0.0-rc.1'),
|
||||
'release.sh should document an accepted prerelease semver example'
|
||||
);
|
||||
assert.ok(
|
||||
source.includes('(-[0-9A-Za-z.-]+)?'),
|
||||
'release.sh should allow prerelease semver suffixes'
|
||||
);
|
||||
assert.ok(
|
||||
source.includes('update_latest_release_heading "$ROOT_ZH_CN_README_FILE"'),
|
||||
'release.sh should update localized latest-release headings that plugin-manifest.test.js verifies'
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('release workflows mark prerelease tags as GitHub prereleases', () => {
|
||||
assert.ok(
|
||||
releaseWorkflowSource.includes('prerelease: ${{ contains(github.ref_name, \'-\') }}'),
|
||||
'release.yml should mark hyphenated tag pushes as GitHub prereleases'
|
||||
);
|
||||
assert.ok(
|
||||
releaseWorkflowSource.includes('make_latest: ${{ contains(github.ref_name, \'-\') && \'false\' || \'true\' }}'),
|
||||
'release.yml should avoid making hyphenated prereleases the latest GitHub release'
|
||||
);
|
||||
assert.ok(
|
||||
reusableReleaseWorkflowSource.includes('prerelease: ${{ contains(inputs.tag, \'-\') }}'),
|
||||
'reusable-release.yml should mark hyphenated manual tags as GitHub prereleases'
|
||||
);
|
||||
assert.ok(
|
||||
reusableReleaseWorkflowSource.includes('make_latest: ${{ contains(inputs.tag, \'-\') && \'false\' || \'true\' }}'),
|
||||
'reusable-release.yml should avoid making hyphenated prereleases the latest GitHub release'
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('reusable release checks out the requested tag before validating and publishing', () => {
|
||||
const checkoutIndex = reusableReleaseWorkflowSource.indexOf('uses: actions/checkout@');
|
||||
const refIndex = reusableReleaseWorkflowSource.indexOf('ref: ${{ inputs.tag }}');
|
||||
const validateIndex = reusableReleaseWorkflowSource.indexOf('name: Validate version tag');
|
||||
|
||||
assert.ok(checkoutIndex >= 0, 'reusable-release.yml should check out repository content');
|
||||
assert.ok(refIndex >= 0, 'reusable-release.yml checkout should use inputs.tag as ref');
|
||||
assert.ok(validateIndex >= 0, 'reusable-release.yml should validate requested tag');
|
||||
assert.ok(
|
||||
checkoutIndex < refIndex && refIndex < validateIndex,
|
||||
'reusable release should check out inputs.tag before tag validation and publish steps'
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('CI runs for release branches and version tags before release workflows execute', () => {
|
||||
const pushBlockMatch = ciWorkflowSource.match(/on:\n\s+push:\n([\s\S]*?)\n\s+pull_request:/);
|
||||
const pushBlock = pushBlockMatch ? pushBlockMatch[1] : '';
|
||||
|
||||
assert.ok(pushBlock, 'ci.yml should define a push trigger block');
|
||||
assert.match(
|
||||
pushBlock,
|
||||
/branches:\s*\[[^\]]*main[^\]]*['"]release\/\*\*['"][^\]]*\]/,
|
||||
'ci.yml push branches should include release/**'
|
||||
);
|
||||
assert.match(
|
||||
pushBlock,
|
||||
/tags:\s*\[[^\]]*['"]v\*['"][^\]]*\]/,
|
||||
'ci.yml push tags should include v*'
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user