diff --git a/packages/nx/release/changelog-renderer/index.ts b/packages/nx/release/changelog-renderer/index.ts index 4cbd618a4627d8..b1eaf6667a91df 100644 --- a/packages/nx/release/changelog-renderer/index.ts +++ b/packages/nx/release/changelog-renderer/index.ts @@ -4,6 +4,7 @@ import { NxReleaseConfig } from '../../src/command-line/release/config/config'; import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from '../../src/command-line/release/config/conventional-commits'; import { GitCommit } from '../../src/command-line/release/utils/git'; import { + GithubRepoData, RepoSlug, formatReferences, } from '../../src/command-line/release/utils/github'; @@ -42,6 +43,7 @@ export type DependencyBump = { * @param {string | false} config.entryWhenNoChanges The (already interpolated) string to use as the changelog entry when there are no changes, or `false` if no entry should be generated * @param {ChangelogRenderOptions} config.changelogRenderOptions The options specific to the ChangelogRenderer implementation * @param {DependencyBump[]} config.dependencyBumps Optional list of additional dependency bumps that occurred as part of the release, outside of the commit data + * @param {GithubRepoData} config.repoData Resolved data for the current GitHub repository */ export type ChangelogRenderer = (config: { projectGraph: ProjectGraph; @@ -53,7 +55,9 @@ export type ChangelogRenderer = (config: { entryWhenNoChanges: string | false; changelogRenderOptions: DefaultChangelogRenderOptions; dependencyBumps?: DependencyBump[]; + // TODO(v20): remove repoSlug in favour of repoData repoSlug?: RepoSlug; + repoData?: GithubRepoData; // TODO(v20): Evaluate if there is a cleaner way to configure this when breaking changes are allowed // null if version plans are being used to generate the changelog conventionalCommitsConfig: NxReleaseConfig['conventionalCommits'] | null; @@ -101,6 +105,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({ dependencyBumps, repoSlug, conventionalCommitsConfig, + repoData, }): Promise => { const markdownLines: string[] = []; @@ -148,7 +153,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({ change, changelogRenderOptions, isVersionPlans, - repoSlug + repoData ); breakingChanges.push(line); relevantChanges.splice(i, 1); @@ -222,7 +227,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({ change, changelogRenderOptions, isVersionPlans, - repoSlug + repoData ); markdownLines.push(line); if (change.isBreaking) { @@ -295,7 +300,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({ change, changelogRenderOptions, isVersionPlans, - repoSlug + repoData ); markdownLines.push(line + '\n'); if (change.isBreaking) { @@ -350,7 +355,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({ } // Try to map authors to github usernames - if (repoSlug && changelogRenderOptions.mapAuthorsToGitHubUsernames) { + if (repoData && changelogRenderOptions.mapAuthorsToGitHubUsernames) { await Promise.all( [..._authors.keys()].map(async (authorName) => { const meta = _authors.get(authorName); @@ -455,7 +460,7 @@ function formatChange( change: ChangelogChange, changelogRenderOptions: DefaultChangelogRenderOptions, isVersionPlans: boolean, - repoSlug?: RepoSlug + repoData?: GithubRepoData ): string { let description = change.description; let extraLines = []; @@ -480,8 +485,8 @@ function formatChange( (!isVersionPlans && change.isBreaking ? '⚠️ ' : '') + (!isVersionPlans && change.scope ? `**${change.scope.trim()}:** ` : '') + description; - if (repoSlug && changelogRenderOptions.commitReferences) { - changeLine += formatReferences(change.githubReferences, repoSlug); + if (repoData && changelogRenderOptions.commitReferences) { + changeLine += formatReferences(change.githubReferences, repoData); } if (extraLinesStr) { changeLine += '\n\n' + extraLinesStr; diff --git a/packages/nx/schemas/nx-schema.json b/packages/nx/schemas/nx-schema.json index 6860662ef0686f..60f55e9e9312ff 100644 --- a/packages/nx/schemas/nx-schema.json +++ b/packages/nx/schemas/nx-schema.json @@ -691,6 +691,9 @@ { "type": "boolean", "enum": [false] + }, + { + "$ref": "#/definitions/CreateReleaseProviderConfiguration" } ] }, @@ -724,6 +727,24 @@ } } }, + "CreateReleaseProviderConfiguration": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": ["github-enterprise-server"] + }, + "hostname": { + "type": "string", + "description": "The hostname of the VCS provider instance, e.g. github.example.com" + }, + "apiBaseUrl": { + "type": "string", + "description": "The base URL for the relevant VCS provider API. If not set, this will default to `https://${hostname}/api/v3`" + } + }, + "required": ["provider", "hostname"] + }, "NxReleaseVersionPlansConfiguration": { "type": "object", "properties": { diff --git a/packages/nx/src/command-line/release/changelog.ts b/packages/nx/src/command-line/release/changelog.ts index 0fb524f6e0e380..7ae26b34236ee1 100644 --- a/packages/nx/src/command-line/release/changelog.ts +++ b/packages/nx/src/command-line/release/changelog.ts @@ -32,6 +32,7 @@ import { ChangelogOptions } from './command-object'; import { NxReleaseConfig, createNxReleaseConfig, + defaultCreateReleaseProvider, handleNxReleaseConfigError, } from './config/config'; import { deepMergeJson } from './config/deep-merge-json'; @@ -58,7 +59,7 @@ import { parseCommits, parseGitCommit, } from './utils/git'; -import { createOrUpdateGithubRelease, getGitHubRepoSlug } from './utils/github'; +import { createOrUpdateGithubRelease, getGitHubRepoData } from './utils/github'; import { launchEditor } from './utils/launch-editor'; import { parseChangelogMarkdown } from './utils/markdown'; import { printAndFlushChanges } from './utils/print-changes'; @@ -411,6 +412,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { output.logSingleLine(`Creating GitHub Release`); await createOrUpdateGithubRelease( + nxReleaseConfig.changelog.workspaceChangelog + ? nxReleaseConfig.changelog.workspaceChangelog.createRelease + : defaultCreateReleaseProvider, workspaceChangelog.releaseVersion, workspaceChangelog.contents, latestCommit, @@ -644,6 +648,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { output.logSingleLine(`Creating GitHub Release`); await createOrUpdateGithubRelease( + releaseGroup.changelog + ? releaseGroup.changelog.createRelease + : defaultCreateReleaseProvider, projectChangelog.releaseVersion, projectChangelog.contents, latestCommit, @@ -797,6 +804,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { output.logSingleLine(`Creating GitHub Release`); await createOrUpdateGithubRelease( + releaseGroup.changelog + ? releaseGroup.changelog.createRelease + : defaultCreateReleaseProvider, projectChangelog.releaseVersion, projectChangelog.contents, latestCommit, @@ -1110,7 +1120,7 @@ async function generateChangelogForWorkspace({ }); } - const githubRepoSlug = getGitHubRepoSlug(gitRemote); + const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease); let contents = await changelogRenderer({ projectGraph, @@ -1118,7 +1128,8 @@ async function generateChangelogForWorkspace({ commits, releaseVersion: releaseVersion.rawVersion, project: null, - repoSlug: githubRepoSlug, + repoSlug: githubRepoData?.slug, + repoData: githubRepoData, entryWhenNoChanges: config.entryWhenNoChanges, changelogRenderOptions: config.renderOptions, conventionalCommitsConfig: nxReleaseConfig.conventionalCommits, @@ -1250,10 +1261,7 @@ async function generateChangelogForProjects({ }); } - const githubRepoSlug = - config.createRelease === 'github' - ? getGitHubRepoSlug(gitRemote) - : undefined; + const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease); let contents = await changelogRenderer({ projectGraph, @@ -1261,7 +1269,8 @@ async function generateChangelogForProjects({ commits, releaseVersion: releaseVersion.rawVersion, project: project.name, - repoSlug: githubRepoSlug, + repoSlug: githubRepoData?.slug, + repoData: githubRepoData, entryWhenNoChanges: typeof config.entryWhenNoChanges === 'string' ? interpolate(config.entryWhenNoChanges, { @@ -1409,7 +1418,7 @@ export function shouldCreateGitHubRelease( return createReleaseArg === 'github'; } - return (changelogConfig || {}).createRelease === 'github'; + return (changelogConfig || {}).createRelease !== false; } async function promptForGitHubRelease(): Promise { diff --git a/packages/nx/src/command-line/release/config/config.spec.ts b/packages/nx/src/command-line/release/config/config.spec.ts index 0551d5ada882e5..966a6d5e1fd3bb 100644 --- a/packages/nx/src/command-line/release/config/config.spec.ts +++ b/packages/nx/src/command-line/release/config/config.spec.ts @@ -4898,15 +4898,19 @@ describe('createNxReleaseConfig()', () => { } `); }); - }); - describe('user config -> top level conventional commits configuration', () => { - it('should use defaults when config is empty', async () => { - const res1 = await createNxReleaseConfig(projectGraph, projectFileMap, { - conventionalCommits: {}, + it('should allow configuring a github-enterprise-server hostname and set a default apiBaseUrl for the workspace changelog', async () => { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { + changelog: { + workspaceChangelog: { + createRelease: { + provider: 'github-enterprise-server', + hostname: 'github.example.com', + }, + }, + }, }); - - expect(res1).toMatchInlineSnapshot(` + expect(res).toMatchInlineSnapshot(` { "error": null, "nxReleaseConfig": { @@ -4923,7 +4927,11 @@ describe('createNxReleaseConfig()', () => { }, "projectChangelogs": false, "workspaceChangelog": { - "createRelease": false, + "createRelease": { + "apiBaseUrl": "https://github.example.com/api/v3", + "hostname": "github.example.com", + "provider": "github-enterprise-server", + }, "entryWhenNoChanges": "This was a version bump only, there were no code changes.", "file": "{workspaceRoot}/CHANGELOG.md", "renderOptions": { @@ -5053,7 +5061,6 @@ describe('createNxReleaseConfig()', () => { "conventionalCommits": false, "generator": "@nx/js:release-version", "generatorOptions": {}, - "groupPreVersionCommand": "", }, "versionPlans": false, }, @@ -5079,14 +5086,21 @@ describe('createNxReleaseConfig()', () => { }, } `); + }); - const res2 = await createNxReleaseConfig(projectGraph, projectFileMap, { - conventionalCommits: { - types: {}, + it('should allow configuring a github-enterprise-server hostname AND a custom apiBaseUrl for the workspace changelog', async () => { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { + changelog: { + workspaceChangelog: { + createRelease: { + provider: 'github-enterprise-server', + hostname: 'github.example.com', + apiBaseUrl: 'http://something-custom.com', + }, + }, }, }); - - expect(res2).toMatchInlineSnapshot(` + expect(res).toMatchInlineSnapshot(` { "error": null, "nxReleaseConfig": { @@ -5103,7 +5117,11 @@ describe('createNxReleaseConfig()', () => { }, "projectChangelogs": false, "workspaceChangelog": { - "createRelease": false, + "createRelease": { + "apiBaseUrl": "http://something-custom.com", + "hostname": "github.example.com", + "provider": "github-enterprise-server", + }, "entryWhenNoChanges": "This was a version bump only, there were no code changes.", "file": "{workspaceRoot}/CHANGELOG.md", "renderOptions": { @@ -5233,7 +5251,6 @@ describe('createNxReleaseConfig()', () => { "conventionalCommits": false, "generator": "@nx/js:release-version", "generatorOptions": {}, - "groupPreVersionCommand": "", }, "versionPlans": false, }, @@ -5261,33 +5278,80 @@ describe('createNxReleaseConfig()', () => { `); }); - it('should merge defaults with overrides and new commit types', async () => { - const res = await createNxReleaseConfig(projectGraph, projectFileMap, { - conventionalCommits: { - types: { - feat: { - changelog: { - hidden: true, - }, - }, - chore: { - semverBump: 'patch', - changelog: { - title: 'Custom Chore Title', - hidden: false, - }, + it('should return an error if an invalid provider, hostname or apiBaseUrl is specified for createRelease for the workspace changelog', async () => { + const res1 = await createNxReleaseConfig(projectGraph, projectFileMap, { + changelog: { + workspaceChangelog: { + createRelease: { + provider: 'something-invalid', + } as any, + }, + }, + }); + expect(res1.error).toMatchInlineSnapshot(` + { + "code": "INVALID_CHANGELOG_CREATE_RELEASE_PROVIDER", + "data": { + "provider": "something-invalid", + "supportedProviders": [ + "github-enterprise-server", + ], + }, + } + `); + + const res2 = await createNxReleaseConfig(projectGraph, projectFileMap, { + changelog: { + workspaceChangelog: { + createRelease: { + provider: 'github-enterprise-server', + hostname: 'not_a_hostname', }, - customType: { - semverBump: 'major', - changelog: { - title: 'Custom Type Title', - }, + }, + }, + }); + expect(res2.error).toMatchInlineSnapshot(` + { + "code": "INVALID_CHANGELOG_CREATE_RELEASE_HOSTNAME", + "data": { + "hostname": "not_a_hostname", + }, + } + `); + + const res3 = await createNxReleaseConfig(projectGraph, projectFileMap, { + changelog: { + workspaceChangelog: { + createRelease: { + provider: 'github-enterprise-server', + hostname: 'example.com', + apiBaseUrl: 'not_a_url', }, - customTypeWithDefaults: {}, }, }, }); + expect(res3.error).toMatchInlineSnapshot(` + { + "code": "INVALID_CHANGELOG_CREATE_RELEASE_API_BASE_URL", + "data": { + "apiBaseUrl": "not_a_url", + }, + } + `); + }); + it('should allow configuring a github-enterprise-server hostname and set a default apiBaseUrl for project changelogs', async () => { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { + changelog: { + workspaceChangelog: false, + projectChangelogs: { + createRelease: { + provider: 'github-enterprise-server', + hostname: 'github.example.com', + }, + }, + }, + }); expect(res).toMatchInlineSnapshot(` { "error": null, @@ -5303,11 +5367,14 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, - "projectChangelogs": false, - "workspaceChangelog": { - "createRelease": false, - "entryWhenNoChanges": "This was a version bump only, there were no code changes.", - "file": "{workspaceRoot}/CHANGELOG.md", + "projectChangelogs": { + "createRelease": { + "apiBaseUrl": "https://github.example.com/api/v3", + "hostname": "github.example.com", + "provider": "github-enterprise-server", + }, + "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", + "file": "{projectRoot}/CHANGELOG.md", "renderOptions": { "authors": true, "commitReferences": true, @@ -5316,6 +5383,7 @@ describe('createNxReleaseConfig()', () => { }, "renderer": "/release/changelog-renderer", }, + "workspaceChangelog": false, }, "conventionalCommits": { "types": { @@ -5328,10 +5396,10 @@ describe('createNxReleaseConfig()', () => { }, "chore": { "changelog": { - "hidden": false, - "title": "Custom Chore Title", + "hidden": true, + "title": "🏡 Chore", }, - "semverBump": "patch", + "semverBump": "none", }, "ci": { "changelog": { @@ -5340,20 +5408,6 @@ describe('createNxReleaseConfig()', () => { }, "semverBump": "none", }, - "customType": { - "changelog": { - "hidden": false, - "title": "Custom Type Title", - }, - "semverBump": "major", - }, - "customTypeWithDefaults": { - "changelog": { - "hidden": false, - "title": "customTypeWithDefaults", - }, - "semverBump": "patch", - }, "docs": { "changelog": { "hidden": true, @@ -5370,7 +5424,7 @@ describe('createNxReleaseConfig()', () => { }, "feat": { "changelog": { - "hidden": true, + "hidden": false, "title": "🚀 Features", }, "semverBump": "minor", @@ -5437,7 +5491,22 @@ describe('createNxReleaseConfig()', () => { }, "groups": { "__default__": { - "changelog": false, + "changelog": { + "createRelease": { + "apiBaseUrl": "https://github.example.com/api/v3", + "hostname": "github.example.com", + "provider": "github-enterprise-server", + }, + "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", + "file": "{projectRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": true, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, "projects": [ "lib-a", "lib-b", @@ -5449,7 +5518,6 @@ describe('createNxReleaseConfig()', () => { "conventionalCommits": false, "generator": "@nx/js:release-version", "generatorOptions": {}, - "groupPreVersionCommand": "", }, "versionPlans": false, }, @@ -5477,16 +5545,19 @@ describe('createNxReleaseConfig()', () => { `); }); - it('should parse shorthand for disabling a commit type', async () => { + it('should allow configuring a github-enterprise-server hostname AND a custom apiBaseUrl for project changelogs', async () => { const res = await createNxReleaseConfig(projectGraph, projectFileMap, { - conventionalCommits: { - types: { - feat: false, - customType: false, + changelog: { + workspaceChangelog: false, + projectChangelogs: { + createRelease: { + provider: 'github-enterprise-server', + hostname: 'github.example.com', + apiBaseUrl: 'http://something-custom.com', + }, }, }, }); - expect(res).toMatchInlineSnapshot(` { "error": null, @@ -5502,11 +5573,14 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, - "projectChangelogs": false, - "workspaceChangelog": { - "createRelease": false, - "entryWhenNoChanges": "This was a version bump only, there were no code changes.", - "file": "{workspaceRoot}/CHANGELOG.md", + "projectChangelogs": { + "createRelease": { + "apiBaseUrl": "http://something-custom.com", + "hostname": "github.example.com", + "provider": "github-enterprise-server", + }, + "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", + "file": "{projectRoot}/CHANGELOG.md", "renderOptions": { "authors": true, "commitReferences": true, @@ -5515,6 +5589,7 @@ describe('createNxReleaseConfig()', () => { }, "renderer": "/release/changelog-renderer", }, + "workspaceChangelog": false, }, "conventionalCommits": { "types": { @@ -5539,13 +5614,6 @@ describe('createNxReleaseConfig()', () => { }, "semverBump": "none", }, - "customType": { - "changelog": { - "hidden": true, - "title": "customType", - }, - "semverBump": "none", - }, "docs": { "changelog": { "hidden": true, @@ -5562,10 +5630,10 @@ describe('createNxReleaseConfig()', () => { }, "feat": { "changelog": { - "hidden": true, + "hidden": false, "title": "🚀 Features", }, - "semverBump": "none", + "semverBump": "minor", }, "fix": { "changelog": { @@ -5629,7 +5697,22 @@ describe('createNxReleaseConfig()', () => { }, "groups": { "__default__": { - "changelog": false, + "changelog": { + "createRelease": { + "apiBaseUrl": "http://something-custom.com", + "hostname": "github.example.com", + "provider": "github-enterprise-server", + }, + "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", + "file": "{projectRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": true, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, "projects": [ "lib-a", "lib-b", @@ -5641,7 +5724,6 @@ describe('createNxReleaseConfig()', () => { "conventionalCommits": false, "generator": "@nx/js:release-version", "generatorOptions": {}, - "groupPreVersionCommand": "", }, "versionPlans": false, }, @@ -5669,20 +5751,76 @@ describe('createNxReleaseConfig()', () => { `); }); - it('should parse shorthand for enabling a commit type', async () => { - const res = await createNxReleaseConfig(projectGraph, projectFileMap, { - conventionalCommits: { - types: { - feat: true, - fix: true, - perf: true, - docs: true, - customType: true, + it('should return an error if an invalid provider, hostname or apiBaseUrl is specified for createRelease for project changelogs', async () => { + const res1 = await createNxReleaseConfig(projectGraph, projectFileMap, { + changelog: { + projectChangelogs: { + createRelease: { + provider: 'something-invalid', + } as any, }, }, }); + expect(res1.error).toMatchInlineSnapshot(` + { + "code": "INVALID_CHANGELOG_CREATE_RELEASE_PROVIDER", + "data": { + "provider": "something-invalid", + "supportedProviders": [ + "github-enterprise-server", + ], + }, + } + `); - expect(res).toMatchInlineSnapshot(` + const res2 = await createNxReleaseConfig(projectGraph, projectFileMap, { + changelog: { + projectChangelogs: { + createRelease: { + provider: 'github-enterprise-server', + hostname: 'not_a_hostname', + }, + }, + }, + }); + expect(res2.error).toMatchInlineSnapshot(` + { + "code": "INVALID_CHANGELOG_CREATE_RELEASE_HOSTNAME", + "data": { + "hostname": "not_a_hostname", + }, + } + `); + + const res3 = await createNxReleaseConfig(projectGraph, projectFileMap, { + changelog: { + projectChangelogs: { + createRelease: { + provider: 'github-enterprise-server', + hostname: 'example.com', + apiBaseUrl: 'not_a_url', + }, + }, + }, + }); + expect(res3.error).toMatchInlineSnapshot(` + { + "code": "INVALID_CHANGELOG_CREATE_RELEASE_API_BASE_URL", + "data": { + "apiBaseUrl": "not_a_url", + }, + } + `); + }); + }); + + describe('user config -> top level conventional commits configuration', () => { + it('should use defaults when config is empty', async () => { + const res1 = await createNxReleaseConfig(projectGraph, projectFileMap, { + conventionalCommits: {}, + }); + + expect(res1).toMatchInlineSnapshot(` { "error": null, "nxReleaseConfig": { @@ -5734,19 +5872,12 @@ describe('createNxReleaseConfig()', () => { }, "semverBump": "none", }, - "customType": { - "changelog": { - "hidden": false, - "title": "customType", - }, - "semverBump": "patch", - }, "docs": { "changelog": { - "hidden": false, + "hidden": true, "title": "📖 Documentation", }, - "semverBump": "patch", + "semverBump": "none", }, "examples": { "changelog": { @@ -5774,7 +5905,7 @@ describe('createNxReleaseConfig()', () => { "hidden": false, "title": "🔥 Performance", }, - "semverBump": "patch", + "semverBump": "none", }, "refactor": { "changelog": { @@ -5862,23 +5993,14 @@ describe('createNxReleaseConfig()', () => { }, } `); - }); - it('should parse shorthand for disabling changelog appearance for a commit type', async () => { - const res = await createNxReleaseConfig(projectGraph, projectFileMap, { + const res2 = await createNxReleaseConfig(projectGraph, projectFileMap, { conventionalCommits: { - types: { - fix: { - changelog: false, - }, - customType: { - changelog: false, - }, - }, + types: {}, }, }); - expect(res).toMatchInlineSnapshot(` + expect(res2).toMatchInlineSnapshot(` { "error": null, "nxReleaseConfig": { @@ -5930,13 +6052,6 @@ describe('createNxReleaseConfig()', () => { }, "semverBump": "none", }, - "customType": { - "changelog": { - "hidden": true, - "title": "customType", - }, - "semverBump": "patch", - }, "docs": { "changelog": { "hidden": true, @@ -5960,7 +6075,7 @@ describe('createNxReleaseConfig()', () => { }, "fix": { "changelog": { - "hidden": true, + "hidden": false, "title": "🩹 Fixes", }, "semverBump": "patch", @@ -6060,19 +6175,29 @@ describe('createNxReleaseConfig()', () => { `); }); - it('should parse shorthand for enabling changelog appearance for a commit type', async () => { + it('should merge defaults with overrides and new commit types', async () => { const res = await createNxReleaseConfig(projectGraph, projectFileMap, { conventionalCommits: { types: { - fix: { - changelog: true, + feat: { + changelog: { + hidden: true, + }, }, - docs: { - changelog: true, + chore: { + semverBump: 'patch', + changelog: { + title: 'Custom Chore Title', + hidden: false, + }, }, customType: { - changelog: true, + semverBump: 'major', + changelog: { + title: 'Custom Type Title', + }, }, + customTypeWithDefaults: {}, }, }, }); @@ -6117,10 +6242,10 @@ describe('createNxReleaseConfig()', () => { }, "chore": { "changelog": { - "hidden": true, - "title": "🏡 Chore", + "hidden": false, + "title": "Custom Chore Title", }, - "semverBump": "none", + "semverBump": "patch", }, "ci": { "changelog": { @@ -6132,16 +6257,23 @@ describe('createNxReleaseConfig()', () => { "customType": { "changelog": { "hidden": false, - "title": "customType", + "title": "Custom Type Title", + }, + "semverBump": "major", + }, + "customTypeWithDefaults": { + "changelog": { + "hidden": false, + "title": "customTypeWithDefaults", }, "semverBump": "patch", }, "docs": { "changelog": { - "hidden": false, + "hidden": true, "title": "📖 Documentation", }, - "semverBump": "patch", + "semverBump": "none", }, "examples": { "changelog": { @@ -6152,7 +6284,7 @@ describe('createNxReleaseConfig()', () => { }, "feat": { "changelog": { - "hidden": false, + "hidden": true, "title": "🚀 Features", }, "semverBump": "minor", @@ -6258,43 +6390,17 @@ describe('createNxReleaseConfig()', () => { } `); }); - }); - describe('user config -> top level and group level changelog combined', () => { - it('should respect any adjustments to default changelog config at the top level and group level in the final config, CASE 1', async () => { + it('should parse shorthand for disabling a commit type', async () => { const res = await createNxReleaseConfig(projectGraph, projectFileMap, { - changelog: { - projectChangelogs: { - // overriding field at the root should be inherited by all groups that do not set their own override - file: './{projectRoot}/custom-path.md', - renderOptions: { - authors: true, // should be overridden by group level config - }, - }, - }, - groups: { - 'group-1': { - projects: ['lib-a'], - changelog: { - createRelease: 'github', // set field in group config - renderOptions: { - authors: false, // override deeply nested field in group config - mapAuthorsToGitHubUsernames: false, // override deeply nested field in group config - }, - }, - }, - 'group-2': { - projects: ['lib-b'], - changelog: false, // disabled changelog for this group - }, - 'group-3': { - projects: ['nx'], - changelog: { - file: './{projectRoot}/a-different-custom-path-at-the-group.md', // a different override field at the group level - }, + conventionalCommits: { + types: { + feat: false, + customType: false, }, }, }); + expect(res).toMatchInlineSnapshot(` { "error": null, @@ -6310,10 +6416,11 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, - "projectChangelogs": { + "projectChangelogs": false, + "workspaceChangelog": { "createRelease": false, - "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", - "file": "./{projectRoot}/custom-path.md", + "entryWhenNoChanges": "This was a version bump only, there were no code changes.", + "file": "{workspaceRoot}/CHANGELOG.md", "renderOptions": { "authors": true, "commitReferences": true, @@ -6322,7 +6429,6 @@ describe('createNxReleaseConfig()', () => { }, "renderer": "/release/changelog-renderer", }, - "workspaceChangelog": false, }, "conventionalCommits": { "types": { @@ -6347,6 +6453,13 @@ describe('createNxReleaseConfig()', () => { }, "semverBump": "none", }, + "customType": { + "changelog": { + "hidden": true, + "title": "customType", + }, + "semverBump": "none", + }, "docs": { "changelog": { "hidden": true, @@ -6363,10 +6476,10 @@ describe('createNxReleaseConfig()', () => { }, "feat": { "changelog": { - "hidden": false, + "hidden": true, "title": "🚀 Features", }, - "semverBump": "minor", + "semverBump": "none", }, "fix": { "changelog": { @@ -6429,61 +6542,11 @@ describe('createNxReleaseConfig()', () => { "tagMessage": "", }, "groups": { - "group-1": { - "changelog": { - "createRelease": "github", - "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", - "file": "./{projectRoot}/custom-path.md", - "renderOptions": { - "authors": false, - "commitReferences": true, - "mapAuthorsToGitHubUsernames": false, - "versionTitleDate": true, - }, - "renderer": "/release/changelog-renderer", - }, - "projects": [ - "lib-a", - ], - "projectsRelationship": "fixed", - "releaseTagPattern": "v{version}", - "version": { - "conventionalCommits": false, - "generator": "@nx/js:release-version", - "generatorOptions": {}, - "groupPreVersionCommand": "", - }, - "versionPlans": false, - }, - "group-2": { + "__default__": { "changelog": false, "projects": [ + "lib-a", "lib-b", - ], - "projectsRelationship": "fixed", - "releaseTagPattern": "v{version}", - "version": { - "conventionalCommits": false, - "generator": "@nx/js:release-version", - "generatorOptions": {}, - "groupPreVersionCommand": "", - }, - "versionPlans": false, - }, - "group-3": { - "changelog": { - "createRelease": false, - "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", - "file": "./{projectRoot}/a-different-custom-path-at-the-group.md", - "renderOptions": { - "authors": true, - "commitReferences": true, - "mapAuthorsToGitHubUsernames": true, - "versionTitleDate": true, - }, - "renderer": "/release/changelog-renderer", - }, - "projects": [ "nx", ], "projectsRelationship": "fixed", @@ -6520,21 +6583,15 @@ describe('createNxReleaseConfig()', () => { `); }); - it('should respect any adjustments to default changelog config at the top level and group level in the final config, CASE 2', async () => { + it('should parse shorthand for enabling a commit type', async () => { const res = await createNxReleaseConfig(projectGraph, projectFileMap, { - groups: { - foo: { - projects: 'lib-a', - releaseTagPattern: '{projectName}-{version}', - }, - }, - changelog: { - workspaceChangelog: { - createRelease: 'github', - }, - // enabling project changelogs at the workspace level should cause each group to have project changelogs enabled - projectChangelogs: { - createRelease: 'github', + conventionalCommits: { + types: { + feat: true, + fix: true, + perf: true, + docs: true, + customType: true, }, }, }); @@ -6554,20 +6611,9 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, - "projectChangelogs": { - "createRelease": "github", - "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", - "file": "{projectRoot}/CHANGELOG.md", - "renderOptions": { - "authors": true, - "commitReferences": true, - "mapAuthorsToGitHubUsernames": true, - "versionTitleDate": true, - }, - "renderer": "/release/changelog-renderer", - }, + "projectChangelogs": false, "workspaceChangelog": { - "createRelease": "github", + "createRelease": false, "entryWhenNoChanges": "This was a version bump only, there were no code changes.", "file": "{workspaceRoot}/CHANGELOG.md", "renderOptions": { @@ -6602,12 +6648,19 @@ describe('createNxReleaseConfig()', () => { }, "semverBump": "none", }, + "customType": { + "changelog": { + "hidden": false, + "title": "customType", + }, + "semverBump": "patch", + }, "docs": { "changelog": { - "hidden": true, + "hidden": false, "title": "📖 Documentation", }, - "semverBump": "none", + "semverBump": "patch", }, "examples": { "changelog": { @@ -6635,7 +6688,7 @@ describe('createNxReleaseConfig()', () => { "hidden": false, "title": "🔥 Performance", }, - "semverBump": "none", + "semverBump": "patch", }, "refactor": { "changelog": { @@ -6684,24 +6737,15 @@ describe('createNxReleaseConfig()', () => { "tagMessage": "", }, "groups": { - "foo": { - "changelog": { - "createRelease": "github", - "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", - "file": "{projectRoot}/CHANGELOG.md", - "renderOptions": { - "authors": true, - "commitReferences": true, - "mapAuthorsToGitHubUsernames": true, - "versionTitleDate": true, - }, - "renderer": "/release/changelog-renderer", - }, + "__default__": { + "changelog": false, "projects": [ "lib-a", + "lib-b", + "nx", ], "projectsRelationship": "fixed", - "releaseTagPattern": "{projectName}-{version}", + "releaseTagPattern": "v{version}", "version": { "conventionalCommits": false, "generator": "@nx/js:release-version", @@ -6734,23 +6778,1384 @@ describe('createNxReleaseConfig()', () => { `); }); - it('should return an error if no projects can be resolved for a group', async () => { + it('should parse shorthand for disabling changelog appearance for a commit type', async () => { const res = await createNxReleaseConfig(projectGraph, projectFileMap, { - groups: { - 'group-1': { - projects: ['lib-does-not-exist'], + conventionalCommits: { + types: { + fix: { + changelog: false, + }, + customType: { + changelog: false, + }, }, }, }); + expect(res).toMatchInlineSnapshot(` { - "error": { - "code": "RELEASE_GROUP_MATCHES_NO_PROJECTS", - "data": { - "releaseGroupName": "group-1", - }, - }, - "nxReleaseConfig": null, + "error": null, + "nxReleaseConfig": { + "changelog": { + "automaticFromRef": false, + "git": { + "commit": true, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": true, + "tagArgs": "", + "tagMessage": "", + }, + "projectChangelogs": false, + "workspaceChangelog": { + "createRelease": false, + "entryWhenNoChanges": "This was a version bump only, there were no code changes.", + "file": "{workspaceRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": true, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, + }, + "conventionalCommits": { + "types": { + "build": { + "changelog": { + "hidden": true, + "title": "📦 Build", + }, + "semverBump": "none", + }, + "chore": { + "changelog": { + "hidden": true, + "title": "🏡 Chore", + }, + "semverBump": "none", + }, + "ci": { + "changelog": { + "hidden": true, + "title": "🤖 CI", + }, + "semverBump": "none", + }, + "customType": { + "changelog": { + "hidden": true, + "title": "customType", + }, + "semverBump": "patch", + }, + "docs": { + "changelog": { + "hidden": true, + "title": "📖 Documentation", + }, + "semverBump": "none", + }, + "examples": { + "changelog": { + "hidden": true, + "title": "🏀 Examples", + }, + "semverBump": "none", + }, + "feat": { + "changelog": { + "hidden": false, + "title": "🚀 Features", + }, + "semverBump": "minor", + }, + "fix": { + "changelog": { + "hidden": true, + "title": "🩹 Fixes", + }, + "semverBump": "patch", + }, + "perf": { + "changelog": { + "hidden": false, + "title": "🔥 Performance", + }, + "semverBump": "none", + }, + "refactor": { + "changelog": { + "hidden": true, + "title": "💅 Refactors", + }, + "semverBump": "none", + }, + "revert": { + "changelog": { + "hidden": true, + "title": "⏪ Revert", + }, + "semverBump": "none", + }, + "style": { + "changelog": { + "hidden": true, + "title": "🎨 Styles", + }, + "semverBump": "none", + }, + "test": { + "changelog": { + "hidden": true, + "title": "✅ Tests", + }, + "semverBump": "none", + }, + "types": { + "changelog": { + "hidden": true, + "title": "🌊 Types", + }, + "semverBump": "none", + }, + }, + }, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "groups": { + "__default__": { + "changelog": false, + "projects": [ + "lib-a", + "lib-b", + "nx", + ], + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "groupPreVersionCommand": "", + }, + "versionPlans": false, + }, + }, + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": true, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "preVersionCommand": "", + }, + "versionPlans": false, + }, + } + `); + }); + + it('should parse shorthand for enabling changelog appearance for a commit type', async () => { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { + conventionalCommits: { + types: { + fix: { + changelog: true, + }, + docs: { + changelog: true, + }, + customType: { + changelog: true, + }, + }, + }, + }); + + expect(res).toMatchInlineSnapshot(` + { + "error": null, + "nxReleaseConfig": { + "changelog": { + "automaticFromRef": false, + "git": { + "commit": true, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": true, + "tagArgs": "", + "tagMessage": "", + }, + "projectChangelogs": false, + "workspaceChangelog": { + "createRelease": false, + "entryWhenNoChanges": "This was a version bump only, there were no code changes.", + "file": "{workspaceRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": true, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, + }, + "conventionalCommits": { + "types": { + "build": { + "changelog": { + "hidden": true, + "title": "📦 Build", + }, + "semverBump": "none", + }, + "chore": { + "changelog": { + "hidden": true, + "title": "🏡 Chore", + }, + "semverBump": "none", + }, + "ci": { + "changelog": { + "hidden": true, + "title": "🤖 CI", + }, + "semverBump": "none", + }, + "customType": { + "changelog": { + "hidden": false, + "title": "customType", + }, + "semverBump": "patch", + }, + "docs": { + "changelog": { + "hidden": false, + "title": "📖 Documentation", + }, + "semverBump": "patch", + }, + "examples": { + "changelog": { + "hidden": true, + "title": "🏀 Examples", + }, + "semverBump": "none", + }, + "feat": { + "changelog": { + "hidden": false, + "title": "🚀 Features", + }, + "semverBump": "minor", + }, + "fix": { + "changelog": { + "hidden": false, + "title": "🩹 Fixes", + }, + "semverBump": "patch", + }, + "perf": { + "changelog": { + "hidden": false, + "title": "🔥 Performance", + }, + "semverBump": "none", + }, + "refactor": { + "changelog": { + "hidden": true, + "title": "💅 Refactors", + }, + "semverBump": "none", + }, + "revert": { + "changelog": { + "hidden": true, + "title": "⏪ Revert", + }, + "semverBump": "none", + }, + "style": { + "changelog": { + "hidden": true, + "title": "🎨 Styles", + }, + "semverBump": "none", + }, + "test": { + "changelog": { + "hidden": true, + "title": "✅ Tests", + }, + "semverBump": "none", + }, + "types": { + "changelog": { + "hidden": true, + "title": "🌊 Types", + }, + "semverBump": "none", + }, + }, + }, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "groups": { + "__default__": { + "changelog": false, + "projects": [ + "lib-a", + "lib-b", + "nx", + ], + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "groupPreVersionCommand": "", + }, + "versionPlans": false, + }, + }, + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": true, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "preVersionCommand": "", + }, + "versionPlans": false, + }, + } + `); + }); + }); + + describe('user config -> top level and group level changelog combined', () => { + it('should respect any adjustments to default changelog config at the top level and group level in the final config, CASE 1', async () => { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { + changelog: { + projectChangelogs: { + // overriding field at the root should be inherited by all groups that do not set their own override + file: './{projectRoot}/custom-path.md', + renderOptions: { + authors: true, // should be overridden by group level config + }, + }, + }, + groups: { + 'group-1': { + projects: ['lib-a'], + changelog: { + createRelease: 'github', // set field in group config + renderOptions: { + authors: false, // override deeply nested field in group config + mapAuthorsToGitHubUsernames: false, // override deeply nested field in group config + }, + }, + }, + 'group-2': { + projects: ['lib-b'], + changelog: false, // disabled changelog for this group + }, + 'group-3': { + projects: ['nx'], + changelog: { + file: './{projectRoot}/a-different-custom-path-at-the-group.md', // a different override field at the group level + }, + }, + }, + }); + expect(res).toMatchInlineSnapshot(` + { + "error": null, + "nxReleaseConfig": { + "changelog": { + "automaticFromRef": false, + "git": { + "commit": true, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": true, + "tagArgs": "", + "tagMessage": "", + }, + "projectChangelogs": { + "createRelease": false, + "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", + "file": "./{projectRoot}/custom-path.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": true, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, + "workspaceChangelog": false, + }, + "conventionalCommits": { + "types": { + "build": { + "changelog": { + "hidden": true, + "title": "📦 Build", + }, + "semverBump": "none", + }, + "chore": { + "changelog": { + "hidden": true, + "title": "🏡 Chore", + }, + "semverBump": "none", + }, + "ci": { + "changelog": { + "hidden": true, + "title": "🤖 CI", + }, + "semverBump": "none", + }, + "docs": { + "changelog": { + "hidden": true, + "title": "📖 Documentation", + }, + "semverBump": "none", + }, + "examples": { + "changelog": { + "hidden": true, + "title": "🏀 Examples", + }, + "semverBump": "none", + }, + "feat": { + "changelog": { + "hidden": false, + "title": "🚀 Features", + }, + "semverBump": "minor", + }, + "fix": { + "changelog": { + "hidden": false, + "title": "🩹 Fixes", + }, + "semverBump": "patch", + }, + "perf": { + "changelog": { + "hidden": false, + "title": "🔥 Performance", + }, + "semverBump": "none", + }, + "refactor": { + "changelog": { + "hidden": true, + "title": "💅 Refactors", + }, + "semverBump": "none", + }, + "revert": { + "changelog": { + "hidden": true, + "title": "⏪ Revert", + }, + "semverBump": "none", + }, + "style": { + "changelog": { + "hidden": true, + "title": "🎨 Styles", + }, + "semverBump": "none", + }, + "test": { + "changelog": { + "hidden": true, + "title": "✅ Tests", + }, + "semverBump": "none", + }, + "types": { + "changelog": { + "hidden": true, + "title": "🌊 Types", + }, + "semverBump": "none", + }, + }, + }, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "groups": { + "group-1": { + "changelog": { + "createRelease": { + "apiBaseUrl": "https://api.github.com", + "hostname": "github.com", + "provider": "github", + }, + "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", + "file": "./{projectRoot}/custom-path.md", + "renderOptions": { + "authors": false, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": false, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, + "projects": [ + "lib-a", + ], + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "groupPreVersionCommand": "", + }, + "versionPlans": false, + }, + "group-2": { + "changelog": false, + "projects": [ + "lib-b", + ], + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "groupPreVersionCommand": "", + }, + "versionPlans": false, + }, + "group-3": { + "changelog": { + "createRelease": false, + "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", + "file": "./{projectRoot}/a-different-custom-path-at-the-group.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": true, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, + "projects": [ + "nx", + ], + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "groupPreVersionCommand": "", + }, + "versionPlans": false, + }, + }, + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": true, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "preVersionCommand": "", + }, + "versionPlans": false, + }, + } + `); + }); + + it('should respect any adjustments to default changelog config at the top level and group level in the final config, CASE 2', async () => { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { + groups: { + foo: { + projects: 'lib-a', + releaseTagPattern: '{projectName}-{version}', + }, + }, + changelog: { + workspaceChangelog: { + createRelease: 'github', + }, + // enabling project changelogs at the workspace level should cause each group to have project changelogs enabled + projectChangelogs: { + createRelease: 'github', + }, + }, + }); + + expect(res).toMatchInlineSnapshot(` + { + "error": null, + "nxReleaseConfig": { + "changelog": { + "automaticFromRef": false, + "git": { + "commit": true, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": true, + "tagArgs": "", + "tagMessage": "", + }, + "projectChangelogs": { + "createRelease": { + "apiBaseUrl": "https://api.github.com", + "hostname": "github.com", + "provider": "github", + }, + "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", + "file": "{projectRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": true, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, + "workspaceChangelog": { + "createRelease": { + "apiBaseUrl": "https://api.github.com", + "hostname": "github.com", + "provider": "github", + }, + "entryWhenNoChanges": "This was a version bump only, there were no code changes.", + "file": "{workspaceRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": true, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, + }, + "conventionalCommits": { + "types": { + "build": { + "changelog": { + "hidden": true, + "title": "📦 Build", + }, + "semverBump": "none", + }, + "chore": { + "changelog": { + "hidden": true, + "title": "🏡 Chore", + }, + "semverBump": "none", + }, + "ci": { + "changelog": { + "hidden": true, + "title": "🤖 CI", + }, + "semverBump": "none", + }, + "docs": { + "changelog": { + "hidden": true, + "title": "📖 Documentation", + }, + "semverBump": "none", + }, + "examples": { + "changelog": { + "hidden": true, + "title": "🏀 Examples", + }, + "semverBump": "none", + }, + "feat": { + "changelog": { + "hidden": false, + "title": "🚀 Features", + }, + "semverBump": "minor", + }, + "fix": { + "changelog": { + "hidden": false, + "title": "🩹 Fixes", + }, + "semverBump": "patch", + }, + "perf": { + "changelog": { + "hidden": false, + "title": "🔥 Performance", + }, + "semverBump": "none", + }, + "refactor": { + "changelog": { + "hidden": true, + "title": "💅 Refactors", + }, + "semverBump": "none", + }, + "revert": { + "changelog": { + "hidden": true, + "title": "⏪ Revert", + }, + "semverBump": "none", + }, + "style": { + "changelog": { + "hidden": true, + "title": "🎨 Styles", + }, + "semverBump": "none", + }, + "test": { + "changelog": { + "hidden": true, + "title": "✅ Tests", + }, + "semverBump": "none", + }, + "types": { + "changelog": { + "hidden": true, + "title": "🌊 Types", + }, + "semverBump": "none", + }, + }, + }, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "groups": { + "foo": { + "changelog": { + "createRelease": { + "apiBaseUrl": "https://api.github.com", + "hostname": "github.com", + "provider": "github", + }, + "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", + "file": "{projectRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": true, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, + "projects": [ + "lib-a", + ], + "projectsRelationship": "fixed", + "releaseTagPattern": "{projectName}-{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "groupPreVersionCommand": "", + }, + "versionPlans": false, + }, + }, + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": true, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "preVersionCommand": "", + }, + "versionPlans": false, + }, + } + `); + }); + + it('should return an error if no projects can be resolved for a group', async () => { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { + groups: { + 'group-1': { + projects: ['lib-does-not-exist'], + }, + }, + }); + expect(res).toMatchInlineSnapshot(` + { + "error": { + "code": "RELEASE_GROUP_MATCHES_NO_PROJECTS", + "data": { + "releaseGroupName": "group-1", + }, + }, + "nxReleaseConfig": null, + } + `); + }); + + it('should allow configuring a github-enterprise-server hostname and set a default apiBaseUrl for project changelogs', async () => { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { + groups: { + foo: { + projects: 'lib-a', + changelog: { + createRelease: { + provider: 'github-enterprise-server', + hostname: 'custom-github-enterprise-server.com', + }, + }, + }, + }, + }); + expect(res).toMatchInlineSnapshot(` + { + "error": null, + "nxReleaseConfig": { + "changelog": { + "automaticFromRef": false, + "git": { + "commit": true, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": true, + "tagArgs": "", + "tagMessage": "", + }, + "projectChangelogs": false, + "workspaceChangelog": { + "createRelease": false, + "entryWhenNoChanges": "This was a version bump only, there were no code changes.", + "file": "{workspaceRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": true, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, + }, + "conventionalCommits": { + "types": { + "build": { + "changelog": { + "hidden": true, + "title": "📦 Build", + }, + "semverBump": "none", + }, + "chore": { + "changelog": { + "hidden": true, + "title": "🏡 Chore", + }, + "semverBump": "none", + }, + "ci": { + "changelog": { + "hidden": true, + "title": "🤖 CI", + }, + "semverBump": "none", + }, + "docs": { + "changelog": { + "hidden": true, + "title": "📖 Documentation", + }, + "semverBump": "none", + }, + "examples": { + "changelog": { + "hidden": true, + "title": "🏀 Examples", + }, + "semverBump": "none", + }, + "feat": { + "changelog": { + "hidden": false, + "title": "🚀 Features", + }, + "semverBump": "minor", + }, + "fix": { + "changelog": { + "hidden": false, + "title": "🩹 Fixes", + }, + "semverBump": "patch", + }, + "perf": { + "changelog": { + "hidden": false, + "title": "🔥 Performance", + }, + "semverBump": "none", + }, + "refactor": { + "changelog": { + "hidden": true, + "title": "💅 Refactors", + }, + "semverBump": "none", + }, + "revert": { + "changelog": { + "hidden": true, + "title": "⏪ Revert", + }, + "semverBump": "none", + }, + "style": { + "changelog": { + "hidden": true, + "title": "🎨 Styles", + }, + "semverBump": "none", + }, + "test": { + "changelog": { + "hidden": true, + "title": "✅ Tests", + }, + "semverBump": "none", + }, + "types": { + "changelog": { + "hidden": true, + "title": "🌊 Types", + }, + "semverBump": "none", + }, + }, + }, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "groups": { + "foo": { + "changelog": { + "createRelease": { + "apiBaseUrl": "https://custom-github-enterprise-server.com/api/v3", + "hostname": "custom-github-enterprise-server.com", + "provider": "github-enterprise-server", + }, + "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", + "file": "{projectRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": true, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, + "projects": [ + "lib-a", + ], + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + }, + "versionPlans": false, + }, + }, + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": true, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "preVersionCommand": "", + }, + "versionPlans": false, + }, + } + `); + }); + + it('should allow configuring a github-enterprise-server hostname AND a custom apiBaseUrl for project changelogs', async () => { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { + groups: { + foo: { + projects: 'lib-a', + changelog: { + createRelease: { + provider: 'github-enterprise-server', + hostname: 'custom-github-enterprise-server.com', + apiBaseUrl: + 'https://custom-github-enterprise-server.com/api/v99', + }, + }, + }, + }, + }); + expect(res).toMatchInlineSnapshot(` + { + "error": null, + "nxReleaseConfig": { + "changelog": { + "automaticFromRef": false, + "git": { + "commit": true, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": true, + "tagArgs": "", + "tagMessage": "", + }, + "projectChangelogs": false, + "workspaceChangelog": { + "createRelease": false, + "entryWhenNoChanges": "This was a version bump only, there were no code changes.", + "file": "{workspaceRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": true, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, + }, + "conventionalCommits": { + "types": { + "build": { + "changelog": { + "hidden": true, + "title": "📦 Build", + }, + "semverBump": "none", + }, + "chore": { + "changelog": { + "hidden": true, + "title": "🏡 Chore", + }, + "semverBump": "none", + }, + "ci": { + "changelog": { + "hidden": true, + "title": "🤖 CI", + }, + "semverBump": "none", + }, + "docs": { + "changelog": { + "hidden": true, + "title": "📖 Documentation", + }, + "semverBump": "none", + }, + "examples": { + "changelog": { + "hidden": true, + "title": "🏀 Examples", + }, + "semverBump": "none", + }, + "feat": { + "changelog": { + "hidden": false, + "title": "🚀 Features", + }, + "semverBump": "minor", + }, + "fix": { + "changelog": { + "hidden": false, + "title": "🩹 Fixes", + }, + "semverBump": "patch", + }, + "perf": { + "changelog": { + "hidden": false, + "title": "🔥 Performance", + }, + "semverBump": "none", + }, + "refactor": { + "changelog": { + "hidden": true, + "title": "💅 Refactors", + }, + "semverBump": "none", + }, + "revert": { + "changelog": { + "hidden": true, + "title": "⏪ Revert", + }, + "semverBump": "none", + }, + "style": { + "changelog": { + "hidden": true, + "title": "🎨 Styles", + }, + "semverBump": "none", + }, + "test": { + "changelog": { + "hidden": true, + "title": "✅ Tests", + }, + "semverBump": "none", + }, + "types": { + "changelog": { + "hidden": true, + "title": "🌊 Types", + }, + "semverBump": "none", + }, + }, + }, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "groups": { + "foo": { + "changelog": { + "createRelease": { + "apiBaseUrl": "https://custom-github-enterprise-server.com/api/v99", + "hostname": "custom-github-enterprise-server.com", + "provider": "github-enterprise-server", + }, + "entryWhenNoChanges": "This was a version bump only for {projectName} to align it with other projects, there were no code changes.", + "file": "{projectRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "mapAuthorsToGitHubUsernames": true, + "versionTitleDate": true, + }, + "renderer": "/release/changelog-renderer", + }, + "projects": [ + "lib-a", + ], + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + }, + "versionPlans": false, + }, + }, + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": true, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "preVersionCommand": "", + }, + "versionPlans": false, + }, + } + `); + }); + + it('should return an error if an invalid provider, hostname or apiBaseUrl is specified for createRelease for project changelogs', async () => { + const res1 = await createNxReleaseConfig(projectGraph, projectFileMap, { + groups: { + foo: { + projects: 'lib-a', + changelog: { + createRelease: { + provider: 'something-invalid', + } as any, + }, + }, + }, + }); + expect(res1.error).toMatchInlineSnapshot(` + { + "code": "INVALID_CHANGELOG_CREATE_RELEASE_PROVIDER", + "data": { + "provider": "something-invalid", + "supportedProviders": [ + "github-enterprise-server", + ], + }, + } + `); + + const res2 = await createNxReleaseConfig(projectGraph, projectFileMap, { + groups: { + foo: { + projects: 'lib-a', + changelog: { + createRelease: { + provider: 'github-enterprise-server', + hostname: 'not_a_hostname', + }, + }, + }, + }, + }); + expect(res2.error).toMatchInlineSnapshot(` + { + "code": "INVALID_CHANGELOG_CREATE_RELEASE_HOSTNAME", + "data": { + "hostname": "not_a_hostname", + }, + } + `); + + const res3 = await createNxReleaseConfig(projectGraph, projectFileMap, { + groups: { + foo: { + projects: 'lib-a', + changelog: { + createRelease: { + provider: 'github-enterprise-server', + hostname: 'example.com', + apiBaseUrl: 'not_a_url', + }, + }, + }, + }, + }); + expect(res3.error).toMatchInlineSnapshot(` + { + "code": "INVALID_CHANGELOG_CREATE_RELEASE_API_BASE_URL", + "data": { + "apiBaseUrl": "not_a_url", + }, } `); }); diff --git a/packages/nx/src/command-line/release/config/config.ts b/packages/nx/src/command-line/release/config/config.ts index 076a77d3300838..8bf907ddd31b4c 100644 --- a/packages/nx/src/command-line/release/config/config.ts +++ b/packages/nx/src/command-line/release/config/config.ts @@ -12,7 +12,11 @@ * and easy to consume config object for all the `nx release` command implementations. */ import { join, relative } from 'node:path'; -import { NxJsonConfiguration } from '../../../config/nx-json'; +import { URL } from 'node:url'; +import { + NxJsonConfiguration, + NxReleaseChangelogConfiguration, +} from '../../../config/nx-json'; import { ProjectFileMap, ProjectGraph } from '../../../config/project-graph'; import { readJsonFile } from '../../../utils/fileutils'; import { findMatchingProjects } from '../../../utils/find-matching-projects'; @@ -41,15 +45,6 @@ type RemoveTrueFromProperties = { type RemoveTrueFromPropertiesOnEach = { [U in keyof T]: RemoveTrueFromProperties; }; - -type RemoveFalseFromType = T extends false ? never : T; -type RemoveFalseFromProperties = { - [P in keyof T]: P extends K ? RemoveFalseFromType : T[P]; -}; -type RemoveFalseFromPropertiesOnEach = { - [U in keyof T]: RemoveFalseFromProperties; -}; - type RemoveBooleanFromType = T extends boolean ? never : T; type RemoveBooleanFromProperties = { [P in keyof T]: P extends K ? RemoveBooleanFromType : T[P]; @@ -111,7 +106,11 @@ export interface CreateNxReleaseConfigError { | 'RELEASE_GROUP_RELEASE_TAG_PATTERN_VERSION_PLACEHOLDER_MISSING_OR_EXCESSIVE' | 'PROJECT_MATCHES_MULTIPLE_GROUPS' | 'CONVENTIONAL_COMMITS_SHORTHAND_MIXED_WITH_OVERLAPPING_GENERATOR_OPTIONS' - | 'GLOBAL_GIT_CONFIG_MIXED_WITH_GRANULAR_GIT_CONFIG'; + | 'GLOBAL_GIT_CONFIG_MIXED_WITH_GRANULAR_GIT_CONFIG' + | 'CANNOT_RESOLVE_CHANGELOG_RENDERER' + | 'INVALID_CHANGELOG_CREATE_RELEASE_PROVIDER' + | 'INVALID_CHANGELOG_CREATE_RELEASE_HOSTNAME' + | 'INVALID_CHANGELOG_CREATE_RELEASE_API_BASE_URL'; data: Record; } @@ -566,7 +565,16 @@ export async function createNxReleaseConfig( releaseGroups[releaseGroupName] = finalReleaseGroup; } - ensureChangelogRenderersAreResolvable(releaseGroups, rootChangelogConfig); + const configError = validateChangelogConfig( + releaseGroups, + rootChangelogConfig + ); + if (configError) { + return { + error: configError, + nxReleaseConfig: null, + }; + } return { error: null, @@ -766,6 +774,52 @@ export async function handleNxReleaseConfigError( }); } break; + case 'CANNOT_RESOLVE_CHANGELOG_RENDERER': { + const nxJsonMessage = await resolveNxJsonConfigErrorMessage(['release']); + output.error({ + title: `There was an error when resolving the configured changelog renderer at path: ${error.data.workspaceRelativePath}`, + bodyLines: [nxJsonMessage], + }); + } + case 'INVALID_CHANGELOG_CREATE_RELEASE_PROVIDER': + { + const nxJsonMessage = await resolveNxJsonConfigErrorMessage([ + 'release', + ]); + output.error({ + title: `Your "changelog.createRelease" config specifies an unsupported provider "${ + error.data.provider + }". The supported providers are ${( + error.data.supportedProviders as string[] + ) + .map((p) => `"${p}"`) + .join(', ')}`, + bodyLines: [nxJsonMessage], + }); + } + break; + case 'INVALID_CHANGELOG_CREATE_RELEASE_HOSTNAME': + { + const nxJsonMessage = await resolveNxJsonConfigErrorMessage([ + 'release', + ]); + output.error({ + title: `Your "changelog.createRelease" config specifies an invalid hostname "${error.data.hostname}". Please ensure you provide a valid hostname value, such as "example.com"`, + bodyLines: [nxJsonMessage], + }); + } + break; + case 'INVALID_CHANGELOG_CREATE_RELEASE_API_BASE_URL': + { + const nxJsonMessage = await resolveNxJsonConfigErrorMessage([ + 'release', + ]); + output.error({ + title: `Your "changelog.createRelease" config specifies an invalid apiBaseUrl "${error.data.apiBaseUrl}". Please ensure you provide a valid URL value, such as "https://example.com"`, + bodyLines: [nxJsonMessage], + }); + } + break; default: throw new Error(`Unhandled error code: ${error.code}`); } @@ -950,10 +1004,16 @@ function isProjectPublic( } } -function ensureChangelogRenderersAreResolvable( +/** + * We need to ensure that changelog renderers are resolvable up front so that we do not end up erroring after performing + * actions later, and we also make sure that any configured createRelease options are valid. + * + * For the createRelease config, we also set a default apiBaseUrl if applicable. + */ +function validateChangelogConfig( releaseGroups: NxReleaseConfig['groups'], rootChangelogConfig: NxReleaseConfig['changelog'] -) { +): CreateNxReleaseConfigError | null { /** * If any form of changelog config is enabled, ensure that any provided changelog renderers are resolvable * up front so that we do not end up erroring only after the versioning step has been completed. @@ -962,42 +1022,148 @@ function ensureChangelogRenderersAreResolvable( if ( rootChangelogConfig.workspaceChangelog && - typeof rootChangelogConfig.workspaceChangelog !== 'boolean' && - rootChangelogConfig.workspaceChangelog.renderer?.length + typeof rootChangelogConfig.workspaceChangelog !== 'boolean' ) { - uniqueRendererPaths.add(rootChangelogConfig.workspaceChangelog.renderer); + if (rootChangelogConfig.workspaceChangelog.renderer?.length) { + uniqueRendererPaths.add(rootChangelogConfig.workspaceChangelog.renderer); + } + const createReleaseError = validateCreateReleaseConfig( + rootChangelogConfig.workspaceChangelog + ); + if (createReleaseError) { + return createReleaseError; + } } if ( rootChangelogConfig.projectChangelogs && - typeof rootChangelogConfig.projectChangelogs !== 'boolean' && - rootChangelogConfig.projectChangelogs.renderer?.length + typeof rootChangelogConfig.projectChangelogs !== 'boolean' ) { - uniqueRendererPaths.add(rootChangelogConfig.projectChangelogs.renderer); + if (rootChangelogConfig.projectChangelogs.renderer?.length) { + uniqueRendererPaths.add(rootChangelogConfig.projectChangelogs.renderer); + } + const createReleaseError = validateCreateReleaseConfig( + rootChangelogConfig.projectChangelogs + ); + if (createReleaseError) { + return createReleaseError; + } } for (const group of Object.values(releaseGroups)) { - if ( - group.changelog && - typeof group.changelog !== 'boolean' && - group.changelog.renderer?.length - ) { - uniqueRendererPaths.add(group.changelog.renderer); + if (group.changelog && typeof group.changelog !== 'boolean') { + if (group.changelog.renderer?.length) { + uniqueRendererPaths.add(group.changelog.renderer); + } + const createReleaseError = validateCreateReleaseConfig(group.changelog); + if (createReleaseError) { + return createReleaseError; + } } } if (!uniqueRendererPaths.size) { - return; + return null; } for (const rendererPath of uniqueRendererPaths) { try { resolveChangelogRenderer(rendererPath); - } catch (e) { - const workspaceRelativePath = relative(workspaceRoot, rendererPath); - output.error({ - title: `There was an error when resolving the configured changelog renderer at path: ${workspaceRelativePath}`, - }); - throw e; + } catch { + return { + code: 'CANNOT_RESOLVE_CHANGELOG_RENDERER', + data: { + workspaceRelativePath: relative(workspaceRoot, rendererPath), + }, + }; } } + + return null; +} + +const supportedCreateReleaseProviders = [ + { + name: 'github-enterprise-server', + defaultApiBaseUrl: 'https://__hostname__/api/v3', + }, +]; + +// User opts into the default by specifying the string value 'github' +export const defaultCreateReleaseProvider = { + provider: 'github', + hostname: 'github.com', + apiBaseUrl: 'https://api.github.com', +} as any; + +function validateCreateReleaseConfig( + changelogConfig: NxReleaseChangelogConfiguration +): CreateNxReleaseConfigError | null { + const createRelease = changelogConfig.createRelease; + // Disabled: valid + if (!createRelease) { + return null; + } + // GitHub shorthand, expand to full object form, mark as valid + if (createRelease === 'github') { + changelogConfig.createRelease = defaultCreateReleaseProvider; + return null; + } + // Object config, ensure that properties are valid + const supportedProvider = supportedCreateReleaseProviders.find( + (p) => p.name === createRelease.provider + ); + if (!supportedProvider) { + return { + code: 'INVALID_CHANGELOG_CREATE_RELEASE_PROVIDER', + data: { + provider: createRelease.provider, + supportedProviders: supportedCreateReleaseProviders.map((p) => p.name), + }, + }; + } + if (!isValidHostname(createRelease.hostname)) { + return { + code: 'INVALID_CHANGELOG_CREATE_RELEASE_HOSTNAME', + data: { + hostname: createRelease.hostname, + }, + }; + } + // user provided a custom apiBaseUrl, ensure it is valid (accounting for empty string case) + if ( + createRelease.apiBaseUrl || + typeof createRelease.apiBaseUrl === 'string' + ) { + if (!isValidUrl(createRelease.apiBaseUrl)) { + return { + code: 'INVALID_CHANGELOG_CREATE_RELEASE_API_BASE_URL', + data: { + apiBaseUrl: createRelease.apiBaseUrl, + }, + }; + } + } else { + // Set default apiBaseUrl when not provided by the user + createRelease.apiBaseUrl = supportedProvider.defaultApiBaseUrl.replace( + '__hostname__', + createRelease.hostname + ); + } + return null; +} + +function isValidHostname(hostname) { + // Regular expression to match a valid hostname + const hostnameRegex = + /^(?!:\/\/)(?=.{1,255}$)(?!.*\.$)(?!.*?\.\.)(?!.*?-$)(?!^-)([a-zA-Z0-9-]{1,63}\.?)+[a-zA-Z]{2,}$/; + return hostnameRegex.test(hostname); +} + +function isValidUrl(str: string): boolean { + try { + new URL(str); + return true; + } catch { + return false; + } } diff --git a/packages/nx/src/command-line/release/release.ts b/packages/nx/src/command-line/release/release.ts index b7428340d451f6..7980aa296dfc00 100644 --- a/packages/nx/src/command-line/release/release.ts +++ b/packages/nx/src/command-line/release/release.ts @@ -252,6 +252,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { latestCommit = await getCommitHash('HEAD'); await createOrUpdateGithubRelease( + nxReleaseConfig.changelog.workspaceChangelog + ? nxReleaseConfig.changelog.workspaceChangelog.createRelease + : false, changelogResult.workspaceChangelog.releaseVersion, changelogResult.workspaceChangelog.contents, latestCommit, @@ -297,6 +300,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { } await createOrUpdateGithubRelease( + releaseGroup.changelog + ? releaseGroup.changelog.createRelease + : false, changelog.releaseVersion, changelog.contents, latestCommit, diff --git a/packages/nx/src/command-line/release/utils/github.ts b/packages/nx/src/command-line/release/utils/github.ts index a166e8e6776e7b..284a1ef57b919b 100644 --- a/packages/nx/src/command-line/release/utils/github.ts +++ b/packages/nx/src/command-line/release/utils/github.ts @@ -8,8 +8,10 @@ import { prompt } from 'enquirer'; import { execSync } from 'node:child_process'; import { existsSync, promises as fsp } from 'node:fs'; import { homedir } from 'node:os'; +import { NxReleaseChangelogConfiguration } from '../../../config/nx-json'; import { output } from '../../../utils/output'; import { joinPathFragments } from '../../../utils/path'; +import { defaultCreateReleaseProvider } from '../config/config'; import { Reference } from './git'; import { printDiff } from './print-changes'; import { ReleaseVersion, noDiffInChangelogMessage } from './shared'; @@ -20,12 +22,14 @@ const axios = _axios as any as (typeof _axios)['default']; export type RepoSlug = `${string}/${string}`; -export interface GithubRequestConfig { +interface GithubRequestConfig { repo: string; + hostname: string; + apiBaseUrl: string; token: string | null; } -export interface GithubRelease { +interface GithubRelease { id?: string; tag_name: string; target_commitish?: string; @@ -35,19 +39,46 @@ export interface GithubRelease { prerelease?: boolean; } -export function getGitHubRepoSlug(remoteName = 'origin'): RepoSlug { +export interface GithubRepoData { + hostname: string; + slug: RepoSlug; + apiBaseUrl: string; +} + +export function getGitHubRepoData( + remoteName = 'origin', + createReleaseConfig: NxReleaseChangelogConfiguration['createRelease'] +): GithubRepoData | null { try { const remoteUrl = execSync(`git remote get-url ${remoteName}`, { encoding: 'utf8', stdio: 'pipe', }).trim(); + // Use the default provider (github.com) if custom one is not specified or releases are disabled + let hostname = defaultCreateReleaseProvider.hostname; + let apiBaseUrl = defaultCreateReleaseProvider.apiBaseUrl; + if ( + createReleaseConfig !== false && + typeof createReleaseConfig !== 'string' + ) { + hostname = createReleaseConfig.hostname; + apiBaseUrl = createReleaseConfig.apiBaseUrl; + } + // Extract the 'user/repo' part from the URL - const regex = /github\.com[/:]([\w-]+\/[\w-]+)/; + const escapedHostname = hostname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regexString = `${escapedHostname}[/:]([\\w.-]+/[\\w.-]+)(\\.git)?`; + const regex = new RegExp(regexString); const match = remoteUrl.match(regex); if (match && match[1]) { - return match[1] as RepoSlug; + return { + hostname, + apiBaseUrl, + // Ensure any trailing .git is stripped + slug: match[1].replace(/\.git$/, '') as RepoSlug, + }; } else { throw new Error( `Could not extract "user/repo" data from the resolved remote URL: ${remoteUrl}` @@ -59,13 +90,14 @@ export function getGitHubRepoSlug(remoteName = 'origin'): RepoSlug { } export async function createOrUpdateGithubRelease( + createReleaseConfig: NxReleaseChangelogConfiguration['createRelease'], releaseVersion: ReleaseVersion, changelogContents: string, latestCommit: string, { dryRun }: { dryRun: boolean } ): Promise { - const githubRepoSlug = getGitHubRepoSlug(); - if (!githubRepoSlug) { + const githubRepoData = getGitHubRepoData(undefined, createReleaseConfig); + if (!githubRepoData) { output.error({ title: `Unable to create a GitHub release because the GitHub repo slug could not be determined.`, bodyLines: [ @@ -75,9 +107,11 @@ export async function createOrUpdateGithubRelease( process.exit(1); } - const token = await resolveGithubToken(); + const token = await resolveGithubToken(githubRepoData.hostname); const githubRequestConfig: GithubRequestConfig = { - repo: githubRepoSlug, + repo: githubRepoData.slug, + hostname: githubRepoData.hostname, + apiBaseUrl: githubRepoData.apiBaseUrl, token, }; @@ -106,7 +140,7 @@ export async function createOrUpdateGithubRelease( } } - const logTitle = `https://github.com/${githubRepoSlug}/releases/tag/${releaseVersion.gitTag}`; + const logTitle = `https://${githubRepoData.hostname}/${githubRepoData.slug}/releases/tag/${releaseVersion.gitTag}`; if (existingGithubReleaseForVersion) { console.error( `${chalk.white('UPDATE')} ${logTitle}${ @@ -304,7 +338,7 @@ async function syncGithubRelease( } } -export async function resolveGithubToken(): Promise { +async function resolveGithubToken(hostname: string): Promise { // Try and resolve from the environment const tokenFromEnv = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; if (tokenFromEnv) { @@ -320,15 +354,15 @@ export async function resolveGithubToken(): Promise { const yamlContents = await fsp.readFile(ghCLIPath, 'utf8'); const { load } = require('@zkochan/js-yaml'); const ghCLIConfig = load(yamlContents); - if (ghCLIConfig['github.com']) { + if (ghCLIConfig[hostname]) { // Web based session (the token is already embedded in the config) - if (ghCLIConfig['github.com'].oauth_token) { - return ghCLIConfig['github.com'].oauth_token; + if (ghCLIConfig[hostname].oauth_token) { + return ghCLIConfig[hostname].oauth_token; } // SSH based session (we need to dynamically resolve a token using the CLI) if ( - ghCLIConfig['github.com'].user && - ghCLIConfig['github.com'].git_protocol === 'ssh' + ghCLIConfig[hostname].user && + ghCLIConfig[hostname].git_protocol === 'ssh' ) { return execSync(`gh auth token`, { encoding: 'utf8', @@ -337,6 +371,11 @@ export async function resolveGithubToken(): Promise { } } } + if (hostname !== 'github.com') { + console.log( + `Warning: It was not possible to automatically resolve a GitHub token from your environment for hostname ${hostname}. If you set the GITHUB_TOKEN or GH_TOKEN environment variable, that will be used for GitHub API requests.` + ); + } return null; } @@ -359,7 +398,7 @@ async function makeGithubRequest( return ( await axios(url, { ...opts, - baseURL: 'https://api.github.com', + baseURL: config.apiBaseUrl, headers: { ...(opts.headers as any), Authorization: config.token ? `Bearer ${config.token}` : undefined, @@ -395,11 +434,18 @@ async function updateGithubRelease( function githubNewReleaseURL( config: GithubRequestConfig, - release: { version: string; body: string } + release: GithubReleaseOptions ) { - return `https://github.com/${config.repo}/releases/new?tag=${ + // Parameters taken from https://github.com/isaacs/github/issues/1410#issuecomment-442240267 + let url = `https://${config.hostname}/${config.repo}/releases/new?tag=${ release.version - }&title=${release.version}&body=${encodeURIComponent(release.body)}`; + }&title=${release.version}&body=${encodeURIComponent(release.body)}&target=${ + release.commit + }`; + if (release.prerelease) { + url += '&prerelease=true'; + } + return url; } type RepoProvider = 'github'; @@ -411,27 +457,30 @@ const providerToRefSpec: Record< github: { 'pull-request': 'pull', hash: 'commit', issue: 'issues' }, }; -function formatReference(ref: Reference, repoSlug: `${string}/${string}`) { +function formatReference(ref: Reference, repoData: GithubRepoData) { const refSpec = providerToRefSpec['github']; - return `[${ref.value}](https://github.com/${repoSlug}/${ + return `[${ref.value}](https://${repoData.hostname}/${repoData.slug}/${ refSpec[ref.type] }/${ref.value.replace(/^#/, '')})`; } -export function formatReferences(references: Reference[], repoSlug: RepoSlug) { +export function formatReferences( + references: Reference[], + repoData: GithubRepoData +) { const pr = references.filter((ref) => ref.type === 'pull-request'); const issue = references.filter((ref) => ref.type === 'issue'); if (pr.length > 0 || issue.length > 0) { return ( ' (' + [...pr, ...issue] - .map((ref) => formatReference(ref, repoSlug)) + .map((ref) => formatReference(ref, repoData)) .join(', ') + ')' ); } if (references.length > 0) { - return ' (' + formatReference(references[0], repoSlug) + ')'; + return ' (' + formatReference(references[0], repoData) + ')'; } return ''; } diff --git a/packages/nx/src/config/nx-json.ts b/packages/nx/src/config/nx-json.ts index 16c5487f8b6459..cac2bb6152978c 100644 --- a/packages/nx/src/config/nx-json.ts +++ b/packages/nx/src/config/nx-json.ts @@ -79,7 +79,17 @@ export interface NxReleaseChangelogConfiguration { * NOTE: if createRelease is set on a group of projects, it will cause the default releaseTagPattern of * "{projectName}@{version}" to be used for those projects, even when versioning everything together. */ - createRelease?: 'github' | false; + createRelease?: + | false + | 'github' + | { + provider: 'github-enterprise-server'; + hostname: string; + /** + * If not set, this will default to `https://${hostname}/api/v3` + */ + apiBaseUrl?: string; + }; /** * This can either be set to a string value that will be written to the changelog file(s) * at the workspace root and/or within project directories, or set to `false` to specify