Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/artifact-acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ jobs:
sparse-checkout: |
/scripts/public-release-acceptance.mjs
/scripts/release-registry.mjs
/scripts/artifact-installation.mjs
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
Expand All @@ -98,8 +99,8 @@ jobs:
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: '1.3.11'
- name: Install pinned npm
run: npm install -g npm@11.16.0 --ignore-scripts
- name: Install pinned npm and pnpm
run: npm install -g npm@11.16.0 pnpm@10.28.2 --ignore-scripts
- name: Verify publication run and retain its manifest
env:
GH_TOKEN: ${{ github.token }}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/build-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@
node scripts/release-artifacts.mjs freeze .artifacts/release
npm run build:offline
git diff --exit-code HEAD
- name: Install pinned pnpm for real package-manager fixtures
if: steps.restore.outputs.restored != 'true'
run: npm install --global --ignore-scripts pnpm@10.28.2
- name: Validate release candidate

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
needs.resolve-source.outputs.commit
. (
workflow_dispatch
).
if: steps.restore.outputs.restored != 'true'
run: |
npm run check
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:

- uses: ./.github/actions/setup

- name: Install pinned pnpm for real package-manager fixtures
run: npm install --global --ignore-scripts pnpm@10.28.2

- name: Test scripts and other workspaces
run: ./test.sh other --shard=${{ matrix.shard }}/2

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/native-artifact-acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: '1.3.11'
- name: Install pinned npm
run: npm install -g npm@11.16.0 --ignore-scripts
- name: Install pinned npm and pnpm
run: npm install -g npm@11.16.0 pnpm@10.28.2 --ignore-scripts
- name: Install controlled Linux search helpers

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
inputs.source_commit
. (
workflow_dispatch
).
Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
inputs.source_commit
. (
workflow_dispatch
).
if: runner.os == 'Linux'
run: |
sudo apt-get update
Expand Down
11 changes: 7 additions & 4 deletions docs/release/artifact-acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
The release gate installs every package returned by `scripts/release-packages.mjs` from its reviewed local tarball.
Each package receives a separate consumer with only its public dependency closure, ordered before the package that requires it.
Consumers live outside the repository in paths containing spaces, with separate homes, configuration, projects, temporary files, and package prefixes.
Each manager starts with an empty registry cache shared only among consumers in that acceptance run, and Bun registry concurrency is bounded.
npm and Bun start with an empty registry cache shared only among consumers in that acceptance run, and Bun registry concurrency is bounded.
pnpm consumers use isolated stores, homes, configuration and global executable directories; no user store or credentials are inherited.
Installed file identities are rechecked in every consumer even when downloaded dependencies are cached.
Lifecycle scripts are disabled and the Diffex registry scope points to an unavailable loopback endpoint, so a missing local override fails instead of fetching an older Diffex package.
External npm dependencies still resolve from the public registry.
Expand Down Expand Up @@ -39,13 +40,15 @@ Malformed and foreign metadata, redirects, outages, no release, and checksum mis
An interrupted npm download demonstrates the documented pinned repair and, when npm reports `ENOTEMPTY`, relocation of the affected CLI installation or its exact stale backup before retrying.
Interruption itself can mutate npm's prefix and is not claimed to preserve the executable.
Uninstall verifies executable removal and retained global and project data.
The same harness verifies Bun global installation and PATH, rejects a foreign release identity without mutation, upgrades with the real Bun subprocess, checks the installed runtime hashes, and uninstalls while retaining user and project data.
The same harness verifies Bun and pnpm global installation and PATH, rejects a foreign release identity without mutation, upgrades with real package-manager subprocesses, checks the installed runtime hashes, and uninstalls while retaining user and project data.
pnpm upgrades reuse the same store location with its contents cleared, preserving store identity while requiring fresh package downloads.
pnpm's shell executable shim and package links are checked against the isolated consumer rather than treated as npm-style direct executable symlinks.
An isolated Bun global scope registry ensures that the upgrade receives the reviewed candidate bytes.
`npm-lifecycle.json` records commands, identities, results, limitations, and cleanup.

## Supported distribution scope

The approved channel remains npm, installed with npm or Bun and executed on Node 22.19.0 or newer.
The approved channel remains the npm registry, installed with npm, pnpm or Bun and executed on Node 22.19.0 or newer.
Bun package installation is distinct from a Bun standalone executable.
Standalone publication remains deferred for every platform under the approved redistribution policy, and the manifest advertises no binary assets or binary platforms.
Windows npm targets are now explicitly deferred until equivalent terminal acceptance is available.
Expand Down Expand Up @@ -87,7 +90,7 @@ Public acceptance must compare the actual downloaded npm bytes with these retain

