nx
Version:
660 lines (658 loc) • 32.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReleaseGraph = exports.validReleaseVersionPrefixes = void 0;
exports.createReleaseGraph = createReleaseGraph;
const config_1 = require("../config/config");
const project_logger_1 = require("../version/project-logger");
const resolve_current_version_1 = require("../version/resolve-current-version");
const topological_sort_1 = require("../version/topological-sort");
const version_actions_1 = require("../version/version-actions");
const git_1 = require("./git");
const shared_1 = require("./shared");
exports.validReleaseVersionPrefixes = ['auto', '', '~', '^', '='];
/**
* The complete release graph containing all relationships, caches, and computed data
* necessary for efficient release operations across versioning, changelog, and publishing.
*
* This class encapsulates the complex dependency graph between projects and release groups,
* providing convenient methods for querying relationships and accessing cached data.
*/
class ReleaseGraph {
constructor(releaseGroups, filters) {
this.releaseGroups = releaseGroups;
this.filters = filters;
this.projectToReleaseGroup = new Map();
this.projectToDependents = new Map();
this.projectToDependencies = new Map();
this.projectToUpdateDependentsSetting = new Map();
this.groupGraph = new Map();
this.sortedReleaseGroups = [];
this.sortedProjects = new Map();
this.allProjectsConfiguredForNxRelease = new Set();
this.allProjectsToProcess = new Set();
this.finalConfigsByProject = new Map();
this.projectsToVersionActions = new Map();
this.uniqueAfterAllProjectsVersioned = new Map();
this.projectLoggers = new Map();
this.cachedCurrentVersions = new Map();
this.cachedLatestMatchingGitTag = new Map();
this.currentVersionsPerFixedReleaseGroup = new Map();
this.originalDependentProjectsPerProject = new Map();
this.releaseGroupToFilteredProjects = new Map();
this.originalFilteredProjects = new Set();
/**
* User-friendly log describing what the filter matched.
* Null if no filters were applied.
*/
this.filterLog = null;
}
/**
* Initialize the graph by building all relationships and caches
* @internal - Called by createReleaseGraph(), not meant for external use
*/
async init(options) {
// Step 1: Setup project to release group mapping
this.setupProjectReleaseGroupMapping();
// Step 2: Apply initial filtering to determine base set of projects and groups to process
this.applyInitialFiltering();
// Step 3: Setup projects to process and resolve version actions
await this.setupProjectsToProcess(options);
// Step 4: Precompute dependency relationships
await this.precomputeDependencyRelationships(options.tree, options.projectGraph);
// Step 5: Apply dependency-aware filtering based on updateDependents
this.applyDependencyAwareFiltering();
// Step 5: Build the group graph structure
this.buildGroupGraphStructure();
// Step 6: Resolve current versions for all projects to process (unless explicitly skipped)
if (!options.skipVersionResolution) {
await this.resolveCurrentVersionsForProjects(options.tree, options.projectGraph, options.preid ?? '');
}
// Step 7: Build dependency relationships between groups
this.buildGroupDependencyGraph();
// Step 8: Topologically sort groups and projects
this.sortedReleaseGroups = this.topologicallySortReleaseGroups();
for (const group of this.releaseGroups) {
this.sortedProjects.set(group.name, this.topologicallySortProjects(group));
}
// Step 9: Populate dependent projects data
await this.populateDependentProjectsData(options.tree, options.projectGraph);
}
/**
* Setup mapping from project to release group and cache updateDependents settings
*/
setupProjectReleaseGroupMapping() {
for (const group of this.releaseGroups) {
for (const project of group.projects) {
this.projectToReleaseGroup.set(project, group);
const updateDependents = group.version?.updateDependents ||
'always';
this.projectToUpdateDependentsSetting.set(project, updateDependents);
}
}
}
/**
* Apply initial filtering to construct releaseGroupToFilteredProjects based on filters.
* This determines the base set of projects and groups before considering dependencies.
*/
applyInitialFiltering() {
const matchedReleaseGroups = [];
for (const releaseGroup of this.releaseGroups) {
// If group filter is applied and this group doesn't match, skip it entirely
if (this.filters.groups?.length &&
!this.filters.groups.includes(releaseGroup.name)) {
continue;
}
// If filtering by groups (not projects), include ALL projects in the matched group
if (this.filters.groups?.length && !this.filters.projects?.length) {
this.releaseGroupToFilteredProjects.set(releaseGroup, new Set(releaseGroup.projects));
matchedReleaseGroups.push(releaseGroup);
continue;
}
// If filtering by projects, filter down to matching projects
const filteredProjects = new Set();
for (const project of releaseGroup.projects) {
if (this.filters.projects?.length &&
!this.filters.projects.includes(project)) {
continue;
}
filteredProjects.add(project);
}
// If no filters applied or group has matching projects, include it
if (filteredProjects.size > 0 || !this.hasAnyFilters()) {
const projectsToInclude = filteredProjects.size > 0
? filteredProjects
: new Set(releaseGroup.projects);
this.releaseGroupToFilteredProjects.set(releaseGroup, projectsToInclude);
matchedReleaseGroups.push(releaseGroup);
}
}
// Update this.releaseGroups to only include matched groups
if (this.hasAnyFilters()) {
this.releaseGroups = matchedReleaseGroups;
}
}
/**
* Check if any filters are applied
*/
hasAnyFilters() {
return !!(this.filters.projects?.length || this.filters.groups?.length);
}
/**
* Setup projects to process and resolve version actions
*/
async setupProjectsToProcess(options) {
const { tree, projectGraph, nxReleaseConfig, filters, firstRelease, versionActionsOptionsOverrides, } = options;
let projectsToProcess = new Set();
const resolveVersionActionsForProjectCallbacks = [];
// Precompute all projects in nx release config
for (const [groupName, group] of Object.entries(nxReleaseConfig.groups)) {
for (const project of group.projects) {
this.allProjectsConfiguredForNxRelease.add(project);
this.projectLoggers.set(project, new project_logger_1.ProjectLogger(project));
if (filters.groups?.includes(groupName)) {
projectsToProcess.add(project);
}
else if (filters.projects?.includes(project)) {
projectsToProcess.add(project);
}
const projectGraphNode = projectGraph.nodes[project];
const releaseGroup = this.projectToReleaseGroup.get(project);
const finalConfigForProject = ReleaseGraph.resolveFinalConfigForProject(releaseGroup, projectGraphNode, firstRelease, versionActionsOptionsOverrides);
this.finalConfigsByProject.set(project, finalConfigForProject);
resolveVersionActionsForProjectCallbacks.push(async () => {
const { versionActionsPath, versionActions, afterAllProjectsVersioned, } = await (0, version_actions_1.resolveVersionActionsForProject)(tree, releaseGroup, projectGraphNode, finalConfigForProject);
if (!this.uniqueAfterAllProjectsVersioned.has(versionActionsPath)) {
this.uniqueAfterAllProjectsVersioned.set(versionActionsPath, afterAllProjectsVersioned);
}
let versionActionsToUse = versionActions;
const shouldSkip = (0, shared_1.shouldSkipVersionActions)(finalConfigForProject.dockerOptions, project);
if (shouldSkip) {
versionActionsToUse = new version_actions_1.NOOP_VERSION_ACTIONS(releaseGroup, projectGraphNode, finalConfigForProject);
}
this.projectsToVersionActions.set(project, versionActionsToUse);
});
}
}
if (!filters.groups?.length && !filters.projects?.length) {
projectsToProcess = this.allProjectsConfiguredForNxRelease;
}
if (projectsToProcess.size === 0) {
throw new Error('No projects are set to be processed, please report this as a bug on https://github.com/nrwl/nx/issues');
}
this.allProjectsToProcess = new Set(projectsToProcess);
for (const cb of resolveVersionActionsForProjectCallbacks) {
await cb();
}
}
/**
* Precompute dependency relationships between all projects
*/
async precomputeDependencyRelationships(tree, projectGraph) {
for (const projectName of this.allProjectsConfiguredForNxRelease) {
const versionActions = this.projectsToVersionActions.get(projectName);
if (!this.projectToDependencies.has(projectName)) {
this.projectToDependencies.set(projectName, new Set());
}
const deps = await versionActions.readDependencies(tree, projectGraph);
for (const dep of deps) {
if (!this.allProjectsConfiguredForNxRelease.has(dep.target)) {
continue;
}
this.projectToDependencies.get(projectName).add(dep.target);
if (!this.projectToDependents.has(dep.target)) {
this.projectToDependents.set(dep.target, new Set());
}
this.projectToDependents.get(dep.target).add(projectName);
}
}
}
/**
* Apply dependency-aware filtering that considers updateDependents configuration.
* This includes transitive dependents based on updateDependents setting ('always' by default, or 'auto').
*/
applyDependencyAwareFiltering() {
// Track the original filtered projects before adding dependents
this.originalFilteredProjects = new Set(this.allProjectsToProcess);
if (!this.hasAnyFilters()) {
// No filtering applied, nothing to do
return;
}
// Validate filtering against fixed release groups
this.validateFilterAgainstFixedGroups();
// Find all dependents that need to be included based on updateDependents setting
this.findDependentsToProcess();
// Generate user-friendly filter log
this.generateFilterLog();
}
/**
* Validate that the filter doesn't try to isolate projects in fixed release groups
*/
validateFilterAgainstFixedGroups() {
if (!this.filters.projects?.length) {
// Group filtering doesn't have this issue
return;
}
for (const releaseGroup of this.releaseGroups) {
if (releaseGroup.projectsRelationship !== 'fixed') {
continue;
}
const filteredProjectsInGroup = releaseGroup.projects.filter((p) => this.releaseGroupToFilteredProjects.get(releaseGroup)?.has(p));
if (filteredProjectsInGroup.length > 0 &&
filteredProjectsInGroup.length < releaseGroup.projects.length) {
throw new Error(`Cannot filter to a subset of projects within fixed release group "${releaseGroup.name}". ` +
`Filtered projects: [${filteredProjectsInGroup.join(', ')}], ` +
`All projects in group: [${releaseGroup.projects.join(', ')}]. ` +
`Either filter to all projects in the group, use --groups to filter by group, or change the group to "independent".`);
}
}
}
/**
* Find dependents that should be included in processing based on updateDependents configuration
*/
findDependentsToProcess() {
const projectsToProcess = Array.from(this.allProjectsToProcess);
const allTrackedDependents = new Set();
const dependentsToProcess = new Set();
const additionalGroups = new Map();
// BFS traversal to find all transitive dependents
let currentLevel = [...projectsToProcess];
while (currentLevel.length > 0) {
const nextLevel = [];
const dependents = this.getAllNonImplicitDependents(currentLevel);
for (const dep of dependents) {
if (allTrackedDependents.has(dep) ||
this.allProjectsToProcess.has(dep)) {
continue;
}
allTrackedDependents.add(dep);
// Check if this dependent should be included based on updateDependents settings
const depUpdateDependentsSetting = this.projectToUpdateDependentsSetting.get(dep);
// Only include if dependent has 'always' or 'auto' (not 'never')
if (depUpdateDependentsSetting !== 'never') {
// Find which project(s) in currentLevel this dependent depends on
const shouldIncludeDependent = currentLevel.some((proj) => {
const projUpdateSetting = this.projectToUpdateDependentsSetting.get(proj);
const projDependents = this.getProjectDependents(proj);
if (!projDependents.has(dep)) {
return false;
}
// Always include if updateDependents is 'always'
if (projUpdateSetting === 'always') {
return true;
}
// For 'auto', include if in the same release group to match historical behavior
if (projUpdateSetting === 'auto') {
const projGroup = this.getReleaseGroupForProject(proj);
const depGroup = this.getReleaseGroupForProject(dep);
return projGroup && depGroup && projGroup.name === depGroup.name;
}
return false;
});
if (shouldIncludeDependent) {
dependentsToProcess.add(dep);
// Track the release group of this dependent
const depGroup = this.getReleaseGroupForProject(dep);
if (depGroup) {
// Check if this group is already in our list by name
const groupAlreadyExists = this.releaseGroups.some((g) => g.name === depGroup.name);
if (!groupAlreadyExists) {
additionalGroups.set(depGroup.name, depGroup);
}
}
}
}
nextLevel.push(dep);
}
currentLevel = nextLevel;
}
dependentsToProcess.forEach((dep) => this.allProjectsToProcess.add(dep));
// Add any additional groups and their filtered projects
additionalGroups.forEach((group) => {
// When adding groups due to dependents, clear version plans to avoid duplication
// Version plans should only be processed for groups that were explicitly included
const groupForDependents = {
...group,
versionPlans: false,
resolvedVersionPlans: false,
};
this.releaseGroups.push(groupForDependents);
// Add the projects from this group that are actually being processed
const projectsInGroup = new Set(group.projects.filter((p) => dependentsToProcess.has(p)));
this.releaseGroupToFilteredProjects.set(groupForDependents, projectsInGroup);
});
}
/**
* Generate user-friendly log describing what the filter matched
*/
generateFilterLog() {
if (this.filters.projects?.length) {
// Projects filter - only show the originally filtered projects to match old behavior
const matchedProjects = Array.from(this.originalFilteredProjects);
this.filterLog = {
title: `Your filter "${this.filters.projects.join(',')}" matched the following projects:`,
bodyLines: matchedProjects.map((p) => {
const releaseGroupForProject = this.projectToReleaseGroup.get(p);
if (!releaseGroupForProject ||
releaseGroupForProject.name === config_1.IMPLICIT_DEFAULT_RELEASE_GROUP) {
return `- ${p}`;
}
return `- ${p} (release group "${releaseGroupForProject.name}")`;
}),
};
}
// TODO: add groups filter log
}
/**
* Build the group graph structure
*/
buildGroupGraphStructure() {
for (const group of this.releaseGroups) {
// Don't overwrite if already exists (may have been added during filtering)
if (!this.groupGraph.has(group.name)) {
this.groupGraph.set(group.name, {
group,
dependencies: new Set(),
dependents: new Set(),
});
}
}
}
/**
* Resolve current versions for all projects that will be processed
*/
async resolveCurrentVersionsForProjects(tree, projectGraph, preid) {
for (const [, releaseGroupNode] of this.groupGraph) {
for (const projectName of releaseGroupNode.group.projects) {
const projectGraphNode = projectGraph.nodes[projectName];
if (!this.allProjectsToProcess.has(projectName)) {
continue;
}
const versionActions = this.projectsToVersionActions.get(projectName);
const finalConfigForProject = this.finalConfigsByProject.get(projectName);
let latestMatchingGitTag;
const releaseTagPattern = releaseGroupNode.group.releaseTag.pattern;
if (finalConfigForProject.currentVersionResolver === 'git-tag') {
latestMatchingGitTag = await (0, git_1.getLatestGitTagForPattern)(releaseTagPattern, {
projectName: (0, git_1.sanitizeProjectNameForGitTag)(projectGraphNode.name),
releaseGroupName: releaseGroupNode.group.name,
}, {
checkAllBranchesWhen: releaseGroupNode.group.releaseTag.checkAllBranchesWhen,
preid: preid,
requireSemver: releaseGroupNode.group.releaseTag.requireSemver,
strictPreid: releaseGroupNode.group.releaseTag.strictPreid,
});
this.cachedLatestMatchingGitTag.set(projectName, latestMatchingGitTag);
}
const currentVersion = await (0, resolve_current_version_1.resolveCurrentVersion)(tree, projectGraphNode, releaseGroupNode.group, versionActions, this.projectLoggers.get(projectName), this.currentVersionsPerFixedReleaseGroup, finalConfigForProject, releaseTagPattern, latestMatchingGitTag);
this.cachedCurrentVersions.set(projectName, currentVersion);
}
}
}
/**
* Build dependency relationships between release groups
*/
buildGroupDependencyGraph() {
for (const [releaseGroupName, releaseGroupNode] of this.groupGraph) {
for (const projectName of releaseGroupNode.group.projects) {
const projectDeps = this.getProjectDependencies(projectName);
for (const dep of projectDeps) {
const dependencyGroup = this.getReleaseGroupNameForProject(dep);
if (dependencyGroup && dependencyGroup !== releaseGroupName) {
releaseGroupNode.dependencies.add(dependencyGroup);
// Only add to dependents if the dependency group exists in the graph
// (it may have been filtered out due to user filters)
const dependencyGroupNode = this.groupGraph.get(dependencyGroup);
if (dependencyGroupNode) {
dependencyGroupNode.dependents.add(releaseGroupName);
}
}
}
}
}
}
/**
* Topologically sort release groups
*/
topologicallySortReleaseGroups() {
const groupNames = Array.from(this.groupGraph.keys());
const getGroupDependencies = (groupName) => {
const groupNode = this.groupGraph.get(groupName);
if (!groupNode) {
return [];
}
return Array.from(groupNode.dependencies);
};
return (0, topological_sort_1.topologicalSort)(groupNames, getGroupDependencies);
}
/**
* Topologically sort projects within a release group
*/
topologicallySortProjects(releaseGroup) {
const projects = releaseGroup.projects.filter((p) => this.allProjectsToProcess.has(p));
const getProjectDependenciesInSameGroup = (project) => {
const deps = this.getProjectDependencies(project);
return Array.from(deps).filter((dep) => this.getReleaseGroupNameForProject(dep) === releaseGroup.name &&
this.allProjectsToProcess.has(dep));
};
return (0, topological_sort_1.topologicalSort)(projects, getProjectDependenciesInSameGroup);
}
async populateDependentProjectsData(tree, projectGraph) {
// Populate detailed dependent projects data for all projects being processed
for (const projectName of this.allProjectsToProcess) {
const dependentProjectNames = Array.from(this.getProjectDependents(projectName)).filter((dep) => this.allProjectsConfiguredForNxRelease.has(dep));
const dependentProjectsData = [];
for (const dependentProjectName of dependentProjectNames) {
const versionActions = this.projectsToVersionActions.get(dependentProjectName);
const { currentVersion, dependencyCollection } = await versionActions.readCurrentVersionOfDependency(tree, projectGraph, projectName);
dependentProjectsData.push({
source: dependentProjectName,
target: projectName,
type: 'static',
dependencyCollection,
rawVersionSpec: currentVersion,
});
}
this.originalDependentProjectsPerProject.set(projectName, dependentProjectsData);
}
}
/**
* Get all non-implicit dependents for a set of projects
*/
getAllNonImplicitDependents(projects) {
return projects
.flatMap((project) => Array.from(this.getProjectDependents(project)))
.filter((dep) => !this.allProjectsToProcess.has(dep));
}
/**
* Resolve final configuration for a project
*
* NOTE: We are providing ultimate fallback values via ?? here mainly just to keep TypeScript happy.
* All default values should have been applied by this point by config.ts but the types can't know
* that for sure at this point.
*/
static resolveFinalConfigForProject(releaseGroup, projectGraphNode, firstRelease, versionActionsOptionsOverrides) {
const releaseGroupVersionConfig = releaseGroup.version;
const projectVersionConfig = projectGraphNode.data.release?.version;
const projectDockerConfig = projectGraphNode.data.release?.docker;
/**
* specifierSource
*
* If the user has provided a specifier, it always takes precedence,
* so the effective specifier source is 'prompt', regardless of what
* the project or release group config says.
*/
const specifierSource = projectVersionConfig?.specifierSource ??
releaseGroupVersionConfig?.specifierSource ??
'prompt';
/**
* versionPrefix, defaults to auto
*/
const versionPrefix = projectVersionConfig?.versionPrefix ??
releaseGroupVersionConfig?.versionPrefix ??
'auto';
if (versionPrefix && !exports.validReleaseVersionPrefixes.includes(versionPrefix)) {
throw new Error(`Invalid value for versionPrefix: "${versionPrefix}"
Valid values are: ${exports.validReleaseVersionPrefixes
.map((s) => `"${s}"`)
.join(', ')}`);
}
/**
* Merge docker options configured in project with release group config,
* project level configuration should take precedence
*/
const dockerOptions = Object.assign({}, releaseGroup.docker || {}, projectDockerConfig || {});
/**
* currentVersionResolver, defaults to disk
*/
let currentVersionResolver = projectVersionConfig?.currentVersionResolver ??
releaseGroupVersionConfig?.currentVersionResolver ??
'disk';
// Check if this project should skip version actions based on docker configuration
const shouldSkip = (0, shared_1.shouldSkipVersionActions)(dockerOptions, projectGraphNode.name);
if (shouldSkip) {
// If the project skips version actions, it doesn't need to resolve a current version
currentVersionResolver = 'none';
}
else if (specifierSource === 'conventional-commits' &&
currentVersionResolver !== 'git-tag') {
throw new Error(`Invalid currentVersionResolver "${currentVersionResolver}" provided for project "${projectGraphNode.name}". Must be "git-tag" when "specifierSource" is "conventional-commits"`);
}
/**
* currentVersionResolverMetadata, defaults to {}
*/
const currentVersionResolverMetadata = projectVersionConfig?.currentVersionResolverMetadata ??
releaseGroupVersionConfig?.currentVersionResolverMetadata ??
{};
/**
* preserveLocalDependencyProtocols
*
* This was false by default in legacy versioning, but is true by default now.
*/
const preserveLocalDependencyProtocols = projectVersionConfig?.preserveLocalDependencyProtocols ??
releaseGroupVersionConfig?.preserveLocalDependencyProtocols ??
true;
/**
* preserveMatchingDependencyRanges
*
* This was false by default until v22, but is true by default now.
*/
const preserveMatchingDependencyRanges = projectVersionConfig?.preserveMatchingDependencyRanges ??
releaseGroupVersionConfig?.preserveMatchingDependencyRanges ??
true;
/**
* fallbackCurrentVersionResolver, defaults to disk when performing a first release, otherwise undefined
*/
const fallbackCurrentVersionResolver = projectVersionConfig?.fallbackCurrentVersionResolver ??
releaseGroupVersionConfig?.fallbackCurrentVersionResolver ??
(firstRelease ? 'disk' : undefined);
/**
* versionActionsOptions, defaults to {}
*/
let versionActionsOptions = projectVersionConfig?.versionActionsOptions ??
releaseGroupVersionConfig?.versionActionsOptions ??
{};
// Apply any optional overrides that may be passed in from the programmatic API
versionActionsOptions = {
...versionActionsOptions,
...(versionActionsOptionsOverrides ?? {}),
};
const manifestRootsToUpdate = (projectVersionConfig?.manifestRootsToUpdate ??
releaseGroupVersionConfig?.manifestRootsToUpdate ??
[]).map((manifestRoot) => {
if (typeof manifestRoot === 'string') {
return {
path: manifestRoot,
// Apply the project level preserveLocalDependencyProtocols setting that was already resolved
preserveLocalDependencyProtocols,
};
}
return manifestRoot;
});
return {
specifierSource,
currentVersionResolver,
currentVersionResolverMetadata,
fallbackCurrentVersionResolver,
versionPrefix,
preserveLocalDependencyProtocols,
preserveMatchingDependencyRanges,
versionActionsOptions,
manifestRootsToUpdate,
dockerOptions,
};
}
/**
* Get the release group for a given project
*/
getReleaseGroupForProject(projectName) {
return this.projectToReleaseGroup.get(projectName);
}
/**
* Get the release group name for a given project
*/
getReleaseGroupNameForProject(projectName) {
const group = this.projectToReleaseGroup.get(projectName);
return group ? group.name : null;
}
/**
* Get the dependencies of a project
*/
getProjectDependencies(projectName) {
return this.projectToDependencies.get(projectName) || new Set();
}
/**
* Get the dependents of a project (projects that depend on it)
*/
getProjectDependents(projectName) {
return this.projectToDependents.get(projectName) || new Set();
}
/**
* Get the version actions for a project
*/
getVersionActionsForProject(projectName) {
return this.projectsToVersionActions.get(projectName);
}
/**
* Check if a project will be processed
*/
isProjectToProcess(projectName) {
return this.allProjectsToProcess.has(projectName);
}
/**
* Runs validation on resolved VersionActions instances. E.g. check that manifest files exist for all projects that will be processed.
* This should be called after preVersionCommand has run, as those commands may create manifest files that are needed for versioning.
*/
async validate(tree) {
const validationPromises = [];
for (const projectName of this.allProjectsToProcess) {
const versionActions = this.projectsToVersionActions.get(projectName);
if (versionActions) {
validationPromises.push(versionActions.validate(tree));
}
}
// Validate in parallel
await Promise.all(validationPromises);
}
}
exports.ReleaseGraph = ReleaseGraph;
/**
* Creates a complete release graph by analyzing all release groups, projects, and their relationships.
*
* This function builds the graph structure, applies filtering logic that considers dependencies
* and updateDependents configuration, and caches all necessary data.
*
* The returned graph can be reused across versioning, changelog, and publishing operations.
*/
async function createReleaseGraph(options) {
// Construct ReleaseGroupWithName objects from nxReleaseConfig
const releaseGroups = Object.entries(options.nxReleaseConfig.groups).map(([name, group]) => {
return {
...group,
name,
resolvedVersionPlans: group.versionPlans ? [] : false,
};
});
const graph = new ReleaseGraph(releaseGroups, options.filters);
await graph.init(options);
return graph;
}