diff --git a/config/hwlab-node-control-plane.yaml b/config/hwlab-node-control-plane.yaml index 135a7f6d..e498c938 100644 --- a/config/hwlab-node-control-plane.yaml +++ b/config/hwlab-node-control-plane.yaml @@ -152,6 +152,8 @@ targets: - COPY --from=python-runtime /usr/local /usr/local - COPY --from=docker-cli-runtime /usr/local/bin/docker /usr/local/bin/docker - RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx + - ENV NODE_PATH=/usr/local/lib/node_modules + - RUN npm install -g --omit=dev --ignore-scripts --no-audit --no-fund yaml@2.8.3 && NODE_PATH=/usr/local/lib/node_modules node -e "require.resolve('yaml')" - RUN node --version && npm --version && bun --version && git --version && python3 --version && docker --version && ssh -V buildArgs: NODE_IMAGE: docker.io/library/node:22-bookworm-slim diff --git a/scripts/src/hwlab-node-impl.ts b/scripts/src/hwlab-node-impl.ts index c1900d14..be1acf37 100644 --- a/scripts/src/hwlab-node-impl.ts +++ b/scripts/src/hwlab-node-impl.ts @@ -4948,60 +4948,18 @@ function nodeRuntimePipelinePostprocessScript(): string[] { "};", "const stepEnv = { ...proxyEnv, ...dockerProxyEnv, ...(overlay.stepEnv || {}) };", "function prepareSourceDependencyScript() {", - " const registry = String(overlay.npmRegistry || 'https://registry.npmjs.org/');", - " const timeoutSeconds = Math.max(15, Math.ceil(Number(overlay.npmFetchTimeoutMs || 120000) / 1000));", - " const retryCount = Math.max(0, Math.floor(Number(overlay.npmRetries || 3)));", " return `prepare_source_dependencies_started_ms=\"$(ci_now_ms)\"", "node <<'NODE_UNIDESK_YAML_DEPENDENCY'", - "const { spawnSync } = require('node:child_process');", - "const fs = require('node:fs');", - "const os = require('node:os');", - "const path = require('node:path');", - "const registry = ${JSON.stringify(registry)};", - "const timeoutMs = ${JSON.stringify(timeoutSeconds * 1000)};", - "const timeoutSeconds = ${JSON.stringify(timeoutSeconds)};", - "const retryCount = ${JSON.stringify(retryCount)};", "const dependency = 'yaml';", - "const version = '2.8.3';", "function emit(status, extra = {}) { console.error(JSON.stringify({ event: 'prepare-source-dependencies', status, dependency, ...extra })); }", - "function hasYaml() { try { require.resolve('yaml'); return true; } catch { return false; } }", - "function run(command, args) {", - " const result = spawnSync(command, args, { stdio: 'inherit', env: process.env, timeout: timeoutMs });", - " if (result.error) console.error(JSON.stringify({ event: 'prepare-source-dependencies', status: 'command-error', command, error: result.error.message }));", - " return result.status === 0;", + "try {", + " const resolved = require.resolve('yaml');", + " emit('cached', { manager: 'tools-image', resolved });", + " process.exit(0);", + "} catch (error) {", + " emit('failed', { reason: 'tools-image-missing-yaml', message: error && error.message ? error.message : String(error) });", + " process.exit(34);", "}", - "function tailNpmLog() {", - " const dir = path.join(process.env.HOME || '/tmp', '.npm', '_logs');", - " if (!fs.existsSync(dir)) return;", - " const files = fs.readdirSync(dir).filter((name) => name.includes('debug') && name.endsWith('.log')).sort();", - " const file = files[files.length - 1];", - " if (!file) return;", - " const full = path.join(dir, file);", - " console.error(JSON.stringify({ event: 'prepare-source-dependencies', status: 'npm-debug-log', path: full }));", - " const lines = fs.readFileSync(full, 'utf8').split(String.fromCharCode(10));", - " console.error(lines.slice(-80).join(String.fromCharCode(10)));", - "}", - "if (hasYaml()) { emit('cached'); process.exit(0); }", - "fs.mkdirSync('node_modules/yaml', { recursive: true });", - "let registryBase = registry;", - "while (registryBase.endsWith('/')) registryBase = registryBase.slice(0, -1);", - "const tarball = registryBase + '/yaml/-/yaml-' + version + '.tgz';", - "const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hwlab-yaml-'));", - "const tgz = path.join(tmpDir, 'yaml.tgz');", - "if (run('curl', ['-fsSL', '--retry', String(retryCount), '--connect-timeout', '10', '--max-time', String(timeoutSeconds), '-o', tgz, tarball]) && run('tar', ['-xzf', tgz, '-C', 'node_modules/yaml', '--strip-components=1'])) emit('installed', { manager: 'tarball' });", - "else emit('tarball-failed', { manager: 'tarball' });", - "fs.rmSync(tmpDir, { recursive: true, force: true });", - "if (!hasYaml()) {", - " fs.rmSync('node_modules/yaml', { recursive: true, force: true });", - " if (run('bun', ['add', '--no-save', '--ignore-scripts', '--registry', registry, 'yaml@' + version]) && hasYaml()) emit('installed', { manager: 'bun' });", - " else emit('bun-failed', { manager: 'bun' });", - "}", - "if (!hasYaml()) {", - " fs.rmSync('node_modules/yaml', { recursive: true, force: true });", - " if (run('npm', ['install', '--package-lock=false', '--no-save', '--ignore-scripts', '--no-audit', '--no-fund', '--omit=dev', '--registry', registry, 'yaml@' + version]) && hasYaml()) emit('installed', { manager: 'npm' });", - " else { emit('npm-failed', { manager: 'npm' }); tailNpmLog(); process.exit(34); }", - "}", - "if (!hasYaml()) { emit('failed', { reason: 'unresolved' }); process.exit(34); }", "NODE_UNIDESK_YAML_DEPENDENCY", "ci_timing_emit prepare-source-dependencies succeeded \"$prepare_source_dependencies_started_ms\"`;", "}", @@ -5902,6 +5860,7 @@ function nodeRuntimePipelinePostprocessScript(): string[] { "if (!structured && !changed && !text.includes(`--gitops-root ${quotedRoot}`) && !text.includes(`--gitops-root ${escapedQuotedRoot}`)) { throw new Error(`generated pipeline missing expected gitops-render invocation in ${pipelinePath}`); }", "if (text.includes('prepare_source_dependencies_started_ms=\"$(ci_now_ms)\"') && text.includes('npm ci --ignore-scripts --no-audit --prefer-offline')) { throw new Error(`generated pipeline still uses full npm ci prepare-source dependency install in ${pipelinePath}`); }", "if (text.includes('prepare_source_dependencies_started_ms=\"$(ci_now_ms)\"') && !text.includes('NODE_UNIDESK_YAML_DEPENDENCY')) { throw new Error(`generated pipeline missing UniDesk yaml dependency install in ${pipelinePath}`); }", + "if (text.includes('/yaml/-/yaml-') || text.includes('bun add --no-save --ignore-scripts') || text.includes('npm install --package-lock=false --no-save')) { throw new Error(`generated pipeline still downloads yaml during prepare-source in ${pipelinePath}`); }", "if (text.includes('npm run gitops:ts:check')) { throw new Error(`generated pipeline still uses npm gitops:ts:check gate in ${pipelinePath}`); }", "fs.writeFileSync(pipelinePath, text);", "patchGitMirrorTransportYaml();",