GitNexus/.devcontainer/dependencies.cjs
Gergő Magyar b961e695ca
Some checks failed
Devcontainer Smoke / Config-transform unit tests (push) Has been cancelled
Devcontainer Smoke / Build devcontainer image (default) (push) Has been cancelled
Publish Development Images / publish (core) (push) Has been cancelled
Publish Development Images / publish (ui) (push) Has been cancelled
Devcontainer Smoke / Build devcontainer image (ui) (push) Has been cancelled
CodeQL / Analyze (python) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / ci (push) Blocked by required conditions
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Skill copy sync / shipped skills drift guard (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
fix(devcontainer): refresh Node and streamline development (#3610)
2026-10-17 21:05:13 +00:00

139 lines
4.9 KiB
JavaScript

'use strict';
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { execFileSync } = require('node:child_process');
const markerName = '.gitnexus-devcontainer-install.json';
const nativeInputs = [
'vendor',
'prebuilds/bundled',
'scripts/build-tree-sitter-grammars.cjs',
'scripts/prepare-tree-sitter-bundle.cjs',
'scripts/arm64-prebuild.cjs',
];
function fingerprint(directory, runtime = process) {
const hash = crypto.createHash('sha256');
hash.update(
JSON.stringify([
3,
runtime.version,
runtime.versions.modules,
runtime.platform,
runtime.arch,
runtime.npmVersion,
]),
);
for (const file of ['package.json', 'package-lock.json']) {
hash.update(file);
hash.update(fs.readFileSync(path.join(directory, file)));
}
const manifest = JSON.parse(fs.readFileSync(path.join(directory, 'package.json')));
for (const dependency of Object.values({
...manifest.dependencies,
...manifest.devDependencies,
...manifest.optionalDependencies,
})) {
if (typeof dependency !== 'string' || !dependency.startsWith('file:')) continue;
const sibling = path.resolve(directory, dependency.slice(5), 'package.json');
hash.update(fs.readFileSync(sibling));
}
if (fs.existsSync(path.join(directory, 'scripts/build-tree-sitter-grammars.cjs'))) {
hash.update(
JSON.stringify({ skipOptionalGrammars: process.env.GITNEXUS_SKIP_OPTIONAL_GRAMMARS === '1' }),
);
// Blob IDs cover committed native inputs; the diff covers local tracked
// edits. Do not read npm credential files or hash large unchanged prebuilds.
for (const args of [
['ls-files', '-s', '--', ...nativeInputs],
['diff', 'HEAD', '--', ...nativeInputs],
]) {
hash.update(execFileSync('git', args, { cwd: directory, maxBuffer: 32 * 1024 * 1024 }));
}
}
return hash.digest('hex');
}
function ensureDependencies(directory, { run = runNpm, force = false, runtime = process } = {}) {
const expected = fingerprint(directory, runtime);
const marker = path.join(directory, 'node_modules', markerName);
const manifest = JSON.parse(fs.readFileSync(path.join(directory, 'package.json')));
const packages = Object.keys({ ...manifest.dependencies, ...manifest.devDependencies });
let saved;
try {
saved = JSON.parse(fs.readFileSync(marker, 'utf8'));
} catch {
/* First or interrupted install. */
}
const present = packages.every((name) =>
fs.existsSync(path.join(directory, 'node_modules', name, 'package.json')),
);
if (!force && present && saved?.fingerprint === expected) {
console.log(
`[install-deps] ${path.basename(directory)} dependencies unchanged; reusing volume`,
);
return false;
}
// Invalidate before installing: a failed attempt must never leave a reusable receipt.
fs.rmSync(marker, { force: true });
run(directory, ['ci']);
fs.mkdirSync(path.dirname(marker), { recursive: true });
const temporary = `${marker}.tmp`;
fs.writeFileSync(temporary, JSON.stringify({ fingerprint: fingerprint(directory, runtime) }));
fs.renameSync(temporary, marker);
return true;
}
function runNpm(directory, args) {
execFileSync('npm', args, { cwd: directory, stdio: 'inherit' });
}
function timed(label, action) {
const start = performance.now();
try {
return action();
} finally {
console.log(`[install-deps] ${label}: ${((performance.now() - start) / 1000).toFixed(2)}s`);
}
}
module.exports = { fingerprint, ensureDependencies, markerName };
if (require.main === module) {
const root = path.resolve(process.argv[2] || '/workspace');
const runtime = {
version: process.version,
versions: process.versions,
platform: process.platform,
arch: process.arch,
npmVersion: execFileSync('npm', ['--version'], { encoding: 'utf8' }).trim(),
};
const ensure = (directory) =>
timed(`dependencies ${path.basename(directory)}`, () =>
ensureDependencies(directory, { force: process.env.GITNEXUS_FORCE_DEPS === '1', runtime }),
);
ensure(root);
// Always restore and verify hooks, including the cached-install path.
runNpm(root, ['run', 'prepare']);
require('./verify-git-hooks.cjs').verifyGitHooks(root);
console.log('[install-deps] Husky hooks verified');
const shared = path.join(root, 'gitnexus-shared');
ensure(shared);
timed('shared build', () => runNpm(shared, ['run', 'build']));
const web = path.join(root, 'gitnexus-web');
ensure(web);
const core = path.join(root, 'gitnexus');
// A fresh npm ci already builds via prepare. A reused install still needs
// source compilation, even when manifests and lockfiles have not changed.
if (!ensure(core)) timed('CLI source build', () => runNpm(core, ['run', 'build']));
if (process.env.GITNEXUS_DEV_UI === '1') {
timed('Chromium provisioning', () =>
execFileSync(path.join(web, 'node_modules/.bin/playwright'), ['install', 'chromium'], {
cwd: web,
stdio: 'inherit',
}),
);
}
}