The baseline CLI-only acceptance passed on Darwin arm64 but emitted no per-package operation or hosted-server result.
The previous expanded acceptance covered eleven packages and the hosted server lifecycle before the owner reduced the public inventory on 2026-09-13.
Current acceptance validates the complete CLI-only tarball through both managers.
Current acceptance validates the complete CLI-only tarball through npm, pnpm and Bun.
The owner specified that no live provider test environment is available, so real login and inference remain pending.
Native runner results and exact committed-candidate evidence are recorded in the implementation pull request and accompanying acceptance record.
Public downloads and public-to-public upgrades remain unexecuted until publication; local fixture inference does not establish those results.
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Changed

- Added a compact terminal activity display for npm and pnpm self-updates, while preserving package-manager warnings, failure diagnostics, and redirected logs.
- Added pnpm installation, upgrade, integrity, and removal coverage to release acceptance alongside npm and Bun.

### Fixed

Expand Down
20 changes: 12 additions & 8 deletions scripts/artifact-acceptance.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assertContained, installConsumer, isolatedEnvironment, verifyTarballs } from "./artifact-installation.mjs";
import { ACCEPTANCE_MANAGERS, assertContained, installConsumer, isolatedEnvironment, verifyTarballs } from "./artifact-installation.mjs";
export { assertContained, installConsumer, isolatedEnvironment, verifyTarballs } from "./artifact-installation.mjs";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
Expand Down Expand Up @@ -104,10 +104,10 @@ export const operations = {
"@diffexai/diffex-coding-agent-server": `const reporter = api.createDurableUsageReporter({ endpoint: "http://127.0.0.1:1/usage", sharedSecret: "local-artifact-fixture-only-32-bytes", outboxDirectory: path.join(process.env.DIFFEX_CODING_AGENT_DIR, "outbox"), fetch: () => { throw new Error("Unexpected usage"); } }); await reporter.close();`,
};

