Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
951 lines
36 KiB
JavaScript
951 lines
36 KiB
JavaScript
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import {
|
|
chmodSync,
|
|
copyFileSync,
|
|
cpSync,
|
|
existsSync,
|
|
lstatSync,
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
realpathSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
import { npmSpawnOptions } from './npm-spawn.mjs';
|
|
import { validateCliReleaseArtifactMetrics } from './release-cli-artifact-policy.mjs';
|
|
import { assertProductNightlyVersion } from './release-version.mjs';
|
|
import {
|
|
isCurrentDevelopmentJavaScript,
|
|
isMakaDevelopmentArtifact,
|
|
isThirdPartyDevelopmentArtifact,
|
|
orderWorkspaceBuilds,
|
|
renderNpmReadme,
|
|
releaseNpmEnvironment,
|
|
resolveReleaseWorkspacePackages,
|
|
resolveWorkspaceReleaseFiles,
|
|
workspaceReleaseManifest,
|
|
} from './release-cli-file-policy.mjs';
|
|
|
|
const repoRoot = resolve(import.meta.dirname, '..');
|
|
const cliSource = join(repoRoot, 'packages/cli');
|
|
const allowDirty = process.argv.includes('--allow-dirty');
|
|
const developmentBuild = process.argv.includes('--development');
|
|
const nightlyVersion = process.env.MAKA_CLI_NIGHTLY_VERSION?.trim();
|
|
const preparedTree = process.env.MAKA_CLI_RELEASE_PREPARED_TREE === '1';
|
|
const releaseRoot = join(cliSource, 'release');
|
|
const artifactRoot = developmentBuild ? createDevelopmentArtifactRoot() : releaseRoot;
|
|
const stageRoot = join(artifactRoot, 'package');
|
|
const peerPrebuildTargets = ['darwin-arm64', 'linux-arm64', 'linux-x64', 'win32-x64'];
|
|
const privatePeerTarget = developmentBuild
|
|
? resolveDevelopmentPeerTarget()
|
|
: `${process.platform}-${process.arch}`;
|
|
const unsupportedArguments = process.argv
|
|
.slice(2)
|
|
.filter((argument) => !['--allow-dirty', '--development'].includes(argument));
|
|
if (unsupportedArguments.length > 0) {
|
|
throw new Error(`Unsupported release argument: ${unsupportedArguments.join(', ')}`);
|
|
}
|
|
const workspacePackages = resolveReleaseWorkspacePackages(repoRoot);
|
|
const internalPackageNames = workspacePackages
|
|
.map(({ name }) => name)
|
|
.filter((name) => name !== 'maka-agent');
|
|
const internalPackageSet = new Set(internalPackageNames);
|
|
const buildOrder = orderWorkspaceBuilds(workspacePackages);
|
|
const developmentGeneratedFiles = new Map([
|
|
['@maka/runtime', new Set(['workers/filesystem-worker.js'])],
|
|
]);
|
|
const strippedInstallScripts = new Map([
|
|
// The clean repository install has already produced every generated file and
|
|
// platform prebuild copied below. Do not run advisory postinstalls on an end
|
|
// user's machine.
|
|
['node-pty@1.2.0-beta.15', new Set(['install', 'postinstall'])],
|
|
['protobufjs@7.6.5', new Set(['postinstall'])],
|
|
]);
|
|
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
if (developmentBuild) rmSync(artifactRoot, { recursive: true, force: true });
|
|
throw error;
|
|
}
|
|
|
|
function main() {
|
|
validateNodeVersion();
|
|
if (developmentBuild && nightlyVersion) {
|
|
throw new Error('A public Nightly cannot be combined with --development');
|
|
}
|
|
if (allowDirty && nightlyVersion) {
|
|
throw new Error('A public Nightly cannot be built from a dirty worktree');
|
|
}
|
|
if (nightlyVersion) {
|
|
const productVersion = readJson(join(repoRoot, 'package.json')).version;
|
|
assertProductNightlyVersion(nightlyVersion, productVersion);
|
|
}
|
|
if (!developmentBuild) validateReleaseNpmVersion();
|
|
if (developmentBuild) {
|
|
if (allowDirty || preparedTree) {
|
|
throw new Error('--development cannot be combined with release build options');
|
|
}
|
|
console.warn('[release-cli] producing a private development tarball');
|
|
buildRuntimeWorkspaces({ clean: false });
|
|
packageCli(false);
|
|
return;
|
|
}
|
|
if (!preparedTree && !allowDirty) {
|
|
validateCleanWorktree();
|
|
buildFromCleanDependencyTree();
|
|
return;
|
|
}
|
|
if (preparedTree && allowDirty) {
|
|
throw new Error('--allow-dirty cannot be combined with a prepared release tree');
|
|
}
|
|
if (preparedTree && existsSync(join(repoRoot, '.git'))) {
|
|
throw new Error('A prepared release build must run inside the isolated source archive');
|
|
}
|
|
if (allowDirty) {
|
|
console.warn(
|
|
'[release-cli] WARNING: producing a private development tarball with publishing disabled',
|
|
);
|
|
}
|
|
buildRuntimeWorkspaces({ clean: true });
|
|
checkProductionAudit();
|
|
runNpm(['run', 'check:cli-third-party-notices']);
|
|
runNpm(['run', 'check:runtime-host-peer-dependencies']);
|
|
runNpm(['run', 'check:runtime-host-peer-notices']);
|
|
|
|
packageCli(preparedTree);
|
|
}
|
|
|
|
function packageCli(publishable) {
|
|
const dependencyTree = readCliDependencyTree();
|
|
const cli = dependencyTree.dependencies?.['maka-agent'];
|
|
if (!cli) throw new Error('npm ls did not return the maka-agent workspace');
|
|
|
|
rmSync(artifactRoot, { recursive: true, force: true });
|
|
mkdirSync(stageRoot, { recursive: true, mode: 0o755 });
|
|
copyCliRuntime();
|
|
copyRuntimeHostPeerPrebuilds(publishable);
|
|
const expectedDependencyManifests = copyDependencyClosure(cli);
|
|
copyReleaseDocuments();
|
|
writeReleaseManifest(cli, publishable);
|
|
validateStaging(publishable);
|
|
|
|
const packOutput = JSON.parse(
|
|
runNpm(['pack', stageRoot, '--json', '--pack-destination', artifactRoot], {
|
|
encoding: 'utf8',
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
}),
|
|
);
|
|
const packs = Array.isArray(packOutput) ? packOutput : Object.values(packOutput);
|
|
const [pack] = packs;
|
|
if (packs.length !== 1 || !pack?.filename || !Array.isArray(pack.files)) {
|
|
throw new Error('npm pack did not return one JSON package result');
|
|
}
|
|
validateCliReleaseArtifactMetrics({
|
|
compressedBytes: pack.size,
|
|
unpackedBytes: pack.unpackedSize,
|
|
entryCount: pack.entryCount,
|
|
});
|
|
const tarballPath = join(artifactRoot, pack.filename);
|
|
validatePackedFiles(pack.files, expectedDependencyManifests, publishable);
|
|
const sha256 = digestFile(tarballPath);
|
|
writeFileSync(`${tarballPath}.sha256`, `${sha256} ${pack.filename}\n`, 'utf8');
|
|
writeFileSync(
|
|
join(artifactRoot, `${pack.filename}.files.json`),
|
|
`${JSON.stringify(pack.files, null, 2)}\n`,
|
|
'utf8',
|
|
);
|
|
|
|
console.log(`[release-cli] tarball: ${tarballPath}`);
|
|
console.log(`[release-cli] sha256: ${sha256}`);
|
|
console.log(
|
|
`[release-cli] size: ${formatBytes(pack.size)} compressed, ${formatBytes(pack.unpackedSize)} unpacked, ${pack.entryCount} files`,
|
|
);
|
|
}
|
|
|
|
function createDevelopmentArtifactRoot() {
|
|
const developmentRoot = join(cliSource, '.development');
|
|
mkdirSync(developmentRoot, { recursive: true, mode: 0o755 });
|
|
const parent = process.env.MAKA_CLI_DEVELOPMENT_OUTPUT_ROOT?.trim();
|
|
if (!parent) return mkdtempSync(join(developmentRoot, 'artifact-'));
|
|
const resolvedParent = realpathSync(parent);
|
|
const relativeParent = relative(realpathSync(developmentRoot), resolvedParent);
|
|
if (
|
|
!relativeParent ||
|
|
relativeParent.startsWith('..') ||
|
|
isAbsolute(relativeParent) ||
|
|
!statSync(resolvedParent).isDirectory()
|
|
) {
|
|
throw new Error('The CLI development output root must be a directory');
|
|
}
|
|
return join(resolvedParent, 'artifact');
|
|
}
|
|
|
|
function buildFromCleanDependencyTree() {
|
|
const temporaryRoot = mkdtempSync(join(tmpdir(), 'maka-cli-release-build-'));
|
|
const archivePath = join(temporaryRoot, 'source.tar');
|
|
const cleanRoot = join(temporaryRoot, 'source');
|
|
try {
|
|
mkdirSync(cleanRoot, { recursive: true, mode: 0o755 });
|
|
execFileSync('git', ['archive', '--format=tar', `--output=${archivePath}`, 'HEAD'], {
|
|
cwd: repoRoot,
|
|
stdio: 'inherit',
|
|
});
|
|
execFileSync('tar', ['-xf', archivePath, '-C', cleanRoot], { stdio: 'inherit' });
|
|
const preparedPeerPrebuilds = copyPeerPrebuildInputToCleanTree(cleanRoot);
|
|
console.log('[release-cli] installing the committed dependency tree with npm ci');
|
|
const cleanEnvironment = releaseNpmEnvironment(process.env, join(cleanRoot, '.npmrc'));
|
|
execFileSync(
|
|
'npm',
|
|
['ci'],
|
|
npmSpawnOptions({ cwd: cleanRoot, env: cleanEnvironment, stdio: 'inherit' }),
|
|
);
|
|
execFileSync(process.execPath, [join(cleanRoot, 'scripts/release-cli-package.mjs')], {
|
|
cwd: cleanRoot,
|
|
env: {
|
|
...cleanEnvironment,
|
|
MAKA_CLI_RELEASE_PREPARED_TREE: '1',
|
|
...(preparedPeerPrebuilds
|
|
? { MAKA_RUNTIME_HOST_PEER_PREBUILDS: preparedPeerPrebuilds }
|
|
: {}),
|
|
},
|
|
stdio: 'inherit',
|
|
});
|
|
|
|
const cleanReleaseRoot = join(cleanRoot, 'packages/cli/release');
|
|
if (!existsSync(cleanReleaseRoot)) {
|
|
throw new Error('The isolated release build did not produce a release directory');
|
|
}
|
|
rmSync(releaseRoot, { recursive: true, force: true });
|
|
cpSync(cleanReleaseRoot, releaseRoot, { recursive: true, preserveTimestamps: true });
|
|
console.log(`[release-cli] copied the isolated release artifacts to ${releaseRoot}`);
|
|
} finally {
|
|
rmSync(temporaryRoot, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function copyPeerPrebuildInputToCleanTree(cleanRoot) {
|
|
const source = process.env.MAKA_RUNTIME_HOST_PEER_PREBUILDS?.trim();
|
|
if (!source) return undefined;
|
|
const destination = join(cleanRoot, '.release-runtime-host-peer-prebuilds');
|
|
cpSync(realpathSync(source), destination, { recursive: true, preserveTimestamps: true });
|
|
return destination;
|
|
}
|
|
|
|
function validateNodeVersion() {
|
|
const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
|
|
if (major < 22 || (major === 22 && minor < 19)) {
|
|
throw new Error(`Node.js >=22.19.0 is required; found ${process.versions.node}`);
|
|
}
|
|
}
|
|
|
|
function validateReleaseNpmVersion() {
|
|
const packageManager = readJson(join(repoRoot, 'package.json')).packageManager;
|
|
const requiredNpmVersion = /^npm@(.+)$/.exec(packageManager)?.[1];
|
|
if (!requiredNpmVersion) {
|
|
throw new Error(
|
|
`The root packageManager must pin an exact npm version; found ${packageManager}`,
|
|
);
|
|
}
|
|
const npmVersion = runNpm(['--version'], { encoding: 'utf8' }).trim();
|
|
if (npmVersion !== requiredNpmVersion) {
|
|
throw new Error(`npm ${requiredNpmVersion} is required; found ${npmVersion}`);
|
|
}
|
|
}
|
|
|
|
function validateCleanWorktree() {
|
|
const status = execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], {
|
|
cwd: repoRoot,
|
|
encoding: 'utf8',
|
|
}).trim();
|
|
if (status) {
|
|
throw new Error('Refusing to build a release tarball from a dirty worktree; commit first');
|
|
}
|
|
}
|
|
|
|
function buildRuntimeWorkspaces(options) {
|
|
if (options.clean) {
|
|
for (const workspace of buildOrder) runNpm(['--workspace', workspace, 'run', 'clean']);
|
|
}
|
|
for (const workspace of buildOrder) runNpm(['--workspace', workspace, 'run', 'build']);
|
|
}
|
|
|
|
function checkProductionAudit() {
|
|
const audit = spawnSync(
|
|
'npm',
|
|
['audit', '--omit=dev', '--workspace', 'maka-agent', '--json'],
|
|
npmSpawnOptions({
|
|
cwd: repoRoot,
|
|
encoding: 'utf8',
|
|
env: releaseNpmEnvironment(process.env, join(repoRoot, '.npmrc')),
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
}),
|
|
);
|
|
const report = JSON.parse(audit.stdout || '{}');
|
|
const vulnerabilities = report.metadata?.vulnerabilities;
|
|
if (audit.error || audit.status !== 0 || vulnerabilities?.total !== 0) {
|
|
throw new Error(
|
|
`CLI production dependency audit failed: ${JSON.stringify(vulnerabilities ?? report.error ?? audit.error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function readCliDependencyTree() {
|
|
return JSON.parse(
|
|
runNpm(['ls', '--workspace', 'maka-agent', '--omit=dev', '--all', '--long', '--json'], {
|
|
encoding: 'utf8',
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function copyCliRuntime() {
|
|
copyRuntimeDist(cliSource, stageRoot);
|
|
chmodSync(join(stageRoot, 'dist/cli.js'), 0o755);
|
|
}
|
|
|
|
function copyDependencyClosure(cli) {
|
|
const copiedDestinations = new Map();
|
|
const visit = (node, parentDestination) => {
|
|
for (const dependency of Object.values(node.dependencies ?? {})) {
|
|
if (!dependency || typeof dependency !== 'object') continue;
|
|
const peerPolicy = dependencyPeerPolicy(node, dependency.name);
|
|
if (peerPolicy === 'optional') continue;
|
|
if (typeof dependency.path === 'string' && existsSync(dependency.path)) {
|
|
const destination = dependencyDestination(dependency);
|
|
const source = realpathSync(dependency.path);
|
|
const previous = copiedDestinations.get(destination);
|
|
if (previous && previous !== source) {
|
|
throw new Error(
|
|
`Dependency destination collision at ${destination}: ${previous} vs ${source}`,
|
|
);
|
|
}
|
|
if (!previous) {
|
|
if (internalPackageSet.has(dependency.name)) {
|
|
copyInternalPackage(source, destination);
|
|
} else {
|
|
copyThirdPartyPackage(source, destination);
|
|
}
|
|
copiedDestinations.set(destination, source);
|
|
}
|
|
if (
|
|
peerPolicy === 'required' &&
|
|
parentDestination &&
|
|
destination === join(parentDestination, 'node_modules', ...dependency.name.split('/'))
|
|
) {
|
|
addBundledDependency(parentDestination, dependency.name, dependency.version);
|
|
}
|
|
visit(dependency, destination);
|
|
}
|
|
}
|
|
};
|
|
visit(cli, stageRoot);
|
|
|
|
const evalUndici = findDependency(cli, 'undici', '8.10.0');
|
|
if (!evalUndici?.path || !existsSync(evalUndici.path)) {
|
|
throw new Error('The installed CLI closure does not contain undici@8.10.0');
|
|
}
|
|
copyThirdPartyPackage(realpathSync(evalUndici.path), join(stageRoot, 'node_modules/undici'));
|
|
copiedDestinations.set(join(stageRoot, 'node_modules/undici'), realpathSync(evalUndici.path));
|
|
return [...copiedDestinations.keys()].map(
|
|
(destination) => `${relative(stageRoot, destination).split(sep).join('/')}/package.json`,
|
|
);
|
|
}
|
|
|
|
function dependencyPeerPolicy(parent, dependencyName) {
|
|
if (!dependencyName || typeof parent.path !== 'string' || !existsSync(parent.path)) return 'none';
|
|
const manifest = readJson(join(realpathSync(parent.path), 'package.json'));
|
|
if (!manifest.peerDependencies?.[dependencyName]) return 'none';
|
|
return manifest.peerDependenciesMeta?.[dependencyName]?.optional ? 'optional' : 'required';
|
|
}
|
|
|
|
function addBundledDependency(packageRoot, dependencyName, dependencyVersion) {
|
|
const manifestPath = join(packageRoot, 'package.json');
|
|
const manifest = readJson(manifestPath);
|
|
manifest.dependencies = { ...manifest.dependencies, [dependencyName]: dependencyVersion };
|
|
manifest.bundledDependencies = [
|
|
...new Set([...(manifest.bundledDependencies ?? []), dependencyName]),
|
|
].sort();
|
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
}
|
|
|
|
function dependencyDestination(dependency) {
|
|
if (internalPackageSet.has(dependency.name)) {
|
|
return join(stageRoot, 'node_modules', ...dependency.name.split('/'));
|
|
}
|
|
if (dependency.name?.startsWith('@maka/')) {
|
|
throw new Error(`Unexpected private workspace in the CLI closure: ${dependency.name}`);
|
|
}
|
|
const sourcePath = resolve(dependency.path);
|
|
const rootModules = join(repoRoot, 'node_modules');
|
|
if (isInside(rootModules, sourcePath)) {
|
|
return join(stageRoot, 'node_modules', relative(rootModules, sourcePath));
|
|
}
|
|
for (const packageName of internalPackageNames) {
|
|
const workspaceRoot = realpathSync(join(repoRoot, 'node_modules', ...packageName.split('/')));
|
|
const workspaceModules = join(workspaceRoot, 'node_modules');
|
|
if (isInside(workspaceModules, sourcePath)) {
|
|
return join(
|
|
stageRoot,
|
|
'node_modules',
|
|
...packageName.split('/'),
|
|
'node_modules',
|
|
relative(workspaceModules, sourcePath),
|
|
);
|
|
}
|
|
}
|
|
throw new Error(`Dependency path is outside the supported installed tree: ${sourcePath}`);
|
|
}
|
|
|
|
function copyInternalPackage(source, destination) {
|
|
mkdirSync(destination, { recursive: true, mode: 0o755 });
|
|
const manifest = readJson(join(source, 'package.json'));
|
|
const releaseManifest = workspaceReleaseManifest(manifest);
|
|
writeFileSync(join(destination, 'package.json'), `${JSON.stringify(releaseManifest, null, 2)}\n`);
|
|
for (const releaseFile of resolveWorkspaceReleaseFiles(source, manifest)) {
|
|
if (releaseFile === 'dist') copyRuntimeDist(source, destination, manifest.name);
|
|
else copyDeclaredReleaseFile(source, destination, releaseFile);
|
|
}
|
|
}
|
|
|
|
function copyRuntimeDist(source, destination, packageName = 'maka-agent') {
|
|
const sourceDist = join(source, 'dist');
|
|
if (!existsSync(sourceDist)) throw new Error(`Missing build output: ${sourceDist}`);
|
|
copyTreeFiles(sourceDist, join(destination, 'dist'), (relativePath) => {
|
|
if (isMakaDevelopmentArtifact(join('dist', relativePath))) return false;
|
|
return (
|
|
!developmentBuild ||
|
|
isCurrentDevelopmentJavaScript(
|
|
source,
|
|
relativePath,
|
|
developmentGeneratedFiles.get(packageName),
|
|
)
|
|
);
|
|
});
|
|
}
|
|
|
|
function copyThirdPartyPackage(source, destination) {
|
|
if (!existsSync(join(source, 'package.json'))) {
|
|
throw new Error(`Installed dependency has no package.json: ${source}`);
|
|
}
|
|
rmSync(destination, { recursive: true, force: true });
|
|
cpSync(source, destination, {
|
|
recursive: true,
|
|
preserveTimestamps: true,
|
|
filter: (path) => {
|
|
if (path === source) return true;
|
|
const relativePath = relative(source, path);
|
|
return (
|
|
!relativePath.split(sep).includes('node_modules') &&
|
|
!isThirdPartyDevelopmentArtifact(relativePath)
|
|
);
|
|
},
|
|
});
|
|
stripReviewedInstallScripts(destination);
|
|
}
|
|
|
|
function stripReviewedInstallScripts(destination) {
|
|
const manifestPath = join(destination, 'package.json');
|
|
const manifest = readJson(manifestPath);
|
|
const packageKey = `${manifest.name}@${manifest.version}`;
|
|
const reviewed = strippedInstallScripts.get(packageKey);
|
|
const present = ['preinstall', 'install', 'postinstall'].filter(
|
|
(name) => typeof manifest.scripts?.[name] === 'string',
|
|
);
|
|
if (present.length === 0) return;
|
|
if (!reviewed || present.some((name) => !reviewed.has(name))) {
|
|
throw new Error(`${packageKey} has an unreviewed install script: ${present.join(', ')}`);
|
|
}
|
|
for (const name of present) delete manifest.scripts[name];
|
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
if (packageKey === 'node-pty@1.2.0-beta.15') pruneNodePtyBuildInputs(destination);
|
|
}
|
|
|
|
function pruneNodePtyBuildInputs(destination) {
|
|
// Without an explicit install script npm treats binding.gyp as a request to
|
|
// run node-gyp. This release supports the platforms validated below through
|
|
// shipped prebuilds, so source compilation would only add network/toolchain
|
|
// dependence and bypass the reviewed artifact.
|
|
for (const path of ['binding.gyp', 'scripts', 'src', 'third_party', 'typings']) {
|
|
rmSync(join(destination, path), { recursive: true, force: true });
|
|
}
|
|
for (const path of walkFiles(destination)) {
|
|
if (lstatSync(path).isFile() && /\.(?:map|pdb)$/.test(path)) rmSync(path, { force: true });
|
|
}
|
|
}
|
|
|
|
function copyReleaseDocuments() {
|
|
const readme = readFileSync(join(cliSource, 'README.md'), 'utf8');
|
|
const readmeZhCn = readFileSync(join(cliSource, 'README.zh-CN.md'), 'utf8');
|
|
const disclaimer = readFileSync(join(repoRoot, 'DISCLAIMER-WIP'), 'utf8');
|
|
const nightlyNotice = nightlyVersion
|
|
? '> **Developer snapshot:** This Nightly is not an Apache release and is not intended for production use. Its version is bound to one exact `apache/maka` commit.\n\n'
|
|
: '';
|
|
const nightlyNoticeZhCn = nightlyVersion
|
|
? '> **开发快照:**此 Nightly 不是 Apache Release,不用于生产环境;其版本只对应一个精确的 `apache/maka` commit。\n\n'
|
|
: '';
|
|
writeFileSync(
|
|
join(stageRoot, 'README.md'),
|
|
`${nightlyNotice}${renderNpmReadme(readme, disclaimer)}`,
|
|
'utf8',
|
|
);
|
|
writeFileSync(join(stageRoot, 'README.zh-CN.md'), `${nightlyNoticeZhCn}${readmeZhCn}`, 'utf8');
|
|
copyFileSync(join(repoRoot, 'LICENSE'), join(stageRoot, 'LICENSE'));
|
|
copyFileSync(join(repoRoot, 'NOTICE'), join(stageRoot, 'NOTICE'));
|
|
// Incubator policy: every public podling artifact carries the incubating
|
|
// disclaimer next to LICENSE/NOTICE, including developer-only Nightlies.
|
|
copyFileSync(join(repoRoot, 'DISCLAIMER-WIP'), join(stageRoot, 'DISCLAIMER-WIP'));
|
|
copyFileSync(
|
|
join(cliSource, 'THIRD_PARTY_NOTICES.txt'),
|
|
join(stageRoot, 'THIRD_PARTY_NOTICES.txt'),
|
|
);
|
|
copyFileSync(
|
|
join(cliSource, 'RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv'),
|
|
join(stageRoot, 'RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv'),
|
|
);
|
|
copyFileSync(
|
|
join(cliSource, 'RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt'),
|
|
join(stageRoot, 'RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt'),
|
|
);
|
|
}
|
|
|
|
function copyRuntimeHostPeerPrebuilds(publishable) {
|
|
const sourceRoot = process.env.MAKA_RUNTIME_HOST_PEER_PREBUILDS?.trim();
|
|
const targets = publishable
|
|
? peerPrebuildTargets
|
|
: privatePeerTarget === 'none'
|
|
? []
|
|
: [privatePeerTarget];
|
|
if (targets.length === 0) return;
|
|
const destinationRoot = join(stageRoot, 'native/runtime-host-peer/prebuilds');
|
|
if (!sourceRoot && !publishable) {
|
|
const [target] = targets;
|
|
const destination = join(destinationRoot, target, 'maka_runtime_host_peer.node');
|
|
buildDevelopmentPeerAddon(target, destination);
|
|
return;
|
|
}
|
|
if (!sourceRoot) {
|
|
throw new Error('MAKA_RUNTIME_HOST_PEER_PREBUILDS must contain all release platform addons');
|
|
}
|
|
for (const target of targets) {
|
|
const source = join(sourceRoot, target, 'maka_runtime_host_peer.node');
|
|
if (!existsSync(source) || !statSync(source).isFile()) {
|
|
throw new Error(`Runtime Host peer prebuild is missing: ${target}`);
|
|
}
|
|
const destination = join(destinationRoot, target, 'maka_runtime_host_peer.node');
|
|
mkdirSync(dirname(destination), { recursive: true, mode: 0o755 });
|
|
copyFileSync(source, destination);
|
|
}
|
|
}
|
|
|
|
function resolveDevelopmentPeerTarget() {
|
|
const configured = process.env.MAKA_CLI_DEVELOPMENT_PEER_TARGET?.trim();
|
|
const target = configured || `${process.platform}-${process.arch}`;
|
|
if (target !== 'none' && !peerPrebuildTargets.includes(target)) {
|
|
throw new Error(
|
|
`MAKA_CLI_DEVELOPMENT_PEER_TARGET must be none or a supported target; found ${target}`,
|
|
);
|
|
}
|
|
return target;
|
|
}
|
|
|
|
function buildDevelopmentPeerAddon(target, output) {
|
|
const hostTarget = `${process.platform}-${process.arch}`;
|
|
const buildScript = join(repoRoot, 'native/runtime-host-peer/build.mjs');
|
|
if (target === hostTarget) {
|
|
execFileSync(process.execPath, [buildScript], {
|
|
cwd: repoRoot,
|
|
env: { ...process.env, MAKA_RUNTIME_HOST_PEER_OUTPUT: output },
|
|
stdio: 'inherit',
|
|
});
|
|
return;
|
|
}
|
|
const rustTarget = {
|
|
'linux-arm64': 'aarch64-unknown-linux-gnu.2.28',
|
|
'linux-x64': 'x86_64-unknown-linux-gnu.2.28',
|
|
}[target];
|
|
if (!rustTarget) {
|
|
throw new Error(
|
|
`Cannot build the ${target} direct-peer addon from ${hostTarget}; run Desktop on that target or provide MAKA_RUNTIME_HOST_PEER_PREBUILDS`,
|
|
);
|
|
}
|
|
requireDevelopmentCommand(
|
|
'zig',
|
|
['version'],
|
|
`Cross-compiling the ${target} direct-peer addon requires Zig on PATH (CI uses 0.16.x)`,
|
|
);
|
|
requireDevelopmentCommand(
|
|
'cargo-zigbuild',
|
|
['--version'],
|
|
`Cross-compiling the ${target} direct-peer addon requires cargo-zigbuild (cargo install cargo-zigbuild --version 0.23.2 --locked)`,
|
|
);
|
|
execFileSync(process.execPath, [buildScript], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
MAKA_RUNTIME_HOST_PEER_CARGO_SUBCOMMAND: 'zigbuild',
|
|
MAKA_RUNTIME_HOST_PEER_CARGO_TARGET: rustTarget,
|
|
MAKA_RUNTIME_HOST_PEER_OUTPUT: output,
|
|
},
|
|
stdio: 'inherit',
|
|
});
|
|
}
|
|
|
|
function requireDevelopmentCommand(command, args, message) {
|
|
const result = spawnSync(command, args, { cwd: repoRoot, encoding: 'utf8' });
|
|
if (result.status === 0) return;
|
|
throw new Error(`${message}; install it before setting up this development Runtime Host`);
|
|
}
|
|
|
|
function writeReleaseManifest(cli, publishable) {
|
|
const source = readJson(join(cliSource, 'package.json'));
|
|
const root = readJson(join(repoRoot, 'package.json'));
|
|
if (source.version !== cli.version) {
|
|
throw new Error(
|
|
`CLI manifest and installed lockfile disagree: ${source.version} vs ${cli.version}`,
|
|
);
|
|
}
|
|
const undici = findDependency(cli, 'undici', '8.10.0');
|
|
const dependencies = { ...source.dependencies, undici: undici.version };
|
|
const updateCompatibility = source.maka?.managedRuntimeHostUpdateCompatibility;
|
|
if (!Number.isSafeInteger(updateCompatibility) || updateCompatibility < 1) {
|
|
throw new Error(
|
|
'CLI manifest must define a positive managed Runtime Host update compatibility',
|
|
);
|
|
}
|
|
const manifest = {
|
|
name: source.name,
|
|
version: publishable && nightlyVersion ? nightlyVersion : source.version,
|
|
description: nightlyVersion
|
|
? 'Apache Maka (Incubating) developer snapshot; not an Apache release.'
|
|
: 'Apache Maka (Incubating), a local-first agent workspace for the terminal.',
|
|
license: source.license,
|
|
type: source.type,
|
|
exports: {},
|
|
bin: source.bin,
|
|
maka: { managedRuntimeHostUpdateCompatibility: updateCompatibility },
|
|
engines: root.engines,
|
|
repository: {
|
|
type: 'git',
|
|
url: 'git+https://github.com/apache/maka.git',
|
|
directory: 'packages/cli',
|
|
},
|
|
homepage: 'https://github.com/apache/maka#readme',
|
|
bugs: { url: 'https://github.com/apache/maka/issues' },
|
|
keywords: ['apache', 'ai', 'agent', 'cli', 'tui', 'local-first'],
|
|
publishConfig: publishable
|
|
? {
|
|
access: 'public',
|
|
registry: 'https://registry.npmjs.org/',
|
|
tag: nightlyVersion ? 'nightly' : 'latest',
|
|
}
|
|
: {
|
|
access: 'restricted',
|
|
registry: 'http://127.0.0.1:9/',
|
|
tag: 'development',
|
|
},
|
|
files: [
|
|
'dist',
|
|
'native',
|
|
'README.md',
|
|
'README.zh-CN.md',
|
|
'LICENSE',
|
|
'NOTICE',
|
|
'DISCLAIMER-WIP',
|
|
'THIRD_PARTY_NOTICES.txt',
|
|
'RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv',
|
|
'RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt',
|
|
],
|
|
dependencies,
|
|
bundledDependencies: Object.keys(dependencies).sort(),
|
|
...(!publishable ? { private: true } : {}),
|
|
};
|
|
if (!publishable) {
|
|
manifest.version = developmentPackageVersion(source.version, manifest);
|
|
}
|
|
writeFileSync(join(stageRoot, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
}
|
|
|
|
function developmentPackageVersion(baseVersion, manifest) {
|
|
const digest = createHash('sha256');
|
|
for (const path of walkFiles(stageRoot)
|
|
.filter((candidate) => lstatSync(candidate).isFile())
|
|
.sort()) {
|
|
digest.update(relative(stageRoot, path).split(sep).join('/'));
|
|
digest.update('\0');
|
|
digest.update(readFileSync(path));
|
|
digest.update('\0');
|
|
}
|
|
const { version: _version, ...manifestIdentity } = manifest;
|
|
digest.update('package.json\0');
|
|
digest.update(JSON.stringify(manifestIdentity));
|
|
digest.update('\0');
|
|
return `${baseVersion}${baseVersion.includes('-') ? '.' : '-'}dev-${digest.digest('hex').slice(0, 12)}`;
|
|
}
|
|
|
|
function validateStaging(publishable) {
|
|
const required = [
|
|
'dist/cli.js',
|
|
'README.zh-CN.md',
|
|
'DISCLAIMER-WIP',
|
|
'RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv',
|
|
'RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt',
|
|
'node_modules/@maka/runtime/dist/workers/filesystem-worker.js',
|
|
'node_modules/@maka/runtime-host/dist/execution-candidate-main.js',
|
|
'node_modules/@maka/eval/dist/harbor-external-subject.js',
|
|
'node_modules/@maka/eval/harbor/relay_agent.py',
|
|
'node_modules/@maka/eval/harbor/docker-compose-egress-proxy.yaml',
|
|
'node_modules/node-pty/prebuilds/linux-x64/pty.node',
|
|
'node_modules/node-pty/prebuilds/darwin-arm64/pty.node',
|
|
'node_modules/node-pty/prebuilds/win32-x64/conpty.node',
|
|
'node_modules/fs-native-extensions/prebuilds/linux-x64/fs-native-extensions.node',
|
|
'node_modules/fs-native-extensions/prebuilds/darwin-arm64/fs-native-extensions.node',
|
|
'node_modules/fs-native-extensions/prebuilds/win32-x64/fs-native-extensions.node',
|
|
];
|
|
if (publishable) {
|
|
required.push(
|
|
...peerPrebuildTargets.map(
|
|
(target) => `native/runtime-host-peer/prebuilds/${target}/maka_runtime_host_peer.node`,
|
|
),
|
|
);
|
|
} else if (privatePeerTarget !== 'none') {
|
|
required.push(
|
|
`native/runtime-host-peer/prebuilds/${privatePeerTarget}/maka_runtime_host_peer.node`,
|
|
);
|
|
}
|
|
for (const path of required) {
|
|
if (!existsSync(join(stageRoot, path)))
|
|
throw new Error(`Required release file is missing: ${path}`);
|
|
}
|
|
assertPatchedFile(
|
|
'node_modules/node-pty/lib/unixTerminal.js',
|
|
'CustomWriteStream.prototype._ownsFileDescriptor',
|
|
);
|
|
assertPatchedFile('node_modules/@ai-sdk/provider-utils/dist/index.js', 'function absentIfBlank');
|
|
|
|
const manifest = readJson(join(stageRoot, 'package.json'));
|
|
for (const [name, specifier] of Object.entries(manifest.dependencies ?? {})) {
|
|
if (
|
|
typeof specifier !== 'string' ||
|
|
/^(?:file:|workspace:)|^[A-Za-z]:[\\/]|^\//.test(specifier)
|
|
) {
|
|
throw new Error(`Release dependency must use a registry version: ${name}@${specifier}`);
|
|
}
|
|
if (
|
|
name.startsWith('@maka/') &&
|
|
!existsSync(join(stageRoot, 'node_modules', ...name.split('/')))
|
|
) {
|
|
throw new Error(`Private dependency is not physically bundled: ${name}`);
|
|
}
|
|
}
|
|
|
|
for (const path of walkFiles(stageRoot)) {
|
|
const relativePath = relative(stageRoot, path);
|
|
const status = lstatSync(path);
|
|
if (status.isSymbolicLink())
|
|
throw new Error(`Release staging contains a symlink: ${relativePath}`);
|
|
if (!status.isFile()) continue;
|
|
if (basename(path) === 'package.json') {
|
|
const packageManifest = readJson(path);
|
|
const installScript = ['preinstall', 'install', 'postinstall'].find(
|
|
(name) => typeof packageManifest.scripts?.[name] === 'string',
|
|
);
|
|
if (installScript) {
|
|
throw new Error(
|
|
`Release dependency retains an install script: ${relativePath} (${installScript})`,
|
|
);
|
|
}
|
|
}
|
|
const content = readFileSync(path);
|
|
if (content.includes(Buffer.from(repoRoot))) {
|
|
throw new Error(`Release file contains the repository path: ${relativePath}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function validatePackedFiles(files, expectedDependencyManifests, publishable) {
|
|
const paths = files.map((file) => file.path);
|
|
for (const file of files) {
|
|
const { path } = file;
|
|
if (isAbsolute(path) || path.split(/[\\/]/).includes('..')) {
|
|
throw new Error(`Unsafe tarball path: ${path}`);
|
|
}
|
|
const segments = path.split('/');
|
|
const makaOwned =
|
|
path.startsWith('dist/') ||
|
|
(segments[0] === 'node_modules' && segments[1] === '@maka' && segments[3] !== 'node_modules');
|
|
if (makaOwned && isMakaDevelopmentArtifact(path)) {
|
|
throw new Error(`Development artifact escaped into the tarball: ${path}`);
|
|
}
|
|
if (!makaOwned && path.startsWith('node_modules/') && isThirdPartyDevelopmentArtifact(path)) {
|
|
throw new Error(`Third-party development artifact escaped into the tarball: ${path}`);
|
|
}
|
|
const fileName = basename(path).toLowerCase();
|
|
if (
|
|
/^\.env(?:\..*)?$/.test(fileName) ||
|
|
fileName === 'deepseek.key' ||
|
|
/\.(?:key|p12|pfx|pem)$/.test(fileName) ||
|
|
fileName === 'id_rsa' ||
|
|
fileName === 'id_ed25519'
|
|
) {
|
|
throw new Error(`Credential-like file escaped into the tarball: ${path}`);
|
|
}
|
|
}
|
|
const requiredPacked = [
|
|
'dist/cli.js',
|
|
'DISCLAIMER-WIP',
|
|
'node_modules/@maka/runtime/dist/workers/filesystem-worker.js',
|
|
'node_modules/@maka/runtime-host/dist/execution-candidate-main.js',
|
|
'node_modules/@maka/eval/harbor/relay_agent.py',
|
|
...(publishable || privatePeerTarget !== 'none' ? ['native/runtime-host-peer/prebuilds/'] : []),
|
|
];
|
|
for (const suffix of requiredPacked) {
|
|
if (
|
|
!paths.some((path) =>
|
|
suffix.endsWith('/') ? path.startsWith(suffix) : path.endsWith(suffix),
|
|
)
|
|
) {
|
|
throw new Error(`Required file was not packed: ${suffix}`);
|
|
}
|
|
}
|
|
for (const manifestPath of expectedDependencyManifests) {
|
|
if (!paths.includes(manifestPath)) {
|
|
throw new Error(`Production dependency was not packed: ${manifestPath}`);
|
|
}
|
|
}
|
|
const bin = files.find((file) => file.path === 'dist/cli.js');
|
|
if (!bin || (process.platform !== 'win32' && (bin.mode & 0o111) === 0)) {
|
|
throw new Error('The packed CLI entrypoint is not executable');
|
|
}
|
|
}
|
|
|
|
function assertPatchedFile(path, marker) {
|
|
const content = readFileSync(join(stageRoot, path), 'utf8');
|
|
if (!content.includes(marker)) throw new Error(`Patched dependency marker is missing: ${path}`);
|
|
console.log(`[release-cli] patch: ${path} sha256=${digestFile(join(stageRoot, path))}`);
|
|
}
|
|
|
|
function findDependency(root, name, version) {
|
|
let result;
|
|
const visit = (node) => {
|
|
for (const dependency of Object.values(node.dependencies ?? {})) {
|
|
if (!dependency || typeof dependency !== 'object') continue;
|
|
if (dependency.name === name && dependency.version === version) result ??= dependency;
|
|
visit(dependency);
|
|
}
|
|
};
|
|
visit(root);
|
|
return result;
|
|
}
|
|
|
|
function copyDeclaredReleaseFile(source, destination, relativePath) {
|
|
const from = join(source, relativePath);
|
|
if (!existsSync(from)) throw new Error(`Declared release asset is missing: ${from}`);
|
|
const entry = statSync(from);
|
|
if (entry.isDirectory()) {
|
|
for (const child of readdirSync(from, { withFileTypes: true })) {
|
|
if (child.isSymbolicLink()) {
|
|
throw new Error(`Declared release directory contains a symlink: ${join(from, child.name)}`);
|
|
}
|
|
copyDeclaredReleaseFile(source, destination, join(relativePath, child.name));
|
|
}
|
|
} else if (entry.isFile()) {
|
|
const to = join(destination, relativePath);
|
|
mkdirSync(dirname(to), { recursive: true, mode: 0o755 });
|
|
copyFileSync(from, to);
|
|
} else {
|
|
throw new Error(`Declared release asset is not a regular file or directory: ${from}`);
|
|
}
|
|
}
|
|
|
|
function copyTreeFiles(source, destination, allow) {
|
|
for (const entry of readdirSync(source, { withFileTypes: true })) {
|
|
const path = join(source, entry.name);
|
|
const relativePath = relative(source, path);
|
|
if (entry.isDirectory()) {
|
|
copyTreeFiles(path, join(destination, entry.name), (nestedPath) =>
|
|
allow(join(entry.name, nestedPath)),
|
|
);
|
|
} else if (entry.isFile() && allow(relativePath)) {
|
|
mkdirSync(destination, { recursive: true, mode: 0o755 });
|
|
copyFileSync(path, join(destination, entry.name));
|
|
} else if (entry.isSymbolicLink()) {
|
|
throw new Error(`Maka build output contains an unexpected symlink: ${path}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function walkFiles(root) {
|
|
const paths = [];
|
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
const path = join(root, entry.name);
|
|
paths.push(path);
|
|
if (entry.isDirectory()) paths.push(...walkFiles(path));
|
|
}
|
|
return paths;
|
|
}
|
|
|
|
function isInside(parent, child) {
|
|
const path = relative(parent, child);
|
|
return path !== '' && !path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path);
|
|
}
|
|
|
|
function readJson(path) {
|
|
return JSON.parse(readFileSync(path, 'utf8'));
|
|
}
|
|
|
|
function runNpm(args, options = {}) {
|
|
const environment = releaseNpmEnvironment(options.env ?? process.env, join(repoRoot, '.npmrc'));
|
|
return execFileSync(
|
|
'npm',
|
|
args,
|
|
npmSpawnOptions({
|
|
cwd: repoRoot,
|
|
stdio: options.encoding ? undefined : 'inherit',
|
|
...options,
|
|
env: environment,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function digestFile(path) {
|
|
return createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
}
|
|
|
|
function formatBytes(value) {
|
|
if (!Number.isFinite(value)) return 'unknown';
|
|
const units = ['B', 'KiB', 'MiB', 'GiB'];
|
|
let size = value;
|
|
let unit = 0;
|
|
while (size >= 1024 && unit < units.length - 1) {
|
|
size /= 1024;
|
|
unit += 1;
|
|
}
|
|
return `${size.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
|
|
}
|