mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-07 08:35:54 +00:00
fix: reserve Grok snapshot equality for releases
This commit is contained in:
1 parent
5819419150
commit
08f5e15aaf
9 files changed
+162
-17
No files matched your search
@@ -104,7 +104,7 @@ jobs:
|
||||
run: node tests/plugin-manifest.test.js
|
||||
|
||||
- name: Validate Grok marketplace source pin
|
||||
run: node scripts/ci/validate-grok-pin.js
|
||||
run: node scripts/ci/validate-grok-pin.js --release
|
||||
|
||||
- name: Check npm publish state
|
||||
id: npm_publish_state
|
||||
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
run: node tests/plugin-manifest.test.js
|
||||
|
||||
- name: Validate Grok marketplace source pin
|
||||
run: node scripts/ci/validate-grok-pin.js
|
||||
run: node scripts/ci/validate-grok-pin.js --release
|
||||
|
||||
- name: Check npm publish state
|
||||
id: npm_publish_state
|
||||
|
||||
@@ -25,6 +25,13 @@ node scripts/uninstall.js --target grok
|
||||
rollback use the same canonical apply path; pass `--source-sha <40-hex>` to
|
||||
reapply a previous pinned commit.
|
||||
|
||||
The marketplace pin advances with releases, not every source commit. Ordinary
|
||||
CI validates the SHA and its ancestry. `scripts/release.sh` commits the release
|
||||
metadata, pins that source commit in a separate catalog commit, and validates
|
||||
the result before tagging. Release workflows also run
|
||||
`node scripts/ci/validate-grok-pin.js --release` to reject unpublished source
|
||||
changes after the pinned snapshot.
|
||||
|
||||
Grok CLI marketplace add / install / enable is **discovery only**.
|
||||
`grok plugin install --trust` skips Grok's confirmation prompt; it is **not
|
||||
ECC capability consent** and does not write an ECC receipt. `plugin.json`
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{
|
||||
"name": "ecc",
|
||||
"description": "Harness-native ECC operator layer - 68 agents, 286 skills, 94 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Grok Build, Claude Code, Codex, OpenCode, Cursor, and related agent harnesses",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
"email": "me@affaanmustafa.com"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ecc",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"description": "Harness-native ECC plugin for engineering teams - 68 agents, 286 skills, 94 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Grok Build plus adjacent agent harnesses",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
|
||||
@@ -14,7 +14,7 @@ function git(root, args) {
|
||||
}).trim();
|
||||
}
|
||||
|
||||
function validateGrokPin(root = path.resolve(__dirname, '../..')) {
|
||||
function validateGrokPin(root = path.resolve(__dirname, '../..'), { release = false } = {}) {
|
||||
const marketplace = JSON.parse(fs.readFileSync(path.join(root, MARKETPLACE_PATH), 'utf8'));
|
||||
const source = marketplace.plugins && marketplace.plugins[0] && marketplace.plugins[0].source;
|
||||
const sha = source && source.sha;
|
||||
@@ -30,18 +30,28 @@ function validateGrokPin(root = path.resolve(__dirname, '../..')) {
|
||||
} catch {
|
||||
throw new Error(`Grok marketplace pin ${sha} is not an ancestor of HEAD ${head}`);
|
||||
}
|
||||
try {
|
||||
git(root, ['diff', '--quiet', sha, 'HEAD', '--', '.', `:(exclude)${MARKETPLACE_PATH}`]);
|
||||
} catch {
|
||||
throw new Error(`Grok marketplace pin ${sha} is stale; source changes exist after the pinned commit`);
|
||||
// Development retains the last pinned snapshot; releases must include all source changes.
|
||||
if (release) {
|
||||
try {
|
||||
git(root, ['diff', '--quiet', sha, 'HEAD', '--', '.', `:(exclude)${MARKETPLACE_PATH}`]);
|
||||
} catch {
|
||||
throw new Error(`Grok marketplace pin ${sha} is stale; source changes exist after the pinned commit`);
|
||||
}
|
||||
}
|
||||
return { sha, head };
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
const { sha, head } = validateGrokPin();
|
||||
console.log(`Grok marketplace pin ${sha} matches source at ${head}`);
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length > 1 || (args.length === 1 && args[0] !== '--release')) {
|
||||
throw new Error('Usage: node scripts/ci/validate-grok-pin.js [--release]');
|
||||
}
|
||||
const release = args[0] === '--release';
|
||||
const { sha, head } = validateGrokPin(undefined, { release });
|
||||
console.log(release
|
||||
? `Grok marketplace pin ${sha} matches source at ${head}`
|
||||
: `Grok marketplace pin ${sha} is a valid ancestor of ${head}`);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
|
||||
+2
-1
@@ -354,9 +354,10 @@ node -e '
|
||||
plugin.source.sha = sha;
|
||||
fs.writeFileSync(file, `${JSON.stringify(marketplace, null, 2)}\n`);
|
||||
' "$GROK_MARKETPLACE_JSON" "$GROK_SOURCE_SHA"
|
||||
node scripts/ci/validate-grok-pin.js
|
||||
node scripts/ci/validate-grok-pin.js --release
|
||||
git add "$GROK_MARKETPLACE_JSON"
|
||||
git commit -m "chore: pin Grok marketplace to release source"
|
||||
node scripts/ci/validate-grok-pin.js --release
|
||||
git tag "v$VERSION"
|
||||
git push origin main "v$VERSION"
|
||||
|
||||
|
||||
+116
-4
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { execFileSync, spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
@@ -16,7 +16,7 @@ function writeMarketplace(root, sha) {
|
||||
const directory = path.join(root, '.grok-plugin');
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
fs.writeFileSync(path.join(directory, 'marketplace.json'), JSON.stringify({
|
||||
plugins: [{ source: { sha } }],
|
||||
plugins: [{ name: 'ecc', source: { sha } }],
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ function createFixture() {
|
||||
git(root, 'init', '--quiet');
|
||||
git(root, 'config', 'user.name', 'ECC test');
|
||||
git(root, 'config', 'user.email', 'ecc-test@example.invalid');
|
||||
const validatorPath = path.join(root, 'scripts/ci/validate-grok-pin.js');
|
||||
fs.mkdirSync(path.dirname(validatorPath), { recursive: true });
|
||||
fs.copyFileSync(path.resolve(__dirname, '../../scripts/ci/validate-grok-pin.js'), validatorPath);
|
||||
fs.writeFileSync(path.join(root, 'payload.txt'), 'verified source\n');
|
||||
writeMarketplace(root, '0'.repeat(40));
|
||||
git(root, 'add', '.');
|
||||
@@ -57,22 +60,131 @@ test('accepts a pin commit that differs only by the pin update', () => {
|
||||
const fixture = createFixture();
|
||||
try {
|
||||
assert.strictEqual(validateGrokPin(fixture.root).sha, fixture.sha);
|
||||
assert.strictEqual(validateGrokPin(fixture.root, { release: true }).sha, fixture.sha);
|
||||
} finally {
|
||||
fs.rmSync(fixture.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects source changes after the pinned snapshot', () => {
|
||||
test('ordinary source commits keep a valid historical pin but cannot be released', () => {
|
||||
const fixture = createFixture();
|
||||
try {
|
||||
fs.writeFileSync(path.join(fixture.root, 'payload.txt'), 'changed source\n');
|
||||
git(fixture.root, 'add', 'payload.txt');
|
||||
git(fixture.root, 'commit', '--quiet', '-m', 'change source without repinning');
|
||||
assert.throws(() => validateGrokPin(fixture.root), /pin .* is stale/);
|
||||
assert.strictEqual(validateGrokPin(fixture.root).sha, fixture.sha);
|
||||
assert.throws(() => validateGrokPin(fixture.root, { release: true }), /pin .* is stale/);
|
||||
|
||||
const validator = path.join(fixture.root, 'scripts/ci/validate-grok-pin.js');
|
||||
const normal = spawnSync(process.execPath, [validator], { encoding: 'utf8' });
|
||||
assert.strictEqual(normal.status, 0, normal.stderr);
|
||||
assert.doesNotMatch(normal.stdout, /matches source/);
|
||||
const release = spawnSync(process.execPath, [validator, '--release'], { encoding: 'utf8' });
|
||||
assert.strictEqual(release.status, 1);
|
||||
assert.match(release.stderr, /pin .* is stale/);
|
||||
|
||||
const sourceSha = git(fixture.root, 'rev-parse', 'HEAD');
|
||||
const releaseScript = fs.readFileSync(path.resolve(__dirname, '../../scripts/release.sh'), 'utf8');
|
||||
const updater = releaseScript.slice(releaseScript.indexOf('GROK_SOURCE_SHA='))
|
||||
.match(/node -e '([\s\S]*?)' "\$GROK_MARKETPLACE_JSON" "\$GROK_SOURCE_SHA"/);
|
||||
assert.ok(updater, 'exercise the pin updater shipped in release.sh');
|
||||
const updated = spawnSync(process.execPath, ['-e', updater[1],
|
||||
path.join(fixture.root, '.grok-plugin/marketplace.json'), sourceSha,
|
||||
], { encoding: 'utf8' });
|
||||
assert.strictEqual(updated.status, 0, updated.stderr);
|
||||
git(fixture.root, 'add', '.');
|
||||
git(fixture.root, 'commit', '--quiet', '-m', 'pin updated source');
|
||||
assert.strictEqual(git(fixture.root, 'diff', '--name-only', 'HEAD^', 'HEAD'),
|
||||
'.grok-plugin/marketplace.json');
|
||||
assert.strictEqual(validateGrokPin(fixture.root, { release: true }).sha, sourceSha);
|
||||
const repinned = spawnSync(process.execPath, [validator, '--release'], { encoding: 'utf8' });
|
||||
assert.strictEqual(repinned.status, 0, repinned.stderr);
|
||||
assert.match(repinned.stdout, /matches source/);
|
||||
} finally {
|
||||
fs.rmSync(fixture.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('both modes reject malformed, missing, and non-ancestor pins', () => {
|
||||
const fixture = createFixture();
|
||||
try {
|
||||
for (const marketplace of [{}, { plugins: [] }, { plugins: [{}] }]) {
|
||||
fs.writeFileSync(path.join(fixture.root, '.grok-plugin/marketplace.json'), JSON.stringify(marketplace));
|
||||
for (const release of [false, true]) {
|
||||
assert.throws(() => validateGrokPin(fixture.root, { release }), /must pin a 40-character/);
|
||||
}
|
||||
}
|
||||
for (const sha of ['main', '0'.repeat(40)]) {
|
||||
writeMarketplace(fixture.root, sha);
|
||||
for (const release of [false, true]) {
|
||||
assert.throws(() => validateGrokPin(fixture.root, { release }));
|
||||
}
|
||||
}
|
||||
// Create a real commit that is not reachable from HEAD, without moving any refs.
|
||||
const tree = git(fixture.root, 'rev-parse', 'HEAD^{tree}');
|
||||
const unrelated = git(fixture.root, 'commit-tree', tree, '-m', 'unrelated history');
|
||||
writeMarketplace(fixture.root, unrelated);
|
||||
for (const release of [false, true]) {
|
||||
assert.throws(() => validateGrokPin(fixture.root, { release }), /not an ancestor/);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(fixture.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('CLI rejects unknown options instead of silently disabling release validation', () => {
|
||||
for (const args of [['--relase'], ['--release', 'unexpected']]) {
|
||||
const result = spawnSync(process.execPath, [
|
||||
path.resolve(__dirname, '../../scripts/ci/validate-grok-pin.js'), ...args,
|
||||
], { encoding: 'utf8' });
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.match(result.stderr, /Usage:/);
|
||||
}
|
||||
});
|
||||
|
||||
// release.sh is a POSIX entrypoint; the validator and updater tests above run on all platforms.
|
||||
if (process.platform !== 'win32') {
|
||||
for (const contaminate of [false, true]) {
|
||||
test(contaminate ? 'release blocks tagging when a commit hook changes source'
|
||||
: 'release script tags the verified pin-only commit', () => {
|
||||
const fixture = createFixture();
|
||||
try {
|
||||
fs.writeFileSync(path.join(fixture.root, 'payload.txt'), 'new release source\n');
|
||||
git(fixture.root, 'add', '.');
|
||||
git(fixture.root, 'commit', '--quiet', '-m', 'release source');
|
||||
const sourceSha = git(fixture.root, 'rev-parse', 'HEAD');
|
||||
if (contaminate) {
|
||||
fs.writeFileSync(path.join(fixture.root, '.git/hooks/pre-commit'),
|
||||
'#!/bin/sh\nprintf "unexpected source change\\n" > payload.txt\ngit add payload.txt\n',
|
||||
{ mode: 0o755 });
|
||||
}
|
||||
const source = fs.readFileSync(path.resolve(__dirname, '../../scripts/release.sh'), 'utf8');
|
||||
const start = source.indexOf('GROK_SOURCE_SHA=$(git rev-parse HEAD)');
|
||||
const end = source.indexOf('git push origin main', start);
|
||||
assert.ok(start >= 0 && end > start);
|
||||
const result = spawnSync('bash', ['-euo', 'pipefail'], {
|
||||
cwd: fixture.root,
|
||||
input: source.slice(start, end),
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, GROK_MARKETPLACE_JSON: '.grok-plugin/marketplace.json', VERSION: '9.9.9' },
|
||||
});
|
||||
if (contaminate) {
|
||||
assert.strictEqual(result.status, 1, result.stderr);
|
||||
assert.match(result.stderr, /pin .* is stale/);
|
||||
assert.strictEqual(git(fixture.root, 'tag', '--list'), '');
|
||||
} else {
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.strictEqual(validateGrokPin(fixture.root, { release: true }).sha, sourceSha);
|
||||
assert.strictEqual(git(fixture.root, 'rev-parse', 'v9.9.9'), git(fixture.root, 'rev-parse', 'HEAD'));
|
||||
assert.strictEqual(git(fixture.root, 'diff', '--name-only', sourceSha, 'HEAD'),
|
||||
'.grok-plugin/marketplace.json');
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(fixture.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
if (failed > 0) process.exitCode = 1;
|
||||
@@ -84,7 +84,7 @@ function runTests() {
|
||||
const releaseCommitIndex = source.indexOf('git commit -m "chore: bump plugin version to $VERSION"');
|
||||
const sourceShaIndex = source.indexOf('GROK_SOURCE_SHA=$(git rev-parse HEAD)');
|
||||
const pinUpdateIndex = source.indexOf('plugin.source.sha = sha;');
|
||||
const pinValidationIndex = source.indexOf('node scripts/ci/validate-grok-pin.js');
|
||||
const pinValidationIndex = source.indexOf('node scripts/ci/validate-grok-pin.js --release');
|
||||
const pinCommitIndex = source.indexOf('git commit -m "chore: pin Grok marketplace to release source"');
|
||||
const tagIndex = source.indexOf('git tag "v$VERSION"');
|
||||
|
||||
@@ -96,9 +96,24 @@ function runTests() {
|
||||
assert.ok(pinUpdateIndex > sourceShaIndex);
|
||||
assert.ok(pinValidationIndex > pinUpdateIndex);
|
||||
assert.ok(pinCommitIndex > pinValidationIndex);
|
||||
const finalValidationIndex = source.lastIndexOf('node scripts/ci/validate-grok-pin.js --release');
|
||||
assert.ok(finalValidationIndex > pinCommitIndex && finalValidationIndex < tagIndex,
|
||||
'validate the committed release snapshot before tagging');
|
||||
assert.ok(tagIndex > pinCommitIndex, 'the release tag must point at the pin-only commit');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('release workflows enforce snapshot equality while ordinary CI accepts historical pins', () => {
|
||||
for (const workflow of [releaseWorkflowSource, reusableReleaseWorkflowSource]) {
|
||||
assert.match(workflow, /run: node scripts\/ci\/validate-grok-pin\.js --release/);
|
||||
}
|
||||
const reusableValidation = fs.readFileSync(path.join(
|
||||
__dirname, '../../.github/workflows/reusable-validate.yml'
|
||||
), 'utf8');
|
||||
for (const workflow of [ciWorkflowSource, reusableValidation]) {
|
||||
assert.match(workflow, /run: node scripts\/ci\/validate-grok-pin\.js\s*\n/);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('release script supports prerelease semver and release heading sync', () => {
|
||||
assert.ok(
|
||||
source.includes('2.0.0-rc.1'),
|
||||
|
||||
Reference in new issue
Block a user