UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

894 lines • 52.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.releaseChangelogCLIHandler = void 0; exports.createAPI = createAPI; exports.shouldCreateGitHubRelease = shouldCreateGitHubRelease; const chalk = require("chalk"); const enquirer_1 = require("enquirer"); const node_fs_1 = require("node:fs"); const semver_1 = require("semver"); const tmp_1 = require("tmp"); const nx_json_1 = require("../../config/nx-json"); const tree_1 = require("../../generators/tree"); const file_map_utils_1 = require("../../project-graph/file-map-utils"); const project_graph_1 = require("../../project-graph/project-graph"); const utils_1 = require("../../tasks-runner/utils"); const is_ci_1 = require("../../utils/is-ci"); const output_1 = require("../../utils/output"); const handle_errors_1 = require("../../utils/handle-errors"); const path_1 = require("../../utils/path"); const workspace_root_1 = require("../../utils/workspace-root"); const config_1 = require("./config/config"); const deep_merge_json_1 = require("./config/deep-merge-json"); const filter_release_groups_1 = require("./config/filter-release-groups"); const version_plans_1 = require("./config/version-plans"); const git_1 = require("./utils/git"); const github_1 = require("./utils/github"); const launch_editor_1 = require("./utils/launch-editor"); const markdown_1 = require("./utils/markdown"); const print_changes_1 = require("./utils/print-changes"); const print_config_1 = require("./utils/print-config"); const resolve_changelog_renderer_1 = require("./utils/resolve-changelog-renderer"); const resolve_nx_json_error_message_1 = require("./utils/resolve-nx-json-error-message"); const shared_1 = require("./utils/shared"); const releaseChangelogCLIHandler = (args) => (0, handle_errors_1.handleErrors)(args.verbose, () => createAPI({})(args)); exports.releaseChangelogCLIHandler = releaseChangelogCLIHandler; function createAPI(overrideReleaseConfig) { /** * NOTE: This function is also exported for programmatic usage and forms part of the public API * of Nx. We intentionally do not wrap the implementation with handleErrors because users need * to have control over their own error handling when using the API. */ return async function releaseChangelog(args) { const projectGraph = await (0, project_graph_1.createProjectGraphAsync)({ exitOnError: true }); const nxJson = (0, nx_json_1.readNxJson)(); const userProvidedReleaseConfig = (0, deep_merge_json_1.deepMergeJson)(nxJson.release ?? {}, overrideReleaseConfig ?? {}); // Apply default configuration to any optional user configuration const { error: configError, nxReleaseConfig } = await (0, config_1.createNxReleaseConfig)(projectGraph, await (0, file_map_utils_1.createProjectFileMapUsingProjectGraph)(projectGraph), userProvidedReleaseConfig); if (configError) { return await (0, config_1.handleNxReleaseConfigError)(configError); } // --print-config exits directly as it is not designed to be combined with any other programmatic operations if (args.printConfig) { return (0, print_config_1.printConfigAndExit)({ userProvidedReleaseConfig, nxReleaseConfig, isDebug: args.printConfig === 'debug', }); } // The nx release top level command will always override these three git args. This is how we can tell // if the top level release command was used or if the user is using the changelog subcommand. // If the user explicitly overrides these args, then it doesn't matter if the top level config is set, // as all of the git options would be overridden anyway. if ((args.gitCommit === undefined || args.gitTag === undefined || args.stageChanges === undefined) && userProvidedReleaseConfig.git) { const nxJsonMessage = await (0, resolve_nx_json_error_message_1.resolveNxJsonConfigErrorMessage)([ 'release', 'git', ]); output_1.output.error({ title: `The "release.git" property in nx.json may not be used with the "nx release changelog" subcommand or programmatic API. Instead, configure git options for subcommands directly with "release.version.git" and "release.changelog.git".`, bodyLines: [nxJsonMessage], }); process.exit(1); } const { error: filterError, filterLog, releaseGroups, releaseGroupToFilteredProjects, } = (0, filter_release_groups_1.filterReleaseGroups)(projectGraph, nxReleaseConfig, args.projects, args.groups); if (filterError) { output_1.output.error(filterError); process.exit(1); } if (filterLog && process.env.NX_RELEASE_INTERNAL_SUPPRESS_FILTER_LOG !== 'true') { output_1.output.note(filterLog); } const rawVersionPlans = await (0, version_plans_1.readRawVersionPlans)(); await (0, version_plans_1.setResolvedVersionPlansOnGroups)(rawVersionPlans, releaseGroups, Object.keys(projectGraph.nodes), args.verbose); if (args.deleteVersionPlans === undefined) { // default to deleting version plans in this command instead of after versioning args.deleteVersionPlans = true; } const changelogGenerationEnabled = !!nxReleaseConfig.changelog.workspaceChangelog || Object.values(nxReleaseConfig.groups).some((g) => g.changelog); if (!changelogGenerationEnabled) { output_1.output.warn({ title: `Changelogs are disabled. No changelog entries will be generated`, bodyLines: [ `To explicitly enable changelog generation, configure "release.changelog.workspaceChangelog" or "release.changelog.projectChangelogs" in nx.json.`, ], }); return {}; } const tree = new tree_1.FsTree(workspace_root_1.workspaceRoot, args.verbose); const useAutomaticFromRef = nxReleaseConfig.changelog?.automaticFromRef || args.firstRelease; /** * For determining the versions to use within changelog files, there are a few different possibilities: * - the user is using the nx CLI, and therefore passes a single --version argument which represents the version for any and all changelog * files which will be generated (i.e. both the workspace changelog, and all project changelogs, depending on which of those has been enabled) * - the user is using the nxReleaseChangelog API programmatically, and: * - passes only a version property * - this works in the same way as described above for the CLI * - passes only a versionData object * - this is a special case where the user is providing a version for each project, and therefore the version argument is not needed * - NOTE: it is not possible to generate a workspace level changelog with only a versionData object, and this will produce an error * - passes both a version and a versionData object * - in this case, the version property will be used as the reference for the workspace changelog, and the versionData object will be used * to generate project changelogs */ const { workspaceChangelogVersion, projectsVersionData } = resolveChangelogVersions(args, releaseGroups, releaseGroupToFilteredProjects); const to = args.to || 'HEAD'; const toSHA = await (0, git_1.getCommitHash)(to); const headSHA = to === 'HEAD' ? toSHA : await (0, git_1.getCommitHash)('HEAD'); /** * Protect the user against attempting to create a new commit when recreating an old release changelog, * this seems like it would always be unintentional. */ const autoCommitEnabled = args.gitCommit ?? nxReleaseConfig.changelog.git.commit; if (autoCommitEnabled && headSHA !== toSHA) { throw new Error(`You are attempting to recreate the changelog for an old release, but you have enabled auto-commit mode. Please disable auto-commit mode by updating your nx.json, or passing --git-commit=false`); } const commitMessage = args.gitCommitMessage || nxReleaseConfig.changelog.git.commitMessage; const commitMessageValues = (0, shared_1.createCommitMessageValues)(releaseGroups, releaseGroupToFilteredProjects, projectsVersionData, commitMessage); // Resolve any git tags as early as possible so that we can hard error in case of any duplicates before reaching the actual git command const gitTagValues = args.gitTag ?? nxReleaseConfig.changelog.git.tag ? (0, shared_1.createGitTagValues)(releaseGroups, releaseGroupToFilteredProjects, projectsVersionData) : []; (0, shared_1.handleDuplicateGitTags)(gitTagValues); const postGitTasks = []; let workspaceChangelogChanges = []; // TODO: remove this after the changelog renderer is refactored to remove coupling with git commits let workspaceChangelogCommits = []; // If there are multiple release groups, we'll just skip the workspace changelog anyway. const versionPlansEnabledForWorkspaceChangelog = releaseGroups[0].resolvedVersionPlans; if (versionPlansEnabledForWorkspaceChangelog) { if (releaseGroups.length === 1) { const releaseGroup = releaseGroups[0]; if (releaseGroup.projectsRelationship === 'fixed') { const versionPlans = releaseGroup.resolvedVersionPlans; workspaceChangelogChanges = versionPlans .flatMap((vp) => { const releaseType = versionPlanSemverReleaseTypeToChangelogType(vp.groupVersionBump); let githubReferences = []; let author = undefined; const parsedCommit = vp.commit ? (0, git_1.parseGitCommit)(vp.commit, true) : null; if (parsedCommit) { githubReferences = parsedCommit.references; author = parsedCommit.author; } const changes = !vp.triggeredByProjects ? { type: releaseType.type, scope: '', description: vp.message, body: '', isBreaking: releaseType.isBreaking, githubReferences, // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors authors: [author], affectedProjects: '*', } : vp.triggeredByProjects.map((project) => { return { type: releaseType.type, scope: project, description: vp.message, body: '', isBreaking: releaseType.isBreaking, githubReferences, // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors authors: [author], affectedProjects: [project], }; }); return changes; }) .filter(Boolean); } } } else { let workspaceChangelogFromRef = args.from || (await (0, git_1.getLatestGitTagForPattern)(nxReleaseConfig.releaseTagPattern, {}, nxReleaseConfig.releaseTagPatternCheckAllBranchesWhen))?.tag; if (!workspaceChangelogFromRef) { if (useAutomaticFromRef) { workspaceChangelogFromRef = await (0, git_1.getFirstGitCommit)(); if (args.verbose) { console.log(`Determined workspace --from ref from the first commit in the workspace: ${workspaceChangelogFromRef}`); } } else { throw new Error(`Unable to determine the previous git tag. If this is the first release of your workspace, use the --first-release option or set the "release.changelog.automaticFromRef" config property in nx.json to generate a changelog from the first commit. Otherwise, be sure to configure the "release.releaseTagPattern" property in nx.json to match the structure of your repository's git tags.`); } } // Make sure that the fromRef is actually resolvable const workspaceChangelogFromSHA = await (0, git_1.getCommitHash)(workspaceChangelogFromRef); workspaceChangelogCommits = await getCommits(workspaceChangelogFromSHA, toSHA); workspaceChangelogChanges = filterHiddenChanges(workspaceChangelogCommits.map((c) => { return { type: c.type, scope: c.scope, description: c.description, body: c.body, isBreaking: c.isBreaking, githubReferences: c.references, authors: [c.author], shortHash: c.shortHash, revertedHashes: c.revertedHashes, affectedProjects: '*', }; }), nxReleaseConfig.conventionalCommits); } const workspaceChangelog = await generateChangelogForWorkspace({ tree, args, projectGraph, nxReleaseConfig, workspaceChangelogVersion, changes: workspaceChangelogChanges, // TODO: remove this after the changelog renderer is refactored to remove coupling with git commits commits: filterHiddenCommits(workspaceChangelogCommits, nxReleaseConfig.conventionalCommits), }); if (workspaceChangelog && shouldCreateGitHubRelease(nxReleaseConfig.changelog.workspaceChangelog, args.createRelease)) { postGitTasks.push(async (latestCommit) => { output_1.output.logSingleLine(`Creating GitHub Release`); await (0, github_1.createOrUpdateGithubRelease)(nxReleaseConfig.changelog.workspaceChangelog ? nxReleaseConfig.changelog.workspaceChangelog.createRelease : config_1.defaultCreateReleaseProvider, workspaceChangelog.releaseVersion, workspaceChangelog.contents, latestCommit, { dryRun: args.dryRun }); }); } /** * Compute any additional dependency bumps up front because there could be cases of circular dependencies, * and figuring them out during the main iteration would be too late. */ const projectToAdditionalDependencyBumps = new Map(); for (const releaseGroup of releaseGroups) { if (releaseGroup.projectsRelationship !== 'independent') { continue; } for (const project of releaseGroup.projects) { // If the project does not have any changes, do not process its dependents if (!projectsVersionData[project] || projectsVersionData[project].newVersion === null) { continue; } const dependentProjects = (projectsVersionData[project].dependentProjects || []) .map((dep) => { return { dependencyName: dep.source, newVersion: projectsVersionData[dep.source].newVersion, }; }) .filter((b) => b.newVersion !== null); for (const dependent of dependentProjects) { const additionalDependencyBumpsForProject = projectToAdditionalDependencyBumps.has(dependent.dependencyName) ? projectToAdditionalDependencyBumps.get(dependent.dependencyName) : []; additionalDependencyBumpsForProject.push({ dependencyName: project, newVersion: projectsVersionData[project].newVersion, }); projectToAdditionalDependencyBumps.set(dependent.dependencyName, additionalDependencyBumpsForProject); } } } const allProjectChangelogs = {}; for (const releaseGroup of releaseGroups) { const config = releaseGroup.changelog; // The entire feature is disabled at the release group level, exit early if (config === false) { continue; } const projects = args.projects?.length ? // If the user has passed a list of projects, we need to use the filtered list of projects within the release group, plus any dependents Array.from(releaseGroupToFilteredProjects.get(releaseGroup)).flatMap((project) => { return [ project, ...(projectsVersionData[project]?.dependentProjects.map((dep) => dep.source) || []), ]; }) : // Otherwise, we use the full list of projects within the release group releaseGroup.projects; const projectNodes = projects.map((name) => projectGraph.nodes[name]); if (releaseGroup.projectsRelationship === 'independent') { for (const project of projectNodes) { let changes = null; // TODO: remove this after the changelog renderer is refactored to remove coupling with git commits let commits; if (releaseGroup.resolvedVersionPlans) { changes = releaseGroup.resolvedVersionPlans .map((vp) => { const bumpForProject = vp.projectVersionBumps[project.name]; if (!bumpForProject) { return null; } const releaseType = versionPlanSemverReleaseTypeToChangelogType(bumpForProject); let githubReferences = []; let authors = []; const parsedCommit = vp.commit ? (0, git_1.parseGitCommit)(vp.commit, true) : null; if (parsedCommit) { githubReferences = parsedCommit.references; // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors authors = [parsedCommit.author]; } return { type: releaseType.type, scope: project.name, description: vp.message, body: '', isBreaking: releaseType.isBreaking, affectedProjects: Object.keys(vp.projectVersionBumps), githubReferences, authors, }; }) .filter(Boolean); } else { let fromRef = args.from || (await (0, git_1.getLatestGitTagForPattern)(releaseGroup.releaseTagPattern, { projectName: project.name, releaseGroupName: releaseGroup.name, }, releaseGroup.releaseTagPatternCheckAllBranchesWhen))?.tag; if (!fromRef && useAutomaticFromRef) { const firstCommit = await (0, git_1.getFirstGitCommit)(); const allCommits = await getCommits(firstCommit, toSHA); const commitsForProject = allCommits.filter((c) => c.affectedFiles.find((f) => f.startsWith(project.data.root))); fromRef = commitsForProject[0]?.shortHash; if (args.verbose) { console.log(`Determined --from ref for ${project.name} from the first commit in which it exists: ${fromRef}`); } commits = commitsForProject; } if (!fromRef && !commits) { throw new Error(`Unable to determine the previous git tag. If this is the first release of your workspace, use the --first-release option or set the "release.changelog.automaticFromRef" config property in nx.json to generate a changelog from the first commit. Otherwise, be sure to configure the "release.releaseTagPattern" property in nx.json to match the structure of your repository's git tags.`); } if (!commits) { commits = await getCommits(fromRef, toSHA); } const { fileMap } = await (0, file_map_utils_1.createFileMapUsingProjectGraph)(projectGraph); const fileToProjectMap = createFileToProjectMap(fileMap.projectFileMap); changes = filterHiddenChanges(commits.map((c) => ({ type: c.type, scope: c.scope, description: c.description, body: c.body, isBreaking: c.isBreaking, githubReferences: c.references, // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors authors: [c.author], shortHash: c.shortHash, revertedHashes: c.revertedHashes, affectedProjects: commitChangesNonProjectFiles(c, fileMap.nonProjectFiles) ? '*' : getProjectsAffectedByCommit(c, fileToProjectMap), })), nxReleaseConfig.conventionalCommits); } const projectChangelogs = await generateChangelogForProjects({ tree, args, changes, projectsVersionData, releaseGroup, projects: [project], nxReleaseConfig, projectToAdditionalDependencyBumps, }); for (const [projectName, projectChangelog] of Object.entries(projectChangelogs)) { if (projectChangelogs && shouldCreateGitHubRelease(releaseGroup.changelog, args.createRelease)) { postGitTasks.push(async (latestCommit) => { output_1.output.logSingleLine(`Creating GitHub Release`); await (0, github_1.createOrUpdateGithubRelease)(releaseGroup.changelog ? releaseGroup.changelog.createRelease : config_1.defaultCreateReleaseProvider, projectChangelog.releaseVersion, projectChangelog.contents, latestCommit, { dryRun: args.dryRun }); }); } allProjectChangelogs[projectName] = projectChangelog; } } } else { let changes = []; // TODO: remove this after the changelog renderer is refactored to remove coupling with git commits let commits = []; if (releaseGroup.resolvedVersionPlans) { changes = releaseGroup.resolvedVersionPlans .flatMap((vp) => { const releaseType = versionPlanSemverReleaseTypeToChangelogType(vp.groupVersionBump); let githubReferences = []; let author = undefined; const parsedCommit = vp.commit ? (0, git_1.parseGitCommit)(vp.commit, true) : null; if (parsedCommit) { githubReferences = parsedCommit.references; author = parsedCommit.author; } const changes = !vp.triggeredByProjects ? { type: releaseType.type, scope: '', description: vp.message, body: '', isBreaking: releaseType.isBreaking, githubReferences, // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors authors: [author], affectedProjects: '*', } : vp.triggeredByProjects.map((project) => { return { type: releaseType.type, scope: project, description: vp.message, body: '', isBreaking: releaseType.isBreaking, githubReferences, // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors authors: [author], affectedProjects: [project], }; }); return changes; }) .filter(Boolean); } else { let fromRef = args.from || (await (0, git_1.getLatestGitTagForPattern)(releaseGroup.releaseTagPattern, {}, releaseGroup.releaseTagPatternCheckAllBranchesWhen))?.tag; if (!fromRef) { if (useAutomaticFromRef) { fromRef = await (0, git_1.getFirstGitCommit)(); if (args.verbose) { console.log(`Determined release group --from ref from the first commit in the workspace: ${fromRef}`); } } else { throw new Error(`Unable to determine the previous git tag. If this is the first release of your release group, use the --first-release option or set the "release.changelog.automaticFromRef" config property in nx.json to generate a changelog from the first commit. Otherwise, be sure to configure the "release.releaseTagPattern" property in nx.json to match the structure of your repository's git tags.`); } } // Make sure that the fromRef is actually resolvable const fromSHA = await (0, git_1.getCommitHash)(fromRef); const { fileMap } = await (0, file_map_utils_1.createFileMapUsingProjectGraph)(projectGraph); const fileToProjectMap = createFileToProjectMap(fileMap.projectFileMap); commits = await getCommits(fromSHA, toSHA); changes = filterHiddenChanges(commits.map((c) => ({ type: c.type, scope: c.scope, description: c.description, body: c.body, isBreaking: c.isBreaking, githubReferences: c.references, // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors authors: [c.author], shortHash: c.shortHash, revertedHashes: c.revertedHashes, affectedProjects: commitChangesNonProjectFiles(c, fileMap.nonProjectFiles) ? '*' : getProjectsAffectedByCommit(c, fileToProjectMap), })), nxReleaseConfig.conventionalCommits); } const projectChangelogs = await generateChangelogForProjects({ tree, args, changes, projectsVersionData, releaseGroup, projects: projectNodes, nxReleaseConfig, projectToAdditionalDependencyBumps, }); for (const [projectName, projectChangelog] of Object.entries(projectChangelogs)) { if (projectChangelogs && shouldCreateGitHubRelease(releaseGroup.changelog, args.createRelease)) { postGitTasks.push(async (latestCommit) => { output_1.output.logSingleLine(`Creating GitHub Release`); await (0, github_1.createOrUpdateGithubRelease)(releaseGroup.changelog ? releaseGroup.changelog.createRelease : config_1.defaultCreateReleaseProvider, projectChangelog.releaseVersion, projectChangelog.contents, latestCommit, { dryRun: args.dryRun }); }); } allProjectChangelogs[projectName] = projectChangelog; } } } await applyChangesAndExit(args, nxReleaseConfig, tree, toSHA, postGitTasks, commitMessageValues, gitTagValues, releaseGroups); return { workspaceChangelog, projectChangelogs: allProjectChangelogs, }; }; } function resolveChangelogVersions(args, releaseGroups, releaseGroupToFilteredProjects) { if (!args.version && !args.versionData) { throw new Error(`You must provide a version string and/or a versionData object.`); } /** * TODO: revaluate this assumption holistically in a dedicated PR when we add support for calver * (e.g. the Release class also uses semver utils to check if prerelease). * * Right now, the given version must be valid semver in order to proceed */ if (args.version && !(0, semver_1.valid)(args.version)) { throw new Error(`The given version "${args.version}" is not a valid semver version. Please provide your version in the format "1.0.0", "1.0.0-beta.1" etc`); } const versionData = releaseGroups.reduce((versionData, releaseGroup) => { const releaseGroupProjectNames = Array.from(releaseGroupToFilteredProjects.get(releaseGroup)); for (const projectName of releaseGroupProjectNames) { if (!args.versionData) { versionData[projectName] = { newVersion: args.version, currentVersion: '', // not relevant within changelog/commit generation dependentProjects: [], // not relevant within changelog/commit generation }; continue; } /** * In the case where a versionData object was provided, we need to make sure all projects are present, * otherwise it suggests a filtering mismatch between the version and changelog command invocations. */ if (!args.versionData[projectName]) { throw new Error(`The provided versionData object does not contain a version for project "${projectName}". This suggests a filtering mismatch between the version and changelog command invocations.`); } } return versionData; }, args.versionData || {}); return { workspaceChangelogVersion: args.version, projectsVersionData: versionData, }; } async function applyChangesAndExit(args, nxReleaseConfig, tree, toSHA, postGitTasks, commitMessageValues, gitTagValues, releaseGroups) { let latestCommit = toSHA; const changes = tree.listChanges(); /** * In the case where we are expecting changelog file updates, but there is nothing * to flush from the tree, we exit early. This could happen we using conventional * commits, for example. */ const changelogFilesEnabled = checkChangelogFilesEnabled(nxReleaseConfig); if (changelogFilesEnabled && !changes.length) { output_1.output.warn({ title: `No changes detected for changelogs`, bodyLines: [ `No changes were detected for any changelog files, so no changelog entries will be generated.`, ], }); if (!postGitTasks.length) { // no GitHub releases to create so we can just exit return; } if ((0, is_ci_1.isCI)()) { output_1.output.warn({ title: `Skipped GitHub release creation because no changes were detected for any changelog files.`, }); return; } // prompt the user to see if they want to create a GitHub release anyway // we know that the user has configured GitHub releases because we have postGitTasks const shouldCreateGitHubReleaseAnyway = await promptForGitHubRelease(); if (!shouldCreateGitHubReleaseAnyway) { return; } for (const postGitTask of postGitTasks) { await postGitTask(latestCommit); } return; } const changedFiles = changes.map((f) => f.path); let deletedFiles = []; if (args.deleteVersionPlans) { const planFiles = new Set(); releaseGroups.forEach((group) => { if (group.resolvedVersionPlans) { group.resolvedVersionPlans.forEach((plan) => { if (!args.dryRun) { (0, node_fs_1.rmSync)(plan.absolutePath, { recursive: true, force: true }); if (args.verbose) { console.log(`Removing ${plan.relativePath}`); } } else { if (args.verbose) { console.log(`Would remove ${plan.relativePath}, but --dry-run was set`); } } planFiles.add(plan.relativePath); }); } }); deletedFiles = Array.from(planFiles); } // Generate a new commit for the changes, if configured to do so if (args.gitCommit ?? nxReleaseConfig.changelog.git.commit) { await (0, shared_1.commitChanges)({ changedFiles, deletedFiles, isDryRun: !!args.dryRun, isVerbose: !!args.verbose, gitCommitMessages: commitMessageValues, gitCommitArgs: args.gitCommitArgs || nxReleaseConfig.changelog.git.commitArgs, }); // Resolve the commit we just made latestCommit = await (0, git_1.getCommitHash)('HEAD'); } else if ((args.stageChanges ?? nxReleaseConfig.changelog.git.stageChanges) && changes.length) { output_1.output.logSingleLine(`Staging changed files with git`); await (0, git_1.gitAdd)({ changedFiles, deletedFiles, dryRun: args.dryRun, verbose: args.verbose, }); } // Generate a one or more git tags for the changes, if configured to do so if (args.gitTag ?? nxReleaseConfig.changelog.git.tag) { output_1.output.logSingleLine(`Tagging commit with git`); for (const tag of gitTagValues) { await (0, git_1.gitTag)({ tag, message: args.gitTagMessage || nxReleaseConfig.changelog.git.tagMessage, additionalArgs: args.gitTagArgs || nxReleaseConfig.changelog.git.tagArgs, dryRun: args.dryRun, verbose: args.verbose, }); } } if (args.gitPush ?? nxReleaseConfig.changelog.git.push) { output_1.output.logSingleLine(`Pushing to git remote "${args.gitRemote}"`); await (0, git_1.gitPush)({ gitRemote: args.gitRemote, dryRun: args.dryRun, verbose: args.verbose, }); } // Run any post-git tasks in series for (const postGitTask of postGitTasks) { await postGitTask(latestCommit); } return; } async function generateChangelogForWorkspace({ tree, args, projectGraph, nxReleaseConfig, workspaceChangelogVersion, changes, commits, }) { const config = nxReleaseConfig.changelog.workspaceChangelog; // The entire feature is disabled at the workspace level, exit early if (config === false) { return; } // If explicitly null it must mean that no changes were detected (e.g. when using conventional commits), so do nothing if (workspaceChangelogVersion === null) { return; } // The user explicitly passed workspaceChangelog=true but does not have a workspace changelog config in nx.json if (!config) { throw new Error(`Workspace changelog is enabled but no configuration was provided. Please provide a workspaceChangelog object in your nx.json`); } if (Object.entries(nxReleaseConfig.groups).length > 1) { output_1.output.warn({ title: `Workspace changelog is enabled, but you have multiple release groups configured. This is not supported, so workspace changelog will be disabled.`, bodyLines: [ `A single workspace version cannot be determined when defining multiple release groups because versions can differ between each group.`, `Project level changelogs can be enabled with the "release.changelog.projectChangelogs" property.`, ], }); return; } if (Object.values(nxReleaseConfig.groups)[0].projectsRelationship === 'independent') { output_1.output.warn({ title: `Workspace changelog is enabled, but you have configured an independent projects relationship. This is not supported, so workspace changelog will be disabled.`, bodyLines: [ `A single workspace version cannot be determined when using independent projects because versions can differ between each project.`, `Project level changelogs can be enabled with the "release.changelog.projectChangelogs" property.`, ], }); return; } // Only trigger interactive mode for the workspace changelog if the user explicitly requested it via "all" or "workspace" const interactive = args.interactive === 'all' || args.interactive === 'workspace'; const dryRun = !!args.dryRun; const gitRemote = args.gitRemote; const ChangelogRendererClass = (0, resolve_changelog_renderer_1.resolveChangelogRenderer)(config.renderer); let interpolatedTreePath = config.file || ''; if (interpolatedTreePath) { interpolatedTreePath = (0, utils_1.interpolate)(interpolatedTreePath, { projectName: '', // n/a for the workspace changelog projectRoot: '', // n/a for the workspace changelog workspaceRoot: '', // within the tree, workspaceRoot is the root }); } const releaseVersion = new shared_1.ReleaseVersion({ version: workspaceChangelogVersion, releaseTagPattern: nxReleaseConfig.releaseTagPattern, }); if (interpolatedTreePath) { const prefix = dryRun ? 'Previewing' : 'Generating'; output_1.output.log({ title: `${prefix} an entry in ${interpolatedTreePath} for ${chalk.white(releaseVersion.gitTag)}`, }); } const githubRepoData = (0, github_1.getGitHubRepoData)(gitRemote, config.createRelease); const changelogRenderer = new ChangelogRendererClass({ changes, changelogEntryVersion: releaseVersion.rawVersion, project: null, isVersionPlans: false, repoData: githubRepoData, entryWhenNoChanges: config.entryWhenNoChanges, changelogRenderOptions: config.renderOptions, conventionalCommitsConfig: nxReleaseConfig.conventionalCommits, }); let contents = await changelogRenderer.render(); /** * If interactive mode, make the changelog contents available for the user to modify in their editor of choice, * in a similar style to git interactive rebases/merges. */ if (interactive) { const tmpDir = (0, tmp_1.dirSync)().name; const changelogPath = (0, path_1.joinPathFragments)(tmpDir, // Include the tree path in the name so that it is easier to identify which changelog file is being edited `PREVIEW__${interpolatedTreePath.replace(/\//g, '_')}`); (0, node_fs_1.writeFileSync)(changelogPath, contents); await (0, launch_editor_1.launchEditor)(changelogPath); contents = (0, node_fs_1.readFileSync)(changelogPath, 'utf-8'); } if (interpolatedTreePath) { let rootChangelogContents = tree.exists(interpolatedTreePath) ? tree.read(interpolatedTreePath).toString() : ''; if (rootChangelogContents) { // NOTE: right now existing releases are always expected to be in markdown format, but in the future we could potentially support others via a custom parser option const changelogReleases = (0, markdown_1.parseChangelogMarkdown)(rootChangelogContents).releases; const existingVersionToUpdate = changelogReleases.find((r) => r.version === releaseVersion.rawVersion); if (existingVersionToUpdate) { rootChangelogContents = rootChangelogContents.replace(`## ${releaseVersion.rawVersion}\n\n\n${existingVersionToUpdate.body}`, contents); } else { // No existing version, simply prepend the new release to the top of the file rootChangelogContents = `${contents}\n\n${rootChangelogContents}`; } } else { // No existing changelog contents, simply create a new one using the generated contents rootChangelogContents = contents; } tree.write(interpolatedTreePath, rootChangelogContents); (0, print_changes_1.printAndFlushChanges)(tree, !!dryRun, 3, false, shared_1.noDiffInChangelogMessage); } return { releaseVersion, contents, }; } async function generateChangelogForProjects({ tree, args, changes, projectsVersionData, releaseGroup, projects, nxReleaseConfig, projectToAdditionalDependencyBumps, }) { const config = releaseGroup.changelog; // The entire feature is disabled at the release group level, exit early if (config === false) { return; } // Only trigger interactive mode for the project changelog if the user explicitly requested it via "all" or "projects" const interactive = args.interactive === 'all' || args.interactive === 'projects'; const dryRun = !!args.dryRun; const gitRemote = args.gitRemote; const ChangelogRendererClass = (0, resolve_changelog_renderer_1.resolveChangelogRenderer)(config.renderer); const projectChangelogs = {}; for (const project of projects) { let interpolatedTreePath = config.file || ''; if (interpolatedTreePath) { interpolatedTreePath = (0, utils_1.interpolate)(interpolatedTreePath, { projectName: project.name, projectRoot: project.data.root, workspaceRoot: '', // within the tree, workspaceRoot is the root }); } /** * newVersion will be null in the case that no changes were detected (e.g. in conventional commits mode), * no changelog entry is relevant in that case. */ if (!projectsVersionData[project.name] || projectsVersionData[project.name].newVersion === null) { continue; } const releaseVersion = new shared_1.ReleaseVersion({ version: projectsVersionData[project.name].newVersion, releaseTagPattern: releaseGroup.releaseTagPattern, projectName: project.name, }); if (interpolatedTreePath) { const prefix = dryRun ? 'Previewing' : 'Generating'; output_1.output.log({ title: `${prefix} an entry in ${interpolatedTreePath} for ${chalk.white(releaseVersion.gitTag)}`, }); } const githubRepoData = (0, github_1.getGitHubRepoData)(gitRemote, config.createRelease); const changelogRenderer = new ChangelogRendererClass({ changes, changelogEntryVersion: releaseVersion.rawVersion, project: project.name, repoData: githubRepoData, entryWhenNoChanges: typeof config.entryWhenNoChanges === 'string' ? (0, utils_1.interpolate)(config.entryWhenNoChanges, { projectName: project.name, projectRoot: project.data.root, workspaceRoot: '', // within the tree, workspaceRoot is the root }) : false, changelogRenderOptions: config.renderOptions, isVersionPlans: !!releaseGroup.versionPlans, conventionalCommitsConfig: releaseGroup.versionPlans ? null : nxReleaseConfig.conventionalCommits, dependencyBumps: projectToAdditionalDependencyBumps.get(project.name), }); let contents = await changelogRenderer.render(); /** * If interactive mode, make the changelog contents available for the user to modify in their editor of choice, * in a similar style to git interactive rebases/merges. */ if (interactive) { const tmpDir = (0, tmp_1.dirSync)().name; const changelogPath = (0, path_1.joinPathFragments)(tmpDir, // Include the tree path in the name so that it is easier to identify which changelog file is being edited `PREVIEW__${interpolatedTreePath.replace(/\//g, '_')}`); (0, node_fs_1.writeFileSync)(changelogPath, contents); await (0, launch_editor_1.launchEditor)(changelogPath); contents = (0, node_fs_1.readFileSync)(changelogPath, 'utf-8'); } if (interpolatedTreePath) { let changelogContents = tree.exists(interpolatedTreePath) ? tree.read(interpolatedTreePath).toString() : ''; if (changelogContents) { // NOTE: right now existing releases are always expected to be in markdown format, but in the future we could potentially support others via a custom parser option const changelogReleases = (0, markdown_1.parseChangelogMarkdown)(changelogContents).releases; const existingVersionToUpdate = changelogReleases.find((r) => r.version === releaseVersion.rawVersion); if (existingVersionToUpdate) { changelogContents = changelogContents.replace(`## ${releaseVersion.rawVersion}\n\n\n${existingVersionToUpdate.body}`, contents); } else { // No existing version, simply prepend the new release to the top of the file changelogContents = `${contents}\n\n${changelogContents}`; } } else { // No existing changelog contents, simply create a new one using the generated contents changelogContents = contents; } tree.write(interpolatedTreePath, changelogContents); (0, print_changes_1.printAndFlushChanges)(tree, !!dryRun, 3, false, shared_1.noDiffInChangelogMessage, // Only print the change for the current changelog file at this point (f) => f.path === interpolatedTreePath); } projectChangelogs[project.name] = { releaseVersion, contents, }; } return projectChangelogs; } function checkChangelogFilesEnabled(nxReleaseConfig) { if (nxReleaseConfig.changelog.workspaceChangelog && nxReleaseConfig.changelog.workspaceChangelog.file) { return true; } for (const releaseGroup of Object.values(nxReleaseConfig.groups)) { if (releaseGroup.changelog && releaseGroup.changelog.file) { return true; } } return false; } async function getCommits(fromSHA, toSHA) { const rawCommits = await (0, git_1.getGitDiff)(fromSHA, toSHA); // Parse as conventional commits return (0, git_1.parseCommits)(rawCommits); } function filterHiddenChanges(changes, conventionalCommitsConfig) { return changes.filter((change) => { co