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
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export class BicepWorkspace implements IWorkspace {
return this.config.options.versionTagPrefix;
}
}

get workspaceDependencies(): string[] {
return this.manifest.dependencies ?? [];
}

constructor(project: BicepProjectImpl, relativeCwd: string, manifestContent: BicepManifestContent) {
this.manifestContent = manifestContent;

Expand Down
165 changes: 98 additions & 67 deletions workspaces/packages/gitversion/src/commands/pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { colorize } from 'colorize-node';
import { mkdir, stat } from 'fs/promises';
import { cpus } from 'os';
import { join, relative } from 'path';
import Queue from 'queue-promise';
import { cascade, isAtLeast, isNumber } from 'typanion';

import { Application, IApplication } from '../core/application';
import { Bump, BumpManifest } from '../core/bump-manifest';
import { formatFileSize, formatPackageName } from '../core/format-utils';
import { PackArtifact } from '../core/pack-artifact';
import { PackArtifact, PackedPackage } from '../core/pack-artifact';
import { PackFileResult, PackResult } from '../core/plugin-manager';
import { topoSort } from '../core/topo-sort';
import { IWorkspace } from '../core/workspace-utils';

import { GitVersionCommand } from './context';
Expand Down Expand Up @@ -77,47 +78,45 @@ export class PackCommand extends GitVersionCommand {
const packFolder = join(configuration.stagingFolder, 'pack');
await mkdir(packFolder, { recursive: true });

const queue = new Queue({
concurrent: this.maxConcurrency ?? cpus().length,
start: false,
});

bumpManifest.bumps.forEach(bump => {
if (bump.packageRelativeCwd === '.' && project.childWorkspaces.length > 0) {
// Root project workspace is already added directly via packManifest.add(projectBump) above.
// Only update its version/changelog on disk; do not pack it again.
const rootWorkspace = project.workspaces.find(w => w.relativeCwd === '.');
if (rootWorkspace) {
queue.enqueue(async () => {
try {
await rootWorkspace.updateVersion(bump.version);
await rootWorkspace.updateChangelog(bump.changeLog);
} catch (error) {
hasErrors = true;
throw error;
}
});
}
return;
}

queue.enqueue(async () => {
// Phase 1: write all versions to disk in parallel so every package.json is
// up-to-date before any pack subprocess reads them.
await Promise.all(bumpManifest.bumps.map(async bump => {
const workspace = project.workspaces.find(w => w.relativeCwd === bump.packageRelativeCwd);
if (workspace) {
try {
const workspace = project.workspaces.find(w => w.relativeCwd === bump.packageRelativeCwd);
if (workspace) {
await workspace.updateVersion(bump.version);
await workspace.updateChangelog(bump.changeLog);
await this.execPackCommand(application, workspace, bump, packManifest, false);
}
await workspace.updateVersion(bump.version);
await workspace.updateChangelog(bump.changeLog);
} catch (error) {
hasErrors = true;
throw error;
}
});
});

while (queue.shouldRun) {
await queue.dequeue();
}
}));

// Phase 2: pack in topological dependency order, parallel within each level.
// The root workspace (when it has children) is registered in packManifest above but not packed.
const workspacesToPack = bumpManifest.bumps
.filter(b => !(b.packageRelativeCwd === '.' && project.childWorkspaces.length > 0))
.map(b => project.workspaces.find(w => w.relativeCwd === b.packageRelativeCwd))
.filter((w): w is IWorkspace => !!w);

const concurrency = this.maxConcurrency ?? cpus().length;
for (const level of topoSort(workspacesToPack)) {
for (let i = 0; i < level.length; i += concurrency) {
const chunk = level.slice(i, i + concurrency);
const chunkResults = await Promise.allSettled(chunk.map(async workspace => {
const bump = bumpManifest.bumps.find(b => b.packageRelativeCwd === workspace.relativeCwd)!;
await this.execPackCommand(application, workspace, bump, packManifest, false);
}));
for (const result of chunkResults) {
if (result.status === 'rejected') {
hasErrors = true;
}
}
}
if (hasErrors) {
break;
}
}
}

Expand All @@ -132,14 +131,11 @@ export class PackCommand extends GitVersionCommand {
const packFolder = join(configuration.stagingFolder, 'pack');
await mkdir(packFolder, { recursive: true });

const queue = new Queue({
concurrent: this.maxConcurrency ?? cpus().length,
start: false,
});

republishWorkspaces.forEach(workspace => {
queue.enqueue(async () => {
try {
const concurrency = this.maxConcurrency ?? cpus().length;
for (const level of topoSort(republishWorkspaces)) {
for (let i = 0; i < level.length; i += concurrency) {
const chunk = level.slice(i, i + concurrency);
const chunkResults = await Promise.allSettled(chunk.map(async workspace => {
const syntheticBump: Bump = {
packageRelativeCwd: workspace.relativeCwd,
packageName: workspace.packageName,
Expand All @@ -151,15 +147,16 @@ export class PackCommand extends GitVersionCommand {
changeLog: { version: workspace.version, headerLine: '', body: '' },
};
await this.execPackCommand(application, workspace, syntheticBump, packManifest, true);
} catch (error) {
hasErrors = true;
throw error;
}));
for (const result of chunkResults) {
if (result.status === 'rejected') {
hasErrors = true;
}
}
});
});

while (queue.shouldRun) {
await queue.dequeue();
}
if (hasErrors) {
break;
}
}
}
}
Expand Down Expand Up @@ -193,21 +190,46 @@ export class PackCommand extends GitVersionCommand {
recursive: true,
});
const packFile = await packManager.pack(workspace, folder);
if (packFile) {
let fileEntry: Record<string, string | string[] | Record<string, string>> = {};
let metadataEntry: { ident: string, data: unknown } | null = null;
if (packFile !== null && typeof packFile === 'object' && 'files' in packFile && Array.isArray((packFile as PackResult).files)) {
// PackResult branch
const packResult = packFile as PackResult;
const fileNames: string[] = [];
const perFileMetadata: Record<string, unknown> = {};
for (const f of packResult.files as PackFileResult[]) {
fileNames.push(f.name);
if (f.metadata !== undefined) {
perFileMetadata[f.name] = f.metadata;
}
const fullName = join(folder, f.name);
const stats = await stat(fullName);
logger.reportInfo(`Generated package: ./${relative(application.cwd, fullName)} (${formatFileSize(stats.size)})`);
}
fileEntry = {
[packManager.ident]: fileNames,
};
if (Object.keys(perFileMetadata).length > 0) {
metadataEntry = {
ident: packManager.ident,
data: perFileMetadata,
};
}
} else if (packFile) {
if (Array.isArray(packFile)) {
for (const file of packFile) {
const fullName = join(folder, file);
const stats = await stat(fullName);
logger.reportInfo(`Generated package: ./${relative(application.cwd, fullName)} (${formatFileSize(stats.size)})`);
}
return {
fileEntry = {
[packManager.ident]: packFile,
};
} if (typeof packFile === 'string') {
} else if (typeof packFile === 'string') {
const fullName = join(folder, packFile);
const stats = await stat(fullName);
logger.reportInfo(`Generated package: ./${relative(application.cwd, fullName)} (${formatFileSize(stats.size)})`);
return {
fileEntry = {
[packManager.ident]: packFile,
};
} else if (typeof packFile === 'object' && packFile !== null) {
Expand All @@ -219,29 +241,38 @@ export class PackCommand extends GitVersionCommand {
logger.reportInfo(`Generated package: ./${relative(application.cwd, fullName)} (${formatFileSize(stats.size)})`);
files[key] = value;
}
return {
fileEntry = {
[packManager.ident]: files,
};
} else {
return {};
}
} else {
return {};
}

return { fileEntry, metadataEntry };
});

const files = (await Promise.all(packCommands)).reduce((p: Record<string, string | string[] | Record<string, string>>, c) => {
const results = await Promise.all(packCommands);

const files = results.reduce((p: Record<string, string | string[] | Record<string, string>>, { fileEntry }) => {
return {
...p,
...c,
...fileEntry,
};
}, {});

packManifest.add({
const packedPackage: PackedPackage = {
packFiles: files,
...bump,
republish: republish || undefined,
});
};

for (const { metadataEntry } of results) {
if (metadataEntry) {
packedPackage.pluginData ??= {};
packedPackage.pluginData[metadataEntry.ident] = metadataEntry.data;
}
}

packManifest.add(packedPackage);
} catch (error) {
logger.reportError(`Error during pack: ${colorize.redBright(`${error}`)}`);
throw error;
Expand Down
1 change: 1 addition & 0 deletions workspaces/packages/gitversion/src/core/pack-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface PackedPackage {
changeLog: ChangelogEntry;
commits: ConventionalCommit[];
republish?: boolean;
pluginData?: Record<string, unknown>;
}

export interface PackManifestGitStatus extends BumpManifestGitStatus {
Expand Down
11 changes: 10 additions & 1 deletion workspaces/packages/gitversion/src/core/plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,18 @@ export interface IGitPlatform {
stripMergeMessage(commit: GitCommit): GitCommit;
}

export interface PackFileResult {
name: string;
metadata?: unknown;
}

export interface PackResult {
files: PackFileResult[];
}

export interface IPackManager {
ident: string;
pack(workspace: IWorkspace, outputFolder: string): Promise<string | string[] | Record<string, string> | null>;
pack(workspace: IWorkspace, outputFolder: string): Promise<string | string[] | Record<string, string> | PackResult | null>;
publish(packedPackage: PackedPackage, fileName: string, releaseTag: string, dryRun: boolean, module?: string): Promise<void>;
}

Expand Down
67 changes: 67 additions & 0 deletions workspaces/packages/gitversion/src/core/topo-sort._test_.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { topoSort } from './topo-sort';
import { IWorkspace } from './workspace-utils';

function makeWorkspace(packageName: string, deps: string[] = []): IWorkspace {
return {
packageName,
workspaceDependencies: deps,
} as unknown as IWorkspace;
}

describe('topoSort', () => {
it('returns empty array for empty input', () => {
expect(topoSort([])).toEqual([]);
});

it('returns single workspace in one level', () => {
const a = makeWorkspace('a');
expect(topoSort([a])).toEqual([[a]]);
});

it('linear chain: dependency before dependent', () => {
const a = makeWorkspace('a');
const b = makeWorkspace('b', ['a']);
const c = makeWorkspace('c', ['b']);
const levels = topoSort([c, b, a]);
expect(levels).toHaveLength(3);
expect(levels[0]).toEqual([a]);
expect(levels[1]).toEqual([b]);
expect(levels[2]).toEqual([c]);
});

it('diamond: shared dep in level 0, both consumers in level 1, final in level 2', () => {
const shared = makeWorkspace('shared');
const left = makeWorkspace('left', ['shared']);
const right = makeWorkspace('right', ['shared']);
const top = makeWorkspace('top', ['left', 'right']);
const levels = topoSort([top, right, left, shared]);
expect(levels[0]).toEqual([shared]);
expect(levels[1]).toHaveLength(2);
expect(levels[1]).toEqual(expect.arrayContaining([left, right]));
expect(levels[2]).toEqual([top]);
});

it('disconnected graph: independent workspaces in the same level', () => {
const a = makeWorkspace('a');
const b = makeWorkspace('b');
const levels = topoSort([a, b]);
expect(levels).toHaveLength(1);
expect(levels[0]).toHaveLength(2);
expect(levels[0]).toEqual(expect.arrayContaining([a, b]));
});

it('external deps (not in workspace set) are ignored', () => {
const a = makeWorkspace('a', ['lodash', 'external-pkg']);
const levels = topoSort([a]);
expect(levels).toEqual([[a]]);
});

it('cycle fallback: all workspaces are still returned', () => {
const a = makeWorkspace('a', ['b']);
const b = makeWorkspace('b', ['a']);
const levels = topoSort([a, b]);
const allWorkspaces = levels.flat();
expect(allWorkspaces).toHaveLength(2);
expect(allWorkspaces).toEqual(expect.arrayContaining([a, b]));
});
});
40 changes: 40 additions & 0 deletions workspaces/packages/gitversion/src/core/topo-sort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { IWorkspace } from './workspace-utils';

export function topoSort(workspaces: IWorkspace[]): IWorkspace[][] {
const nameToWs = new Map(workspaces.map(w => [w.packageName, w]));
const inDegree = new Map(workspaces.map(w => [w.packageName, 0]));
const dependents = new Map<string, string[]>(workspaces.map(w => [w.packageName, []]));

for (const w of workspaces) {
for (const dep of w.workspaceDependencies) {
if (nameToWs.has(dep)) {
dependents.get(dep)!.push(w.packageName);
inDegree.set(w.packageName, inDegree.get(w.packageName)! + 1);
}
}
}

const levels: IWorkspace[][] = [];
let current = workspaces.filter(w => inDegree.get(w.packageName) === 0);
const processed = new Set<string>();

while (current.length > 0) {
levels.push(current);
const next: IWorkspace[] = [];
for (const w of current) {
processed.add(w.packageName);
for (const dependent of dependents.get(w.packageName) ?? []) {
const d = inDegree.get(dependent)! - 1;
inDegree.set(dependent, d);
if (d === 0) next.push(nameToWs.get(dependent)!);
}
}
current = next;
}

// Cycle fallback: append any unresolved workspaces as a final level
const unprocessed = workspaces.filter(w => !processed.has(w.packageName));
if (unprocessed.length > 0) levels.push(unprocessed);

return levels;
}
Loading
Loading