Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): handle sync generator failures #27650

Merged
merged 3 commits into from
Sep 10, 2024
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
98 changes: 89 additions & 9 deletions packages/nx/src/command-line/sync/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import { handleErrors } from '../../utils/params';
import {
collectAllRegisteredSyncGenerators,
flushSyncGeneratorChanges,
getFailedSyncGeneratorsFixMessageLines,
getFlushFailureMessageLines,
getSyncGeneratorChanges,
syncGeneratorResultsToMessageLines,
getSyncGeneratorSuccessResultsMessageLines,
processSyncGeneratorResultErrors,
} from '../../utils/sync-generators';
import type { SyncArgs } from './command-object';
import chalk = require('chalk');
Expand Down Expand Up @@ -52,29 +55,106 @@ export function syncHandler(options: SyncOptions): Promise<number> {
return 0;
}

const {
failedGeneratorsCount,
areAllResultsFailures,
anySyncGeneratorsFailed,
} = processSyncGeneratorResultErrors(results);
const failedSyncGeneratorsFixMessageLines =
getFailedSyncGeneratorsFixMessageLines(results, options.verbose);

if (areAllResultsFailures) {
output.error({
title: `The workspace is probably out of sync because ${
failedGeneratorsCount === 1
? 'a sync generator'
: 'some sync generators'
} failed to run`,
bodyLines: failedSyncGeneratorsFixMessageLines,
});

return 1;
}

const resultBodyLines = getSyncGeneratorSuccessResultsMessageLines(results);
if (options.check) {
output.error({
title: `The workspace is out of sync`,
bodyLines: syncGeneratorResultsToMessageLines(results),
title: 'The workspace is out of sync',
bodyLines: resultBodyLines,
});

if (anySyncGeneratorsFailed) {
output.error({
title:
failedGeneratorsCount === 1
? 'A sync generator failed to run'
: 'Some sync generators failed to run',
bodyLines: failedSyncGeneratorsFixMessageLines,
});
}

return 1;
}

output.warn({
title: `The workspace is out of sync`,
bodyLines: syncGeneratorResultsToMessageLines(results),
title: 'The workspace is out of sync',
bodyLines: resultBodyLines,
});

const spinner = ora('Syncing the workspace...');
spinner.start();

await flushSyncGeneratorChanges(results);
try {
const flushResult = await flushSyncGeneratorChanges(results);

if ('generatorFailures' in flushResult) {
spinner.fail();
output.error({
title: 'Failed to sync the workspace',
bodyLines: getFlushFailureMessageLines(flushResult, options.verbose),
});

return 1;
}
} catch (e) {
spinner.fail();
output.error({
title: 'Failed to sync the workspace',
bodyLines: [
'Syncing the workspace failed with the following error:',
'',
e.message,
...(options.verbose && !!e.stack ? [`\n${e.stack}`] : []),
'',
'Please rerun with `--verbose` and report the error at: https://github.com/nrwl/nx/issues/new/choose',
],
});

return 1;
}

spinner.succeed(`The workspace was synced successfully!
const successTitle = anySyncGeneratorsFailed
? // the identified changes were synced successfully, but the workspace
// is still not up to date, which we'll mention next
'The identified changes were synced successfully!'
: // the workspace is fully up to date
'The workspace was synced successfully!';
const successSubtitle =
'Please make sure to commit the changes to your repository.';
spinner.succeed(`${successTitle}\n\n${successSubtitle}`);

Please make sure to commit the changes to your repository.
`);
if (anySyncGeneratorsFailed) {
output.error({
title: `The workspace is probably still out of sync because ${
failedGeneratorsCount === 1
? 'a sync generator'
: 'some sync generators'
} failed to run`,
bodyLines: failedSyncGeneratorsFixMessageLines,
});

return 1;
}