export function acceptArtifacts(artifactDirectory, outputDirectory, { managers = ["npm", "bun"] } = {}) {
export function acceptArtifacts(artifactDirectory, outputDirectory, { managers = ACCEPTANCE_MANAGERS } = {}) {
assert(
managers.length && managers.every((manager) => ["npm", "bun"].includes(manager)),
"Expected npm or Bun acceptance",
managers.length && managers.every((manager) => ACCEPTANCE_MANAGERS.includes(manager)),
"Expected npm, pnpm or Bun acceptance",
);
const packages = getPublicWorkspacePackages().map((pkg) => ({
...cliDistribution(root).manifest,
Expand Down Expand Up @@ -138,8 +138,10 @@ export function acceptArtifacts(artifactDirectory, outputDirectory, { managers =
assert(operations[pkg.name], `Missing representative operation: ${pkg.name}`);
const consumer = join(temporary, manager, pkg.name.split("/").pop());
const selected = packageClosure(pkg.name, packages);
const env = installConsumer(consumer, manager, selected, records, tarballDirectory, cacheDirectory);
const packageRoot = join(consumer, "node_modules", pkg.name);
const env = installConsumer(consumer, manager, selected, records, tarballDirectory,
manager === "pnpm" ? join(consumer, "cache") : cacheDirectory);
// createRequire preserves an artificial filename's lexical path; use pnpm's real package root.
const packageRoot = realpathSync(join(consumer, "node_modules", pkg.name));
const imports = declaredEntrypoints(pkg, packageRoot);
const source = [
`import assert from "node:assert/strict"; import path from "node:path"; import { createRequire } from "node:module"; import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs";`,
Expand All @@ -163,7 +165,7 @@ export function acceptArtifacts(artifactDirectory, outputDirectory, { managers =
});
evidence.packages.push({
manager,
managerVersion: execFileSync(manager, ["--version"], { encoding: "utf8" }).trim(),
managerVersion: execFileSync(manager, ["--version"], { cwd: consumer, env, encoding: "utf8" }).trim(),
name: pkg.name,
version: pkg.version,
entrypoints: imports,
Expand All @@ -173,7 +175,8 @@ export function acceptArtifacts(artifactDirectory, outputDirectory, { managers =
console.log(`Accepted ${manager}: ${pkg.name}`);
}
const consumer = join(temporary, manager, "process acceptance");
const env = installConsumer(consumer, manager, packages, records, tarballDirectory, cacheDirectory);
const env = installConsumer(consumer, manager, packages, records, tarballDirectory,
manager === "pnpm" ? join(consumer, "cache") : cacheDirectory);
const project = join(consumer, "project");
writeFileSync(join(project, "artifact helper.txt"), "artifact-helper-confirmation\n");
const helperOptions = { cwd: project, env, encoding: "utf8", timeout: 10000 };
Expand Down Expand Up @@ -217,6 +220,7 @@ export function acceptArtifacts(artifactDirectory, outputDirectory, { managers =
join(tarballDirectory, cli.filename),
outputDirectory,
...(managers.includes("bun") ? [] : ["--skip-bun"]),
...(managers.includes("pnpm") ? [] : ["--skip-pnpm"]),
],
{ cwd: root, stdio: "inherit", timeout: 3_600_000 },
);
Expand Down
4 changes: 2 additions & 2 deletions scripts/artifact-acceptance.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
Expand Down Expand Up @@ -72,7 +72,7 @@ test("declared exports include patterns and reject missing declarations and esca
const env = isolatedEnvironment(consumer);
assert.equal(env.NODE_PATH, undefined);
assert.equal(env.NODE_OPTIONS, undefined);
assert.equal(env.HOME, join(consumer, "home"));
assert.equal(env.HOME, join(realpathSync(consumer), "home"));
} finally {
rmSync(directory, { recursive: true, force: true });
}
Expand Down
74 changes: 64 additions & 10 deletions scripts/artifact-installation.mjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
export const PNPM_VERSION = "10.28.2";
export const ACCEPTANCE_MANAGERS = ["npm", "pnpm", "bun"];

const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
const readJson = (file) => JSON.parse(readFileSync(file, "utf8"));

export function resolveArtifactDirectory(value) {
assert(typeof value === "string" && value.length > 0 && !value.includes("\0"), "Invalid artifact directory");
const directory = resolve(value);
for (const base of [process.cwd(), tmpdir(), homedir(), "/tmp"]) {
for (const base of [process.cwd(), tmpdir(), realpathSync(tmpdir()), homedir(), "/tmp"]) {
const root = resolve(base);
if (!directory.startsWith(root + sep)) continue;
let current = root;
Expand Down Expand Up @@ -47,8 +50,12 @@ export function verifyTarballs(directory, records, packages) {
}
}

export function isolatedEnvironment(directory) {
export function isolatedEnvironment(directory, manager = "npm") {
assert(ACCEPTANCE_MANAGERS.includes(manager), "Unsupported package manager");
directory = resolveArtifactDirectory(directory);
mkdirSync(directory, { recursive: true });
// pnpm records absolute virtual-store paths; macOS /var and /private/var must not mix.
directory = realpathSync(directory);
// Read literal allowlisted names: dynamic environment lookups can accidentally
// admit credentials when this helper is reused by installation/lifecycle code.
const env = Object.fromEntries(Object.entries({
Expand All @@ -59,8 +66,10 @@ export function isolatedEnvironment(directory) {
PATHEXT: process.env.PATHEXT,
LANG: process.env.LANG,
}).filter(([, value]) => typeof value === "string"));
for (const dir of ["home", "config", "cache", "prefix", "project", "agent", "temp"])
for (const dir of ["home", "config", "cache", "prefix", "project", "agent", "temp", "pnpm/bin"])
mkdirSync(resolveArtifactDirectory(join(directory, dir)), { recursive: true });
writeFileSync(join(directory, "empty.npmrc"), "");
writeFileSync(join(directory, "empty-global.npmrc"), "");
return {
...env,
HOME: join(directory, "home"),
Expand All @@ -73,6 +82,14 @@ export function isolatedEnvironment(directory) {
npm_config_globalconfig: join(directory, "empty-global.npmrc"),
npm_config_registry: "https://registry.npmjs.org",
npm_config_update_notifier: "false",
...(manager === "pnpm" ? {
PNPM_HOME: join(directory, "pnpm", "bin"),
npm_config_global_bin_dir: join(directory, "pnpm", "bin"),
npm_config_global_dir: join(directory, "pnpm", "global"),
npm_config_store_dir: join(directory, "cache", "pnpm"),
npm_config_manage_package_manager_versions: "false",
npm_config_package_manager_strict: "false",
} : {}),
BUN_INSTALL_CACHE_DIR: join(directory, "cache", "bun"),
TMPDIR: join(directory, "temp"),
TMP: join(directory, "temp"),
Expand All @@ -95,14 +112,19 @@ export function installConsumer(
directory = resolveArtifactDirectory(directory);
cacheDirectory = resolveArtifactDirectory(cacheDirectory);
tarballDirectory = resolveArtifactDirectory(tarballDirectory);
assert(manager === "npm" || manager === "bun", "Unsupported package manager");
const command = manager === "npm" ? "npm" : "bun";
assert(ACCEPTANCE_MANAGERS.includes(manager), "Unsupported package manager");
const command = manager;
assert(selected.every((pkg) => typeof pkg.name === "string" && /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(pkg.name)), "Invalid package name");
mkdirSync(directory, { recursive: true });
const env = isolatedEnvironment(directory);
const env = isolatedEnvironment(directory, manager);
mkdirSync(cacheDirectory, { recursive: true });
env.npm_config_cache = join(cacheDirectory, "npm");
env.BUN_INSTALL_CACHE_DIR = join(cacheDirectory, "bun");
if (manager === "pnpm") {
env.npm_config_store_dir = join(realpathSync(cacheDirectory), "pnpm");
assert(!existsSync(env.npm_config_store_dir), "pnpm consumer acceptance requires an empty store");
assertPnpmVersion(directory, env);
}
writeFileSync(join(directory, ".npmrc"), "@diffexai:registry=http://127.0.0.1:9/\n");
const dependencies = Object.fromEntries(
selected.map((pkg) => [
Expand All @@ -112,15 +134,22 @@ export function installConsumer(
);
writeFileSync(
join(directory, "package.json"),
JSON.stringify({ private: true, type: "module", dependencies, overrides: dependencies }),
JSON.stringify({ private: true, type: "module", dependencies, overrides: dependencies, pnpm: { overrides: dependencies } }),
);
execFileSync(
command,
manager === "npm"
? ["install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund"]
: ["install", "--production", "--ignore-scripts", "--network-concurrency=8"],
: manager === "pnpm"
? ["install", "--prod", "--ignore-scripts", "--no-frozen-lockfile"]
: ["install", "--production", "--ignore-scripts", "--network-concurrency=8"],
{ cwd: directory, env, stdio: "inherit", timeout: 600_000 },
);
verifyInstalledConsumer(directory, selected, records, tarballDirectory);
return env;
}

export function verifyInstalledConsumer(directory, selected, records, tarballDirectory) {
for (const pkg of selected) {
const installed = join(directory, "node_modules", pkg.name);
assertContained(installed, directory);
Expand Down Expand Up @@ -149,5 +178,30 @@ export function installConsumer(
);
}
}
return env;
}

export function assertPnpmVersion(directory, env) {
let version;
const remedy = `Install it with: npm install --global --ignore-scripts pnpm@${PNPM_VERSION}`;
try {
version = execFileSync("pnpm", ["--version"], { cwd: directory, env, encoding: "utf8", timeout: 30000 }).trim();
} catch (cause) {
throw new Error(`Acceptance requires pnpm@${PNPM_VERSION} on PATH. ${remedy}`, { cause });
}
assert.equal(version, PNPM_VERSION, `Acceptance requires pnpm@${PNPM_VERSION}; found ${version}. ${remedy}`);
return version;
}

export function globalManagerLayout(manager, directory, env) {
assert(ACCEPTANCE_MANAGERS.includes(manager), "Unsupported package manager");
const bin = manager === "npm" ? join(env.npm_config_prefix, "bin")
: manager === "pnpm" ? env.PNPM_HOME : env.BUN_INSTALL_BIN;
const modules = manager === "npm" ? join(env.npm_config_prefix, "lib/node_modules")
: manager === "pnpm" ? execFileSync("pnpm", ["root", "--global"], {
cwd: directory, env: { ...env, PATH: `${bin}:${env.PATH}` }, encoding: "utf8", timeout: 30000,
}).trim() : join(env.BUN_INSTALL_GLOBAL_DIR, "node_modules");
assert(isAbsolute(modules), `Invalid ${manager} global module directory: ${modules}`);
const local = relative(realpathSync(directory), modules);
assert(local && !local.startsWith("..") && !isAbsolute(local), `${manager} global root escapes isolation`);
return { bin, modules, packageRoot: join(modules, "@diffexai/diffex"), executable: join(bin, "diffex") };
}
Loading
Loading