Skip to content

Commit c5cdb06

Browse files
committed
Implement retry for CDS compilation tasks
This commit updates the implementation of the CDS extractor in order to attempt to retry failed compilation tasks using the full set of node dependencies specified by the `package.json` config file of the associated CDS project. The new implementation continues to use a minimal cache of `@sap/cds` and `@sap/cds-dk` dependencies as the default/initial installation behavior as a performance optimization. When this does not work, the CDS extractor should now attempt to install all node dependencies for the associated (CAP) project before re-attempting the failed compilation tasks(s) for that project. This commit also updates the `package.json` config for the CDS extractor itself in order to ensure that we build test code before running unit tests, which addresses a bug in the previous testing implementation. The test code is now automatically compiled as part of running commands like `npm run test` or `npm run test:coverage`.
1 parent f31e5c7 commit c5cdb06

File tree

18 files changed

+2099
-113
lines changed

18 files changed

+2099
-113
lines changed

extractors/cds/tools/dist/cds-extractor.bundle.js

Lines changed: 626 additions & 98 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extractors/cds/tools/dist/cds-extractor.bundle.js.map

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extractors/cds/tools/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@
1010
"scripts": {
1111
"build": "node esbuild.config.mjs",
1212
"build:all": "npm run lint:fix && npm run test:coverage && npm run build:validate",
13+
"build:tests": "tsc --noEmit",
1314
"build:validate": "npm run prebuild && npm run build && npm run bundle:validate",
1415
"bundle:validate": "node validate-bundle.js",
1516
"clean": "rm -rf coverage dist",
1617
"lint": "eslint --ext .ts cds-extractor.ts src/ test/src/",
1718
"lint:fix": "eslint --ext .ts --fix cds-extractor.ts src/ test/src/",
1819
"format": "prettier --write 'src/**/*.ts'",
1920
"prebuild": "npm run clean",
20-
"test": "jest",
21-
"test:watch": "jest --watch",
22-
"test:coverage": "jest --coverage --collectCoverageFrom='src/**/*.ts'"
21+
"test": "npm run build:tests && jest",
22+
"test:watch": "npm run build:tests && jest --watch",
23+
"test:coverage": "npm run build:tests && jest --coverage --collectCoverageFrom='src/**/*.ts'"
2324
},
2425
"dependencies": {
2526
"child_process": "^1.0.2",

extractors/cds/tools/src/cds/compiler/graph.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { determineCdsCommand } from './command';
22
import { compileCdsToJson } from './compile';
3+
import { orchestrateRetryAttempts } from './retry';
34
import { CompilationAttempt, CompilationTask, CompilationConfig } from './types';
4-
import { addCompilationDiagnostic } from '../../diagnostics';
55
import { cdsExtractorLog, generateStatusReport } from '../../logging';
66
import { CdsDependencyGraph, CdsProject } from '../parser/types';
77

@@ -127,7 +127,7 @@ function executeCompilationTask(
127127
task: CompilationTask,
128128
project: CdsProject,
129129
dependencyGraph: CdsDependencyGraph,
130-
codeqlExePath: string,
130+
_codeqlExePath: string,
131131
): void {
132132
task.status = 'in_progress';
133133

@@ -158,10 +158,8 @@ function executeCompilationTask(
158158
task.errorSummary = lastError?.message || 'Compilation failed';
159159
dependencyGraph.statusSummary.failedCompilations++;
160160

161-
// Add diagnostic for failed compilation
162-
for (const sourceFile of task.sourceFiles) {
163-
addCompilationDiagnostic(sourceFile, task.errorSummary, codeqlExePath);
164-
}
161+
// Note: Diagnostics are deferred until after retry phase completes
162+
// to implement "Silent Success" - only add diagnostics for definitively failed tasks
165163

166164
cdsExtractorLog('error', `Compilation failed for task ${task.id}: ${task.errorSummary}`);
167165
}
@@ -252,11 +250,29 @@ export function orchestrateCompilation(
252250
codeqlExePath: string,
253251
): void {
254252
try {
253+
// Phase 1: Initial compilation
255254
planCompilationTasks(dependencyGraph, projectCacheDirMap);
256-
257255
executeCompilationTasks(dependencyGraph, codeqlExePath);
258256

259-
// Update overall status
257+
// Phase 2: Retry orchestration
258+
cdsExtractorLog('info', 'Starting retry orchestration phase...');
259+
const retryResults = orchestrateRetryAttempts(
260+
dependencyGraph,
261+
projectCacheDirMap,
262+
codeqlExePath,
263+
);
264+
265+
// Log retry results
266+
if (retryResults.totalTasksRequiringRetry > 0) {
267+
cdsExtractorLog(
268+
'info',
269+
`Retry phase completed: ${retryResults.totalTasksRequiringRetry} tasks retried, ${retryResults.totalSuccessfulRetries} successful, ${retryResults.totalFailedRetries} failed`,
270+
);
271+
} else {
272+
cdsExtractorLog('info', 'Retry phase completed: no tasks required retry');
273+
}
274+
275+
// Phase 3: Final status update
260276
const hasFailures =
261277
dependencyGraph.statusSummary.failedCompilations > 0 ||
262278
dependencyGraph.errors.critical.length > 0;

extractors/cds/tools/src/cds/compiler/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ export { determineCdsCommand } from './command';
22
export { compileCdsToJson } from './compile';
33
export { orchestrateCompilation } from './graph';
44
export { findProjectForCdsFile } from './project';
5+
export { orchestrateRetryAttempts } from './retry';
56
export { getCdsVersion } from './version';
7+
export { validateOutputFile, validateTaskOutputs, identifyTasksRequiringRetry } from './validator';
8+
export { installFullDependencies } from './installer';
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/** Full dependency installation utilities for retry scenarios. */
2+
3+
import { execFileSync } from 'child_process';
4+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
5+
import { join } from 'path';
6+
7+
import { cdsExtractorLog } from '../../logging';
8+
import type { CdsProject } from '../parser/types';
9+
10+
/** Result of full dependency installation for a project */
11+
export interface FullDependencyInstallationResult {
12+
/** Whether installation was successful */
13+
success: boolean;
14+
/** Path to the retry cache directory */
15+
retryCacheDir: string;
16+
/** Installation error message if failed */
17+
error?: string;
18+
/** Warnings during installation */
19+
warnings: string[];
20+
/** Duration of installation in milliseconds */
21+
durationMs: number;
22+
/** Whether a timeout occurred */
23+
timedOut: boolean;
24+
}
25+
26+
/**
27+
* Installs full dependencies for a project using its package.json
28+
* @param project The CDS project to install dependencies for
29+
* @param sourceRoot Source root directory
30+
* @param codeqlExePath Path to the CodeQL executable for diagnostics
31+
* @returns Installation result with details
32+
*/
33+
export function installFullDependencies(
34+
project: CdsProject,
35+
sourceRoot: string,
36+
_codeqlExePath: string,
37+
): FullDependencyInstallationResult {
38+
const startTime = Date.now();
39+
40+
const result: FullDependencyInstallationResult = {
41+
success: false,
42+
retryCacheDir: '',
43+
warnings: [],
44+
durationMs: 0,
45+
timedOut: false,
46+
};
47+
48+
try {
49+
// Create retry-specific cache directory
50+
const retryCacheDir = createRetryCacheDirectory(project, sourceRoot);
51+
result.retryCacheDir = retryCacheDir;
52+
53+
// Create package.json in retry cache directory
54+
if (!createPackageJsonForRetry(project, sourceRoot, retryCacheDir)) {
55+
result.error = 'Failed to create package.json for retry';
56+
return result;
57+
}
58+
59+
// Install dependencies using npm
60+
cdsExtractorLog(
61+
'info',
62+
`Installing full dependencies for project ${project.projectDir} in retry cache directory`,
63+
);
64+
65+
try {
66+
execFileSync('npm', ['install', '--quiet', '--no-audit', '--no-fund'], {
67+
cwd: retryCacheDir,
68+
stdio: 'inherit',
69+
timeout: 120000, // 2-minute timeout
70+
});
71+
72+
result.success = true;
73+
cdsExtractorLog(
74+
'info',
75+
`Successfully installed full dependencies for project ${project.projectDir}`,
76+
);
77+
} catch (execError) {
78+
if (execError instanceof Error && 'signal' in execError && execError.signal === 'SIGTERM') {
79+
result.timedOut = true;
80+
result.error = 'Dependency installation timed out';
81+
} else {
82+
result.error = `npm install failed: ${String(execError)}`;
83+
}
84+
85+
// Still attempt retry compilation even if dependency installation fails (optimistic approach)
86+
result.warnings.push(
87+
`Dependency installation failed but will still attempt retry compilation: ${result.error}`,
88+
);
89+
cdsExtractorLog('warn', result.warnings[0]);
90+
}
91+
} catch (error) {
92+
result.error = `Failed to install full dependencies: ${String(error)}`;
93+
cdsExtractorLog('error', result.error);
94+
} finally {
95+
result.durationMs = Date.now() - startTime;
96+
}
97+
98+
return result;
99+
}
100+
101+
/**
102+
* Determines if a project needs full dependency installation
103+
* @param project The CDS project to check
104+
* @returns Whether full dependency installation is needed
105+
*/
106+
export function needsFullDependencyInstallation(project: CdsProject): boolean {
107+
// Check if already installed
108+
if (project.retryStatus?.fullDependenciesInstalled) {
109+
return false;
110+
}
111+
112+
// Check if project has failed tasks that could benefit from full dependencies
113+
const hasFailedTasks = project.compilationTasks.some(
114+
task => task.status === 'failed' && !task.retryInfo?.hasBeenRetried,
115+
);
116+
117+
return hasFailedTasks && project.packageJson !== undefined;
118+
}
119+
120+
/**
121+
* Creates retry-specific cache directory for a project
122+
* @param project The CDS project
123+
* @param sourceRoot Source root directory
124+
* @returns Path to the created retry cache directory
125+
*/
126+
export function createRetryCacheDirectory(project: CdsProject, sourceRoot: string): string {
127+
const cacheSubDirName = '.cds-extractor-cache';
128+
const cacheRootDir = join(sourceRoot, cacheSubDirName);
129+
130+
// Generate unique retry cache directory name
131+
const projectHash = Buffer.from(project.projectDir).toString('base64').replace(/[/+=]/g, '_');
132+
const timestamp = Date.now();
133+
const retryCacheDirName = `retry-${projectHash}-${timestamp}`;
134+
const retryCacheDir = join(cacheRootDir, retryCacheDirName);
135+
136+
// Create cache root directory if it doesn't exist
137+
if (!existsSync(cacheRootDir)) {
138+
try {
139+
mkdirSync(cacheRootDir, { recursive: true });
140+
cdsExtractorLog('info', `Created cache root directory: ${cacheRootDir}`);
141+
} catch (error) {
142+
throw new Error(`Failed to create cache root directory: ${String(error)}`);
143+
}
144+
}
145+
146+
// Create retry-specific cache directory
147+
try {
148+
mkdirSync(retryCacheDir, { recursive: true });
149+
cdsExtractorLog('info', `Created retry cache directory: ${retryCacheDirName}`);
150+
} catch (error) {
151+
throw new Error(`Failed to create retry cache directory: ${String(error)}`);
152+
}
153+
154+
return retryCacheDir;
155+
}
156+
157+
/**
158+
* Creates a package.json file in the retry cache directory based on the project's original package.json
159+
* @param project The CDS project
160+
* @param sourceRoot Source root directory
161+
* @param retryCacheDir Path to the retry cache directory
162+
* @returns Whether package.json creation was successful
163+
*/
164+
function createPackageJsonForRetry(
165+
project: CdsProject,
166+
sourceRoot: string,
167+
retryCacheDir: string,
168+
): boolean {
169+
if (!project.packageJson) {
170+
cdsExtractorLog('warn', `No package.json found for project ${project.projectDir}`);
171+
return false;
172+
}
173+
174+
try {
175+
// Check if original package-lock.json exists
176+
const originalPackageLockPath = join(sourceRoot, project.projectDir, 'package-lock.json');
177+
let packageLockContent: unknown = undefined;
178+
179+
if (existsSync(originalPackageLockPath)) {
180+
try {
181+
const lockContent = readFileSync(originalPackageLockPath, 'utf8');
182+
packageLockContent = JSON.parse(lockContent);
183+
cdsExtractorLog('info', `Found package-lock.json for project ${project.projectDir}`);
184+
} catch (error) {
185+
cdsExtractorLog(
186+
'warn',
187+
`Failed to read package-lock.json for project ${project.projectDir}: ${String(error)}`,
188+
);
189+
}
190+
}
191+
192+
// Create package.json with all dependencies
193+
const retryPackageJson: Record<string, unknown> = {
194+
name: `${project.packageJson.name ?? 'unknown'}-retry`,
195+
version: project.packageJson.version ?? '1.0.0',
196+
private: true,
197+
dependencies: {
198+
...(project.packageJson.dependencies ?? {}),
199+
...(project.packageJson.devDependencies ?? {}), // Include dev dependencies as dependencies
200+
},
201+
};
202+
203+
// Copy other relevant fields that might affect dependency resolution
204+
if (project.packageJson.engines) {
205+
retryPackageJson.engines = project.packageJson.engines;
206+
}
207+
if (project.packageJson.peerDependencies) {
208+
retryPackageJson.peerDependencies = project.packageJson.peerDependencies;
209+
}
210+
211+
// Write package.json
212+
const packageJsonPath = join(retryCacheDir, 'package.json');
213+
writeFileSync(packageJsonPath, JSON.stringify(retryPackageJson, null, 2));
214+
cdsExtractorLog('info', `Created retry package.json for project ${project.projectDir}`);
215+
216+
// Copy package-lock.json if it exists
217+
if (packageLockContent) {
218+
const packageLockPath = join(retryCacheDir, 'package-lock.json');
219+
writeFileSync(packageLockPath, JSON.stringify(packageLockContent, null, 2));
220+
cdsExtractorLog('info', `Copied package-lock.json for project ${project.projectDir}`);
221+
}
222+
223+
return true;
224+
} catch (error) {
225+
cdsExtractorLog('error', `Failed to create package.json for retry: ${String(error)}`);
226+
return false;
227+
}
228+
}

0 commit comments

Comments
 (0)