return 0;
});
Expand Down
11 changes: 8 additions & 3 deletions packages/nx/src/daemon/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ import {
GET_SYNC_GENERATOR_CHANGES,
type HandleGetSyncGeneratorChangesMessage,
} from '../message-types/get-sync-generator-changes';
import type { SyncGeneratorChangesResult } from '../../utils/sync-generators';
import type {
FlushSyncGeneratorChangesResult,
SyncGeneratorRunResult,
} from '../../utils/sync-generators';
import {
GET_REGISTERED_SYNC_GENERATORS,
type HandleGetRegisteredSyncGeneratorsMessage,
Expand Down Expand Up @@ -364,15 +367,17 @@ export class DaemonClient {

getSyncGeneratorChanges(
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
): Promise<SyncGeneratorRunResult[]> {
const message: HandleGetSyncGeneratorChangesMessage = {
type: GET_SYNC_GENERATOR_CHANGES,
generators,
};
return this.sendToDaemonViaQueue(message);
}

flushSyncGeneratorChangesToDisk(generators: string[]): Promise<void> {
flushSyncGeneratorChangesToDisk(
generators: string[]
): Promise<FlushSyncGeneratorChangesResult> {
const message: HandleFlushSyncGeneratorChangesToDiskMessage = {
type: FLUSH_SYNC_GENERATOR_CHANGES_TO_DISK,
generators,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { flushSyncGeneratorChangesToDisk } from './sync-generators';
export async function handleFlushSyncGeneratorChangesToDisk(
generators: string[]
): Promise<HandlerResult> {
await flushSyncGeneratorChangesToDisk(generators);
const result = await flushSyncGeneratorChangesToDisk(generators);

return {
response: '{}',
response: JSON.stringify(result),
description: 'handleFlushSyncGeneratorChangesToDisk',
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ export async function handleGetSyncGeneratorChanges(
): Promise<HandlerResult> {
const changes = await getCachedSyncGeneratorChanges(generators);

// strip out the content of the changes and any potential callback
const result = changes.map((change) => ({
generatorName: change.generatorName,
changes: change.changes.map((c) => ({ ...c, content: null })),
outOfSyncMessage: change.outOfSyncMessage,
}));
const result = changes.map((change) =>
'error' in change
? change
: // strip out the content of the changes and any potential callback
{
generatorName: change.generatorName,
changes: change.changes.map((c) => ({ ...c, content: null })),
outOfSyncMessage: change.outOfSyncMessage,
}
);

return {
response: JSON.stringify(result),
Expand Down
4 changes: 2 additions & 2 deletions packages/nx/src/daemon/server/sync-generators.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { SyncGeneratorChangesResult } from '../../utils/sync-generators';
import type { SyncGeneratorRunResult } from '../../utils/sync-generators';
import { _getConflictingGeneratorGroups } from './sync-generators';

describe('_getConflictingGeneratorGroups', () => {
it('should return grouped conflicting generators', () => {
const results: SyncGeneratorChangesResult[] = [
const results: SyncGeneratorRunResult[] = [
{
generatorName: 'a',
changes: [
Expand Down
39 changes: 25 additions & 14 deletions packages/nx/src/daemon/server/sync-generators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ import {
collectRegisteredGlobalSyncGenerators,
flushSyncGeneratorChanges,
runSyncGenerator,
type SyncGeneratorChangesResult,
type FlushSyncGeneratorChangesResult,
type SyncGeneratorRunResult,
type SyncGeneratorRunSuccessResult,
} from '../../utils/sync-generators';
import { workspaceRoot } from '../../utils/workspace-root';
import { serverLogger } from './logger';
import { getCachedSerializedProjectGraphPromise } from './project-graph-incremental-recomputation';

const syncGeneratorsCacheResultPromises = new Map<
string,
Promise<SyncGeneratorChangesResult>
Promise<SyncGeneratorRunResult>
>();
let registeredTaskSyncGenerators = new Set<string>();
let registeredGlobalSyncGenerators = new Set<string>();
Expand All @@ -36,7 +38,7 @@ const log = (...messageParts: unknown[]) => {

export async function getCachedSyncGeneratorChanges(
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
): Promise<SyncGeneratorRunResult[]> {
try {
log('get sync generators changes on demand', generators);
// this is invoked imperatively, so we clear any scheduled run
Expand Down Expand Up @@ -70,7 +72,7 @@ export async function getCachedSyncGeneratorChanges(

export async function flushSyncGeneratorChangesToDisk(
generators: string[]
): Promise<void> {
): Promise<FlushSyncGeneratorChangesResult> {
log('flush sync generators changes', generators);

const results = await getCachedSyncGeneratorChanges(generators);
Expand All @@ -79,7 +81,7 @@ export async function flushSyncGeneratorChangesToDisk(
syncGeneratorsCacheResultPromises.delete(generator);
}

await flushSyncGeneratorChanges(results);
return await flushSyncGeneratorChanges(results);
}

export function collectAndScheduleSyncGenerators(
Expand Down Expand Up @@ -154,7 +156,7 @@ export async function getCachedRegisteredSyncGenerators(): Promise<string[]> {

async function getFromCacheOrRunGenerators(
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
): Promise<SyncGeneratorRunResult[]> {
let projects: Record<string, ProjectConfiguration> | null;
let errored = false;
const getProjectsConfigurations = async () => {
Expand Down Expand Up @@ -223,7 +225,7 @@ async function getFromCacheOrRunGenerators(
async function runConflictingGenerators(
tree: Tree,
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
): Promise<SyncGeneratorRunResult[]> {
const { projectGraph } = await getCachedSerializedProjectGraphPromise();
const projects = projectGraph
? readProjectsConfigurationFromProjectGraph(projectGraph).projects
Expand All @@ -246,7 +248,7 @@ async function runConflictingGenerators(
}

// we need to run conflicting generators sequentially because they use the same tree
const results: SyncGeneratorChangesResult[] = [];
const results: SyncGeneratorRunResult[] = [];
for (const generator of generators) {
log(generator, 'running it now');
results.push(await runGenerator(generator, projects, tree));
Expand All @@ -257,16 +259,17 @@ async function runConflictingGenerators(

async function processConflictingGenerators(
conflicts: string[][],
initialResults: SyncGeneratorChangesResult[]
): Promise<SyncGeneratorChangesResult[]> {
initialResults: SyncGeneratorRunResult[]
): Promise<SyncGeneratorRunResult[]> {
const conflictRunResults = (
await Promise.all(
conflicts.map((generators) => {
const [firstGenerator, ...generatorsToRun] = generators;
// it must exists because the conflicts were identified from the initial results
// and it's guaranteed to be a success result
const firstGeneratorResult = initialResults.find(
(r) => r.generatorName === firstGenerator
)!;
)! as SyncGeneratorRunSuccessResult;

const tree = new FsTree(
workspaceRoot,
Expand Down Expand Up @@ -319,10 +322,14 @@ async function processConflictingGenerators(
* @internal
*/
export function _getConflictingGeneratorGroups(
results: SyncGeneratorChangesResult[]
results: SyncGeneratorRunResult[]
): string[][] {
const changedFileToGeneratorMap = new Map<string, Set<string>>();
for (const result of results) {
if ('error' in result) {
continue;
}

for (const change of result.changes) {
if (!changedFileToGeneratorMap.has(change.path)) {
changedFileToGeneratorMap.set(change.path, new Set());
Expand Down Expand Up @@ -419,7 +426,7 @@ function runGenerator(
generator: string,
projects: Record<string, ProjectConfiguration>,
tree?: Tree
): Promise<SyncGeneratorChangesResult> {
): Promise<SyncGeneratorRunResult> {
log('running scheduled generator', generator);
// remove it from the scheduled set
scheduledGenerators.delete(generator);
Expand All @@ -430,7 +437,11 @@ function runGenerator(
);

return runSyncGenerator(tree, generator, projects).then((result) => {
log(generator, 'changes:', result.changes.map((c) => c.path).join(', '));
if ('error' in result) {
log(generator, 'error:', result.error.message);
} else {
log(generator, 'changes:', result.changes.map((c) => c.path).join(', '));
}
return result;
});
}
Expand Down
Loading