fix(release): enforce publish and runtime policy

This commit is contained in:
Dragan Spiridonov
2026-09-07 17:30:44 +00:00
parent 321ccc19a9
commit c094bee857
11 changed files with 92 additions and 21 deletions
+1 -1
View File
@@ -48,7 +48,7 @@
],
"engines": {
"claudeCode": ">=2.0.0",
"node": ">=20.0.0"
"node": ">=22.13.0"
},
"mcpServers": {
"agentic-qe": {
+6 -5
View File
@@ -144,12 +144,13 @@ if command -v node >/dev/null 2>&1; then
node_version=$(node --version)
log_success "Node.js installed: $node_version"
# Check if Node.js version is 20+
# Check if Node.js version is 22.13+
node_major=$(echo "$node_version" | cut -d'.' -f1 | sed 's/v//')
if [ "$node_major" -ge 20 ]; then
log_success "Node.js version meets requirements (≥20.0.0)"
node_minor=$(echo "$node_version" | cut -d'.' -f2)
if [ "$node_major" -gt 22 ] || { [ "$node_major" -eq 22 ] && [ "$node_minor" -ge 13 ]; }; then
log_success "Node.js version meets requirements (≥22.13.0)"
else
log_error "Node.js version too old. Required: ≥20.0.0, Found: $node_version"
log_error "Node.js version too old. Required: ≥22.13.0, Found: $node_version"
fi
else
log_error "Node.js not installed"
@@ -213,4 +214,4 @@ else
fi
log_error "Please fix critical errors before proceeding with V3 development."
exit 1
fi
fi
+1 -1
View File
@@ -3,7 +3,7 @@
* Resilient AQE hook shim (#510 item 5 — ports ruflo's hook-shim contract).
*
* Written in Node (CommonJS) so ONE file is cross-platform — Node is already a
* hard dependency of AQE (the CLI is Node >= 20), so this runs identically on
* hard dependency of AQE (the CLI is Node >= 22.13), so this runs identically on
* Windows / macOS / Linux with no POSIX `.sh` + `.cjs` twin. (It replaced an
* earlier `aqe-hook.sh`, which forced a Windows twin and a `node_modules/.bin`
* shell-wrapper spawn — see PR #512 discussion.)
+13 -10
View File
@@ -4,12 +4,6 @@ on:
release:
types: [published]
workflow_dispatch:
inputs:
dry_run:
description: 'Perform a dry run (no actual publish)'
required: false
default: 'false'
type: boolean
# Required for npm trusted publishers (OIDC)
permissions:
@@ -360,7 +354,7 @@ jobs:
pushd "${CONSUMER_DIR}" >/dev/null
echo '{"name":"audit-consumer","version":"0.0.0","private":true}' > package.json
# Install our tarball as a real consumer would.
npm install --no-audit --no-fund "${GITHUB_WORKSPACE}/${{ steps.pack.outputs.tarball }}"
npm install --no-audit --no-fund "${{ steps.pack.outputs.tarball }}"
# Now the moment of truth: audit from the consumer's POV, where
# our root-only overrides do NOT apply.
npm audit --audit-level=high
@@ -428,14 +422,23 @@ jobs:
fi
echo "Version verification passed: $PACKAGE_VERSION"
git fetch origin main --no-tags --depth=1
TAG_SHA=$(git rev-parse HEAD)
MAIN_SHA=$(git rev-parse origin/main)
if [ "$TAG_SHA" != "$MAIN_SHA" ]; then
echo "Error: release tag SHA ($TAG_SHA) is not current merged main ($MAIN_SHA)"
exit 1
fi
echo "Release lineage verification passed: $TAG_SHA"
- name: Publish to npm (dry run)
if: github.event.inputs.dry_run == 'true'
if: github.event_name == 'workflow_dispatch'
run: npm publish "${{ steps.pack.outputs.tarball }}" --access public --dry-run
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish to npm
if: github.event.inputs.dry_run != 'true'
if: github.event_name == 'release'
run: npm publish "${{ steps.pack.outputs.tarball }}" --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -451,7 +454,7 @@ jobs:
echo "- **Version**: $PACKAGE_VERSION" >> $GITHUB_STEP_SUMMARY
echo "- **Registry**: https://www.npmjs.com/package/$PACKAGE_NAME" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "> **Note**: This was a dry run. No package was actually published." >> $GITHUB_STEP_SUMMARY
else
echo "Install with: \`npm install $PACKAGE_NAME@$PACKAGE_VERSION\`" >> $GITHUB_STEP_SUMMARY
+1 -1
View File
@@ -5,7 +5,7 @@ All notable changes to the Agentic QE project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.14.1] - 2026-09-06
## [3.14.1] - 2026-09-07
This release moves AQE to maintained Node.js runtimes and strengthens the
evidence boundaries around runtime diagnosis, verification, session recovery,
+1 -1
View File
@@ -4,7 +4,7 @@ All Agentic QE release notes organized by version.
| Version | Date | Highlights |
|---------|------|------------|
| [v3.14.1](v3.14.1.md) | 2026-09-06 | Node 22+ support and stronger verification, recovery, and learning evidence. |
| [v3.14.1](v3.14.1.md) | 2026-09-07 | Node 22+ support and stronger verification, recovery, and learning evidence. |
| [v3.14.0](v3.14.0.md) | 2026-08-30 | Qualified learning evidence, embedding provenance, fail-closed RVF recovery, and packaged QE Court. |
| [v3.13.12](v3.13.12.md) | 2026-08-21 | External LLM provider registration for downstream hosts. |
| [v3.13.11](v3.13.11.md) | 2026-08-14 | Reliable diagnostics, native ESM exports, and learned pattern visibility. |
+1 -1
View File
@@ -1,6 +1,6 @@
# v3.14.1 Release Notes
**Release Date:** 2026-09-06
**Release Date:** 2026-09-07
## Highlights
+4 -1
View File
@@ -12,6 +12,7 @@
*/
import { toErrorMessage } from '../shared/error-utils.js';
import { createRequire } from 'node:module';
import { Command } from 'commander';
import chalk from 'chalk';
import type { QEKernel } from '../kernel/interfaces.js';
@@ -257,7 +258,9 @@ async function cleanupAndExit(code: number = 0): Promise<never> {
const program = new Command();
const VERSION = typeof __CLI_VERSION__ !== 'undefined' ? __CLI_VERSION__ : '0.0.0-dev';
const require = createRequire(import.meta.url);
const packageVersion = (require('../../package.json') as { version: string }).version;
const VERSION = typeof __CLI_VERSION__ !== 'undefined' ? __CLI_VERSION__ : packageVersion;
program
.name('aqe')
@@ -9,6 +9,7 @@ describe('packed package native ESM contracts', () => {
const tempRoot = path.join(packageRoot, 'node_modules', '.cache', `aqe-pack-esm-${process.pid}`);
const installedRoot = path.join(tempRoot, 'node_modules', 'agentic-qe');
let packageJson: {
version: string;
exports: Record<string, { import?: string }>;
bin?: Record<string, string>;
};
@@ -48,6 +49,19 @@ describe('packed package native ESM contracts', () => {
expectNativeImport('./dist/integrations/ruvector/index.js', 'RuVector barrel');
});
it('reports the package version from the public CLI export', () => {
const cliEntry = packageJson.exports['./cli']?.import;
expect(cliEntry).toBeDefined();
const url = pathToFileURL(path.resolve(installedRoot, cliEntry!)).href;
const result = spawnSync(
process.execPath,
['--input-type=module', '--eval', `await import(${JSON.stringify(url)});`, '--', '--version'],
{ cwd: installedRoot, encoding: 'utf8', timeout: 30_000 },
);
expect(result.status, result.stderr || result.stdout).toBe(0);
expect(result.stdout.trim()).toBe(packageJson.version);
});
it('exposes pattern mutation outcomes from the package root', () => {
const packageEntry = packageJson.exports['.']?.import;
expect(packageEntry).toBeDefined();
@@ -30,6 +30,26 @@ describe('Node support policy', () => {
expect(lock.packages?.['']?.engines?.node).toBe('>=22.13.0');
});
it('uses the same floor for the Claude Code plugin', () => {
const manifest = JSON.parse(read('.claude-plugin/plugin.json')) as {
engines?: { node?: string };
};
expect(manifest.engines?.node).toBe('>=22.13.0');
});
it('enforces the exact floor in the shipped environment validator', () => {
const validator = read('.claude/helpers/validate-v3-config.sh');
expect(validator).toContain('[ "$node_major" -gt 22 ]');
expect(validator).toContain('[ "$node_major" -eq 22 ] && [ "$node_minor" -ge 13 ]');
expect(validator).toContain('Required: ≥22.13.0');
});
it('documents the exact floor in the shipped lifecycle hook', () => {
expect(read('.claude/hooks/aqe-hook.cjs')).toContain('CLI is Node >= 22.13');
});
it('uses Node 24 for the production container', () => {
expect(read('Dockerfile').match(/^FROM node:([^\s]+)/gm)).toEqual([
'FROM node:24-alpine',
@@ -0,0 +1,30 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
const workflow = readFileSync(
resolve(import.meta.dirname, '../../../.github/workflows/npm-publish.yml'),
'utf8',
);
describe('npm publish workflow policy', () => {
it('keeps manual workflow dispatch dry-run only', () => {
expect(workflow).toContain("if: github.event_name == 'workflow_dispatch'");
expect(workflow).toContain("if: github.event_name == 'release'");
expect(workflow).not.toContain('github.event.inputs.dry_run');
});
it('passes the absolute sealed artifact directly to consumer install', () => {
expect(workflow).toContain(
'npm install --no-audit --no-fund "${{ steps.pack.outputs.tarball }}"',
);
expect(workflow).not.toContain(
'"${GITHUB_WORKSPACE}/${{ steps.pack.outputs.tarball }}"',
);
});
it('binds release publication to current merged main', () => {
expect(workflow).toContain('git fetch origin main --no-tags --depth=1');
expect(workflow).toContain('if [ "$TAG_SHA" != "$MAIN_SHA" ]; then');
});
});