fix(release): move github release creation to git tasks (#21510)
This commit is contained in:
parent
f4dd4403f5
commit
b625a79cca
@ -9,7 +9,7 @@ Description of a file change in the Nx virtual file system/
|
|||||||
- [content](../../devkit/documents/FileChange#content): Buffer
|
- [content](../../devkit/documents/FileChange#content): Buffer
|
||||||
- [options](../../devkit/documents/FileChange#options): TreeWriteOptions
|
- [options](../../devkit/documents/FileChange#options): TreeWriteOptions
|
||||||
- [path](../../devkit/documents/FileChange#path): string
|
- [path](../../devkit/documents/FileChange#path): string
|
||||||
- [type](../../devkit/documents/FileChange#type): "DELETE" | "CREATE" | "UPDATE"
|
- [type](../../devkit/documents/FileChange#type): "CREATE" | "DELETE" | "UPDATE"
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
@ -39,6 +39,6 @@ Path relative to the workspace root
|
|||||||
|
|
||||||
### type
|
### type
|
||||||
|
|
||||||
• **type**: `"DELETE"` \| `"CREATE"` \| `"UPDATE"`
|
• **type**: `"CREATE"` \| `"DELETE"` \| `"UPDATE"`
|
||||||
|
|
||||||
Type of change: 'CREATE' | 'DELETE' | 'UPDATE'
|
Type of change: 'CREATE' | 'DELETE' | 'UPDATE'
|
||||||
|
|||||||
@ -105,16 +105,12 @@ import * as yargs from 'yargs';
|
|||||||
verbose: options.verbose,
|
verbose: options.verbose,
|
||||||
});
|
});
|
||||||
|
|
||||||
// The returned number value from releaseChangelog will be non-zero if something went wrong
|
await releaseChangelog({
|
||||||
const changelogStatus = await releaseChangelog({
|
|
||||||
versionData: projectsVersionData,
|
versionData: projectsVersionData,
|
||||||
version: workspaceVersion,
|
version: workspaceVersion,
|
||||||
dryRun: options.dryRun,
|
dryRun: options.dryRun,
|
||||||
verbose: options.verbose,
|
verbose: options.verbose,
|
||||||
});
|
});
|
||||||
if (changelogStatus !== 0) {
|
|
||||||
process.exit(changelogStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The returned number value from releasePublish will be zero if all projects are published successfully, non-zero if not
|
// The returned number value from releasePublish will be zero if all projects are published successfully, non-zero if not
|
||||||
const publishStatus = await releasePublish({
|
const publishStatus = await releasePublish({
|
||||||
|
|||||||
165
e2e/release/src/create-github-release.test.ts
Normal file
165
e2e/release/src/create-github-release.test.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { NxJsonConfiguration } from '@nx/devkit';
|
||||||
|
import {
|
||||||
|
cleanupProject,
|
||||||
|
newProject,
|
||||||
|
runCLI,
|
||||||
|
runCommandAsync,
|
||||||
|
uniq,
|
||||||
|
updateJson,
|
||||||
|
} from '@nx/e2e/utils';
|
||||||
|
|
||||||
|
expect.addSnapshotSerializer({
|
||||||
|
serialize(str: string) {
|
||||||
|
return (
|
||||||
|
str
|
||||||
|
// Remove all output unique to specific projects to ensure deterministic snapshots
|
||||||
|
.replaceAll(/my-pkg-\d+/g, '{project-name}')
|
||||||
|
.replaceAll(
|
||||||
|
/integrity:\s*.*/g,
|
||||||
|
'integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
|
||||||
|
)
|
||||||
|
.replaceAll(/\b[0-9a-f]{40}\b/g, '{SHASUM}')
|
||||||
|
.replaceAll(/\d*B index\.js/g, 'XXB index.js')
|
||||||
|
.replaceAll(/\d*B project\.json/g, 'XXB project.json')
|
||||||
|
.replaceAll(/\d*B package\.json/g, 'XXXB package.json')
|
||||||
|
.replaceAll(/size:\s*\d*\s?B/g, 'size: XXXB')
|
||||||
|
.replaceAll(/\d*\.\d*\s?kB/g, 'XXX.XXX kb')
|
||||||
|
.replaceAll(/[a-fA-F0-9]{7}/g, '{COMMIT_SHA}')
|
||||||
|
.replaceAll(/Test @[\w\d]+/g, 'Test @{COMMIT_AUTHOR}')
|
||||||
|
// Normalize the version title date.
|
||||||
|
.replaceAll(/\(\d{4}-\d{2}-\d{2}\)/g, '(YYYY-MM-DD)')
|
||||||
|
// We trim each line to reduce the chances of snapshot flakiness
|
||||||
|
.split('\n')
|
||||||
|
.map((r) => r.trim())
|
||||||
|
.join('\n')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
test(val: string) {
|
||||||
|
return val != null && typeof val === 'string';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('nx release create github release', () => {
|
||||||
|
let pkg1: string;
|
||||||
|
let pkg2: string;
|
||||||
|
let pkg3: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
newProject({
|
||||||
|
unsetProjectNameAndRootFormat: false,
|
||||||
|
packages: ['@nx/js'],
|
||||||
|
});
|
||||||
|
|
||||||
|
pkg1 = uniq('my-pkg-1');
|
||||||
|
runCLI(`generate @nx/workspace:npm-package ${pkg1}`);
|
||||||
|
|
||||||
|
pkg2 = uniq('my-pkg-2');
|
||||||
|
runCLI(`generate @nx/workspace:npm-package ${pkg2}`);
|
||||||
|
|
||||||
|
pkg3 = uniq('my-pkg-3');
|
||||||
|
runCLI(`generate @nx/workspace:npm-package ${pkg3}`);
|
||||||
|
|
||||||
|
// Update pkg2 to depend on pkg1
|
||||||
|
updateJson(`${pkg2}/package.json`, (json) => {
|
||||||
|
json.dependencies ??= {};
|
||||||
|
json.dependencies[`@proj/${pkg1}`] = '0.0.0';
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Normalize git committer information so it is deterministic in snapshots
|
||||||
|
await runCommandAsync(`git config user.email "test@test.com"`);
|
||||||
|
await runCommandAsync(`git config user.name "Test"`);
|
||||||
|
|
||||||
|
// update my-pkg-1 with a fix commit
|
||||||
|
updateJson(`${pkg1}/package.json`, (json) => ({
|
||||||
|
...json,
|
||||||
|
license: 'MIT',
|
||||||
|
}));
|
||||||
|
await runCommandAsync(`git add ${pkg1}/package.json`);
|
||||||
|
await runCommandAsync(`git commit -m "fix(${pkg1}): fix 1"`);
|
||||||
|
|
||||||
|
// update my-pkg-2 with a breaking change
|
||||||
|
updateJson(`${pkg2}/package.json`, (json) => ({
|
||||||
|
...json,
|
||||||
|
license: 'GNU GPLv3',
|
||||||
|
}));
|
||||||
|
await runCommandAsync(`git add ${pkg2}/package.json`);
|
||||||
|
await runCommandAsync(`git commit -m "feat(${pkg2})!: breaking change 2"`);
|
||||||
|
|
||||||
|
// update my-pkg-3 with a feature commit
|
||||||
|
updateJson(`${pkg3}/package.json`, (json) => ({
|
||||||
|
...json,
|
||||||
|
license: 'GNU GPLv3',
|
||||||
|
}));
|
||||||
|
await runCommandAsync(`git add ${pkg3}/package.json`);
|
||||||
|
await runCommandAsync(`git commit -m "feat(${pkg3}): feat 3"`);
|
||||||
|
|
||||||
|
// We need a valid git origin to exist for the commit references to work (and later the test for createRelease)
|
||||||
|
await runCommandAsync(
|
||||||
|
`git remote add origin https://github.com/nrwl/fake-repo.git`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
afterAll(() => cleanupProject());
|
||||||
|
|
||||||
|
it('should create github release for the first release', async () => {
|
||||||
|
updateJson<NxJsonConfiguration>('nx.json', (nxJson) => {
|
||||||
|
nxJson.release = {
|
||||||
|
changelog: {
|
||||||
|
workspaceChangelog: {
|
||||||
|
createRelease: 'github',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return nxJson;
|
||||||
|
});
|
||||||
|
const result = runCLI('release patch -d --first-release --verbose');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
result.match(new RegExp(`> NX Pushing to git remote`, 'g')).length
|
||||||
|
).toEqual(1);
|
||||||
|
expect(
|
||||||
|
result.match(new RegExp(`> NX Creating GitHub Release`, 'g')).length
|
||||||
|
).toEqual(1);
|
||||||
|
|
||||||
|
// should have two occurrences of each - one for the changelog file, one for the github release
|
||||||
|
expect(result.match(new RegExp(`### 🚀 Features`, 'g')).length).toEqual(2);
|
||||||
|
expect(result.match(new RegExp(`### 🩹 Fixes`, 'g')).length).toEqual(2);
|
||||||
|
expect(
|
||||||
|
result.match(new RegExp(`#### ⚠️ Breaking Changes`, 'g')).length
|
||||||
|
).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create github releases for all independent packages', async () => {
|
||||||
|
updateJson<NxJsonConfiguration>('nx.json', (nxJson) => {
|
||||||
|
nxJson.release = {
|
||||||
|
projectsRelationship: 'independent',
|
||||||
|
version: {
|
||||||
|
conventionalCommits: true,
|
||||||
|
},
|
||||||
|
changelog: {
|
||||||
|
projectChangelogs: {
|
||||||
|
file: false,
|
||||||
|
createRelease: 'github',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return nxJson;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = runCLI('release -d --first-release --verbose');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
result.match(new RegExp(`> NX Pushing to git remote`, 'g')).length
|
||||||
|
).toEqual(1);
|
||||||
|
expect(
|
||||||
|
result.match(new RegExp(`> NX Creating GitHub Release`, 'g')).length
|
||||||
|
).toEqual(3);
|
||||||
|
|
||||||
|
// should have one occurrence of each because files are disabled
|
||||||
|
expect(result.match(new RegExp(`### 🚀 Features`, 'g')).length).toEqual(2);
|
||||||
|
expect(result.match(new RegExp(`### 🩹 Fixes`, 'g')).length).toEqual(1);
|
||||||
|
expect(
|
||||||
|
result.match(new RegExp(`#### ⚠️ Breaking Changes`, 'g')).length
|
||||||
|
).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -726,7 +726,7 @@ ${JSON.stringify(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
> NX Previewing a GitHub release and an entry in {project-name}/CHANGELOG.md for v1000.0.0-next.0
|
> NX Previewing an entry in {project-name}/CHANGELOG.md for v1000.0.0-next.0
|
||||||
|
|
||||||
|
|
||||||
+ ## 1000.0.0-next.0
|
+ ## 1000.0.0-next.0
|
||||||
@ -734,7 +734,7 @@ ${JSON.stringify(
|
|||||||
+ This was a version bump only for {project-name} to align it with other projects, there were no code changes.
|
+ This was a version bump only for {project-name} to align it with other projects, there were no code changes.
|
||||||
|
|
||||||
|
|
||||||
> NX Previewing a GitHub release and an entry in {project-name}/CHANGELOG.md for v1000.0.0-next.0
|
> NX Previewing an entry in {project-name}/CHANGELOG.md for v1000.0.0-next.0
|
||||||
|
|
||||||
|
|
||||||
+ ## 1000.0.0-next.0
|
+ ## 1000.0.0-next.0
|
||||||
@ -742,7 +742,7 @@ ${JSON.stringify(
|
|||||||
+ This was a version bump only for {project-name} to align it with other projects, there were no code changes.
|
+ This was a version bump only for {project-name} to align it with other projects, there were no code changes.
|
||||||
|
|
||||||
|
|
||||||
> NX Previewing a GitHub release and an entry in {project-name}/CHANGELOG.md for v1000.0.0-next.0
|
> NX Previewing an entry in {project-name}/CHANGELOG.md for v1000.0.0-next.0
|
||||||
|
|
||||||
|
|
||||||
+ ## 1000.0.0-next.0
|
+ ## 1000.0.0-next.0
|
||||||
@ -756,6 +756,33 @@ ${JSON.stringify(
|
|||||||
> NX Tagging commit with git
|
> NX Tagging commit with git
|
||||||
|
|
||||||
|
|
||||||
|
> NX Pushing to git remote
|
||||||
|
|
||||||
|
|
||||||
|
> NX Creating GitHub Release
|
||||||
|
|
||||||
|
|
||||||
|
+ ## 1000.0.0-next.0
|
||||||
|
+
|
||||||
|
+ This was a version bump only for {project-name} to align it with other projects, there were no code changes.
|
||||||
|
|
||||||
|
|
||||||
|
> NX Creating GitHub Release
|
||||||
|
|
||||||
|
|
||||||
|
+ ## 1000.0.0-next.0
|
||||||
|
+
|
||||||
|
+ This was a version bump only for {project-name} to align it with other projects, there were no code changes.
|
||||||
|
|
||||||
|
|
||||||
|
> NX Creating GitHub Release
|
||||||
|
|
||||||
|
|
||||||
|
+ ## 1000.0.0-next.0
|
||||||
|
+
|
||||||
|
+ This was a version bump only for {project-name} to align it with other projects, there were no code changes.
|
||||||
|
|
||||||
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// port and process cleanup
|
// port and process cleanup
|
||||||
|
|||||||
@ -3,7 +3,10 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|||||||
import { valid } from 'semver';
|
import { valid } from 'semver';
|
||||||
import { dirSync } from 'tmp';
|
import { dirSync } from 'tmp';
|
||||||
import type { ChangelogRenderer } from '../../../release/changelog-renderer';
|
import type { ChangelogRenderer } from '../../../release/changelog-renderer';
|
||||||
import { readNxJson } from '../../config/nx-json';
|
import {
|
||||||
|
NxReleaseChangelogConfiguration,
|
||||||
|
readNxJson,
|
||||||
|
} from '../../config/nx-json';
|
||||||
import {
|
import {
|
||||||
ProjectGraph,
|
ProjectGraph,
|
||||||
ProjectGraphProjectNode,
|
ProjectGraphProjectNode,
|
||||||
@ -38,17 +41,10 @@ import {
|
|||||||
gitTag,
|
gitTag,
|
||||||
parseCommits,
|
parseCommits,
|
||||||
} from './utils/git';
|
} from './utils/git';
|
||||||
import {
|
import { createOrUpdateGithubRelease, getGitHubRepoSlug } from './utils/github';
|
||||||
GithubRelease,
|
|
||||||
GithubRequestConfig,
|
|
||||||
createOrUpdateGithubRelease,
|
|
||||||
getGitHubRepoSlug,
|
|
||||||
getGithubReleaseByTag,
|
|
||||||
resolveGithubToken,
|
|
||||||
} from './utils/github';
|
|
||||||
import { launchEditor } from './utils/launch-editor';
|
import { launchEditor } from './utils/launch-editor';
|
||||||
import { parseChangelogMarkdown } from './utils/markdown';
|
import { parseChangelogMarkdown } from './utils/markdown';
|
||||||
import { printAndFlushChanges, printDiff } from './utils/print-changes';
|
import { printAndFlushChanges } from './utils/print-changes';
|
||||||
import { resolveNxJsonConfigErrorMessage } from './utils/resolve-nx-json-error-message';
|
import { resolveNxJsonConfigErrorMessage } from './utils/resolve-nx-json-error-message';
|
||||||
import {
|
import {
|
||||||
ReleaseVersion,
|
ReleaseVersion,
|
||||||
@ -57,8 +53,22 @@ import {
|
|||||||
createCommitMessageValues,
|
createCommitMessageValues,
|
||||||
createGitTagValues,
|
createGitTagValues,
|
||||||
handleDuplicateGitTags,
|
handleDuplicateGitTags,
|
||||||
|
noDiffInChangelogMessage,
|
||||||
} from './utils/shared';
|
} from './utils/shared';
|
||||||
|
|
||||||
|
export interface NxReleaseChangelogResult {
|
||||||
|
workspaceChangelog?: {
|
||||||
|
releaseVersion: ReleaseVersion;
|
||||||
|
contents: string;
|
||||||
|
};
|
||||||
|
projectChangelogs?: {
|
||||||
|
[projectName: string]: {
|
||||||
|
releaseVersion: ReleaseVersion;
|
||||||
|
contents: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
type PostGitTask = (latestCommit: string) => Promise<void>;
|
type PostGitTask = (latestCommit: string) => Promise<void>;
|
||||||
|
|
||||||
export const releaseChangelogCLIHandler = (args: ChangelogOptions) =>
|
export const releaseChangelogCLIHandler = (args: ChangelogOptions) =>
|
||||||
@ -71,7 +81,7 @@ export const releaseChangelogCLIHandler = (args: ChangelogOptions) =>
|
|||||||
*/
|
*/
|
||||||
export async function releaseChangelog(
|
export async function releaseChangelog(
|
||||||
args: ChangelogOptions
|
args: ChangelogOptions
|
||||||
): Promise<number> {
|
): Promise<NxReleaseChangelogResult> {
|
||||||
const projectGraph = await createProjectGraphAsync({ exitOnError: true });
|
const projectGraph = await createProjectGraphAsync({ exitOnError: true });
|
||||||
const nxJson = readNxJson();
|
const nxJson = readNxJson();
|
||||||
|
|
||||||
@ -134,7 +144,7 @@ export async function releaseChangelog(
|
|||||||
`To explicitly enable changelog generation, configure "release.changelog.workspaceChangelog" or "release.changelog.projectChangelogs" in nx.json.`,
|
`To explicitly enable changelog generation, configure "release.changelog.workspaceChangelog" or "release.changelog.projectChangelogs" in nx.json.`,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
return 0;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const useAutomaticFromRef =
|
const useAutomaticFromRef =
|
||||||
@ -230,16 +240,51 @@ export async function releaseChangelog(
|
|||||||
toSHA
|
toSHA
|
||||||
);
|
);
|
||||||
|
|
||||||
await generateChangelogForWorkspace(
|
const workspaceChangelog = await generateChangelogForWorkspace(
|
||||||
tree,
|
tree,
|
||||||
args,
|
args,
|
||||||
projectGraph,
|
projectGraph,
|
||||||
nxReleaseConfig,
|
nxReleaseConfig,
|
||||||
workspaceChangelogVersion,
|
workspaceChangelogVersion,
|
||||||
workspaceChangelogCommits,
|
workspaceChangelogCommits
|
||||||
postGitTasks
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
workspaceChangelog &&
|
||||||
|
shouldCreateGitHubRelease(
|
||||||
|
nxReleaseConfig.changelog.workspaceChangelog,
|
||||||
|
args.createRelease
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
let hasPushed = false;
|
||||||
|
|
||||||
|
postGitTasks.push(async (latestCommit) => {
|
||||||
|
if (!hasPushed) {
|
||||||
|
output.logSingleLine(`Pushing to git remote`);
|
||||||
|
|
||||||
|
// Before we can create/update the release we need to ensure the commit exists on the remote
|
||||||
|
await gitPush({
|
||||||
|
gitRemote: args.gitRemote,
|
||||||
|
dryRun: args.dryRun,
|
||||||
|
verbose: args.verbose,
|
||||||
|
});
|
||||||
|
hasPushed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.logSingleLine(`Creating GitHub Release`);
|
||||||
|
|
||||||
|
await createOrUpdateGithubRelease(
|
||||||
|
workspaceChangelog.releaseVersion,
|
||||||
|
workspaceChangelog.contents,
|
||||||
|
latestCommit,
|
||||||
|
{ dryRun: args.dryRun }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const allProjectChangelogs: NxReleaseChangelogResult['projectChangelogs'] =
|
||||||
|
{};
|
||||||
|
|
||||||
for (const releaseGroup of releaseGroups) {
|
for (const releaseGroup of releaseGroups) {
|
||||||
const config = releaseGroup.changelog;
|
const config = releaseGroup.changelog;
|
||||||
// The entire feature is disabled at the release group level, exit early
|
// The entire feature is disabled at the release group level, exit early
|
||||||
@ -292,7 +337,7 @@ export async function releaseChangelog(
|
|||||||
commits = await getCommits(fromRef, toSHA);
|
commits = await getCommits(fromRef, toSHA);
|
||||||
}
|
}
|
||||||
|
|
||||||
await generateChangelogForProjects(
|
const projectChangelogs = await generateChangelogForProjects(
|
||||||
tree,
|
tree,
|
||||||
args,
|
args,
|
||||||
projectGraph,
|
projectGraph,
|
||||||
@ -302,6 +347,43 @@ export async function releaseChangelog(
|
|||||||
releaseGroup,
|
releaseGroup,
|
||||||
[project]
|
[project]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let hasPushed = false;
|
||||||
|
for (const [projectName, projectChangelog] of Object.entries(
|
||||||
|
projectChangelogs
|
||||||
|
)) {
|
||||||
|
if (
|
||||||
|
projectChangelogs &&
|
||||||
|
shouldCreateGitHubRelease(
|
||||||
|
releaseGroup.changelog,
|
||||||
|
args.createRelease
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
postGitTasks.push(async (latestCommit) => {
|
||||||
|
if (!hasPushed) {
|
||||||
|
output.logSingleLine(`Pushing to git remote`);
|
||||||
|
|
||||||
|
// Before we can create/update the release we need to ensure the commit exists on the remote
|
||||||
|
await gitPush({
|
||||||
|
gitRemote: args.gitRemote,
|
||||||
|
dryRun: args.dryRun,
|
||||||
|
verbose: args.verbose,
|
||||||
|
});
|
||||||
|
hasPushed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.logSingleLine(`Creating GitHub Release`);
|
||||||
|
|
||||||
|
await createOrUpdateGithubRelease(
|
||||||
|
projectChangelog.releaseVersion,
|
||||||
|
projectChangelog.contents,
|
||||||
|
latestCommit,
|
||||||
|
{ dryRun: args.dryRun }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
allProjectChangelogs[projectName] = projectChangelog;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const fromRef =
|
const fromRef =
|
||||||
@ -318,7 +400,7 @@ export async function releaseChangelog(
|
|||||||
|
|
||||||
const commits = await getCommits(fromSHA, toSHA);
|
const commits = await getCommits(fromSHA, toSHA);
|
||||||
|
|
||||||
await generateChangelogForProjects(
|
const projectChangelogs = await generateChangelogForProjects(
|
||||||
tree,
|
tree,
|
||||||
args,
|
args,
|
||||||
projectGraph,
|
projectGraph,
|
||||||
@ -328,10 +410,44 @@ export async function releaseChangelog(
|
|||||||
releaseGroup,
|
releaseGroup,
|
||||||
projectNodes
|
projectNodes
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let hasPushed = false;
|
||||||
|
for (const [projectName, projectChangelog] of Object.entries(
|
||||||
|
projectChangelogs
|
||||||
|
)) {
|
||||||
|
if (
|
||||||
|
projectChangelogs &&
|
||||||
|
shouldCreateGitHubRelease(releaseGroup.changelog, args.createRelease)
|
||||||
|
) {
|
||||||
|
postGitTasks.push(async (latestCommit) => {
|
||||||
|
if (!hasPushed) {
|
||||||
|
output.logSingleLine(`Pushing to git remote`);
|
||||||
|
|
||||||
|
// Before we can create/update the release we need to ensure the commit exists on the remote
|
||||||
|
await gitPush({
|
||||||
|
gitRemote: args.gitRemote,
|
||||||
|
dryRun: args.dryRun,
|
||||||
|
verbose: args.verbose,
|
||||||
|
});
|
||||||
|
hasPushed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.logSingleLine(`Creating GitHub Release`);
|
||||||
|
|
||||||
|
await createOrUpdateGithubRelease(
|
||||||
|
projectChangelog.releaseVersion,
|
||||||
|
projectChangelog.contents,
|
||||||
|
latestCommit,
|
||||||
|
{ dryRun: args.dryRun }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
allProjectChangelogs[projectName] = projectChangelog;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await applyChangesAndExit(
|
await applyChangesAndExit(
|
||||||
args,
|
args,
|
||||||
nxReleaseConfig,
|
nxReleaseConfig,
|
||||||
tree,
|
tree,
|
||||||
@ -340,6 +456,11 @@ export async function releaseChangelog(
|
|||||||
commitMessageValues,
|
commitMessageValues,
|
||||||
gitTagValues
|
gitTagValues
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
workspaceChangelog,
|
||||||
|
projectChangelogs: allProjectChangelogs,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveChangelogVersions(
|
function resolveChangelogVersions(
|
||||||
@ -429,7 +550,7 @@ async function applyChangesAndExit(
|
|||||||
`No changes were detected for any changelog files, so no changelog entries will be generated.`,
|
`No changes were detected for any changelog files, so no changelog entries will be generated.`,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
return 0;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new commit for the changes, if configured to do so
|
// Generate a new commit for the changes, if configured to do so
|
||||||
@ -475,7 +596,7 @@ async function applyChangesAndExit(
|
|||||||
await postGitTask(latestCommit);
|
await postGitTask(latestCommit);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveChangelogRenderer(
|
function resolveChangelogRenderer(
|
||||||
@ -504,9 +625,8 @@ async function generateChangelogForWorkspace(
|
|||||||
projectGraph: ProjectGraph,
|
projectGraph: ProjectGraph,
|
||||||
nxReleaseConfig: NxReleaseConfig,
|
nxReleaseConfig: NxReleaseConfig,
|
||||||
workspaceChangelogVersion: (string | null) | undefined,
|
workspaceChangelogVersion: (string | null) | undefined,
|
||||||
commits: GitCommit[],
|
commits: GitCommit[]
|
||||||
postGitTasks: PostGitTask[]
|
): Promise<NxReleaseChangelogResult['workspaceChangelog']> {
|
||||||
) {
|
|
||||||
const config = nxReleaseConfig.changelog.workspaceChangelog;
|
const config = nxReleaseConfig.changelog.workspaceChangelog;
|
||||||
// The entire feature is disabled at the workspace level, exit early
|
// The entire feature is disabled at the workspace level, exit early
|
||||||
if (config === false) {
|
if (config === false) {
|
||||||
@ -572,27 +692,15 @@ async function generateChangelogForWorkspace(
|
|||||||
releaseTagPattern: nxReleaseConfig.releaseTagPattern,
|
releaseTagPattern: nxReleaseConfig.releaseTagPattern,
|
||||||
});
|
});
|
||||||
|
|
||||||
// We are either creating/previewing a changelog file, a GitHub release, or both
|
if (interpolatedTreePath) {
|
||||||
let logTitle = dryRun ? 'Previewing a' : 'Generating a';
|
const prefix = dryRun ? 'Previewing' : 'Generating';
|
||||||
switch (true) {
|
output.log({
|
||||||
case interpolatedTreePath && config.createRelease === 'github':
|
title: `${prefix} an entry in ${interpolatedTreePath} for ${chalk.white(
|
||||||
logTitle += ` GitHub release and an entry in ${interpolatedTreePath} for ${chalk.white(
|
|
||||||
releaseVersion.gitTag
|
releaseVersion.gitTag
|
||||||
)}`;
|
)}`,
|
||||||
break;
|
});
|
||||||
case !!interpolatedTreePath:
|
|
||||||
logTitle += `n entry in ${interpolatedTreePath} for ${chalk.white(
|
|
||||||
releaseVersion.gitTag
|
|
||||||
)}`;
|
|
||||||
break;
|
|
||||||
case config.createRelease === 'github':
|
|
||||||
logTitle += ` GitHub release for ${chalk.white(releaseVersion.gitTag)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output.log({
|
|
||||||
title: logTitle,
|
|
||||||
});
|
|
||||||
|
|
||||||
const githubRepoSlug = getGitHubRepoSlug(gitRemote);
|
const githubRepoSlug = getGitHubRepoSlug(gitRemote);
|
||||||
|
|
||||||
let contents = await changelogRenderer({
|
let contents = await changelogRenderer({
|
||||||
@ -621,15 +729,6 @@ async function generateChangelogForWorkspace(
|
|||||||
contents = readFileSync(changelogPath, 'utf-8');
|
contents = readFileSync(changelogPath, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The exact logic we use for printing the summary/diff to the user is dependent upon whether they are creating
|
|
||||||
* a changelog file, a GitHub release, or both.
|
|
||||||
*/
|
|
||||||
let printSummary = () => {};
|
|
||||||
const noDiffInChangelogMessage = chalk.yellow(
|
|
||||||
`NOTE: There was no diff detected for the changelog entry. Maybe you intended to pass alternative git references via --from and --to?`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (interpolatedTreePath) {
|
if (interpolatedTreePath) {
|
||||||
let rootChangelogContents = tree.exists(interpolatedTreePath)
|
let rootChangelogContents = tree.exists(interpolatedTreePath)
|
||||||
? tree.read(interpolatedTreePath).toString()
|
? tree.read(interpolatedTreePath).toString()
|
||||||
@ -659,104 +758,13 @@ async function generateChangelogForWorkspace(
|
|||||||
|
|
||||||
tree.write(interpolatedTreePath, rootChangelogContents);
|
tree.write(interpolatedTreePath, rootChangelogContents);
|
||||||
|
|
||||||
printSummary = () =>
|
printAndFlushChanges(tree, !!dryRun, 3, false, noDiffInChangelogMessage);
|
||||||
printAndFlushChanges(tree, !!dryRun, 3, false, noDiffInChangelogMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.createRelease === 'github') {
|
return {
|
||||||
if (!githubRepoSlug) {
|
releaseVersion,
|
||||||
output.error({
|
contents,
|
||||||
title: `Unable to create a GitHub release because the GitHub repo slug could not be determined.`,
|
};
|
||||||
bodyLines: [
|
|
||||||
`Please ensure you have a valid GitHub remote configured. You can run \`git remote -v\` to list your current remotes.`,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await resolveGithubToken();
|
|
||||||
const githubRequestConfig: GithubRequestConfig = {
|
|
||||||
repo: githubRepoSlug,
|
|
||||||
token,
|
|
||||||
};
|
|
||||||
|
|
||||||
let existingGithubReleaseForVersion: GithubRelease;
|
|
||||||
try {
|
|
||||||
existingGithubReleaseForVersion = await getGithubReleaseByTag(
|
|
||||||
githubRequestConfig,
|
|
||||||
releaseVersion.gitTag
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.response?.status === 401) {
|
|
||||||
output.error({
|
|
||||||
title: `Unable to resolve data via the GitHub API. You can use any of the following options to resolve this:`,
|
|
||||||
bodyLines: [
|
|
||||||
'- Set the `GITHUB_TOKEN` or `GH_TOKEN` environment variable to a valid GitHub token with `repo` scope',
|
|
||||||
'- Have an active session via the official gh CLI tool (https://cli.github.com) in your current terminal',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (err.response?.status === 404) {
|
|
||||||
// No existing release found, this is fine
|
|
||||||
} else {
|
|
||||||
// Rethrow unknown errors for now
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let existingPrintSummaryFn = printSummary;
|
|
||||||
printSummary = () => {
|
|
||||||
const logTitle = `https://github.com/${githubRepoSlug}/releases/tag/${releaseVersion.gitTag}`;
|
|
||||||
if (existingGithubReleaseForVersion) {
|
|
||||||
console.error(
|
|
||||||
`${chalk.white('UPDATE')} ${logTitle}${
|
|
||||||
dryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
`${chalk.green('CREATE')} ${logTitle}${
|
|
||||||
dryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Only print the diff here if we are not already going to be printing changes from the Tree
|
|
||||||
if (!interpolatedTreePath) {
|
|
||||||
console.log('');
|
|
||||||
printDiff(
|
|
||||||
existingGithubReleaseForVersion
|
|
||||||
? existingGithubReleaseForVersion.body
|
|
||||||
: '',
|
|
||||||
contents,
|
|
||||||
3,
|
|
||||||
noDiffInChangelogMessage
|
|
||||||
);
|
|
||||||
}
|
|
||||||
existingPrintSummaryFn();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only schedule the actual GitHub update when not in dry-run mode
|
|
||||||
if (!dryRun) {
|
|
||||||
postGitTasks.push(async (latestCommit) => {
|
|
||||||
// Before we can create/update the release we need to ensure the commit exists on the remote
|
|
||||||
await gitPush();
|
|
||||||
|
|
||||||
await createOrUpdateGithubRelease(
|
|
||||||
githubRequestConfig,
|
|
||||||
{
|
|
||||||
version: releaseVersion.gitTag,
|
|
||||||
prerelease: releaseVersion.isPrerelease,
|
|
||||||
body: contents,
|
|
||||||
commit: latestCommit,
|
|
||||||
},
|
|
||||||
existingGithubReleaseForVersion
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
printSummary();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateChangelogForProjects(
|
async function generateChangelogForProjects(
|
||||||
@ -768,7 +776,7 @@ async function generateChangelogForProjects(
|
|||||||
postGitTasks: PostGitTask[],
|
postGitTasks: PostGitTask[],
|
||||||
releaseGroup: ReleaseGroupWithName,
|
releaseGroup: ReleaseGroupWithName,
|
||||||
projects: ProjectGraphProjectNode[]
|
projects: ProjectGraphProjectNode[]
|
||||||
) {
|
): Promise<NxReleaseChangelogResult['projectChangelogs']> {
|
||||||
const config = releaseGroup.changelog;
|
const config = releaseGroup.changelog;
|
||||||
// The entire feature is disabled at the release group level, exit early
|
// The entire feature is disabled at the release group level, exit early
|
||||||
if (config === false) {
|
if (config === false) {
|
||||||
@ -783,6 +791,8 @@ async function generateChangelogForProjects(
|
|||||||
|
|
||||||
const changelogRenderer = resolveChangelogRenderer(config.renderer);
|
const changelogRenderer = resolveChangelogRenderer(config.renderer);
|
||||||
|
|
||||||
|
const projectChangelogs: NxReleaseChangelogResult['projectChangelogs'] = {};
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
let interpolatedTreePath = config.file || '';
|
let interpolatedTreePath = config.file || '';
|
||||||
if (interpolatedTreePath) {
|
if (interpolatedTreePath) {
|
||||||
@ -807,27 +817,15 @@ async function generateChangelogForProjects(
|
|||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
// We are either creating/previewing a changelog file, a GitHub release, or both
|
if (interpolatedTreePath) {
|
||||||
let logTitle = dryRun ? 'Previewing a' : 'Generating a';
|
const prefix = dryRun ? 'Previewing' : 'Generating';
|
||||||
switch (true) {
|
output.log({
|
||||||
case interpolatedTreePath && config.createRelease === 'github':
|
title: `${prefix} an entry in ${interpolatedTreePath} for ${chalk.white(
|
||||||
logTitle += ` GitHub release and an entry in ${interpolatedTreePath} for ${chalk.white(
|
|
||||||
releaseVersion.gitTag
|
releaseVersion.gitTag
|
||||||
)}`;
|
)}`,
|
||||||
break;
|
});
|
||||||
case !!interpolatedTreePath:
|
|
||||||
logTitle += `n entry in ${interpolatedTreePath} for ${chalk.white(
|
|
||||||
releaseVersion.gitTag
|
|
||||||
)}`;
|
|
||||||
break;
|
|
||||||
case config.createRelease === 'github':
|
|
||||||
logTitle += ` GitHub release for ${chalk.white(releaseVersion.gitTag)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output.log({
|
|
||||||
title: logTitle,
|
|
||||||
});
|
|
||||||
|
|
||||||
const githubRepoSlug =
|
const githubRepoSlug =
|
||||||
config.createRelease === 'github'
|
config.createRelease === 'github'
|
||||||
? getGitHubRepoSlug(gitRemote)
|
? getGitHubRepoSlug(gitRemote)
|
||||||
@ -866,15 +864,6 @@ async function generateChangelogForProjects(
|
|||||||
contents = readFileSync(changelogPath, 'utf-8');
|
contents = readFileSync(changelogPath, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The exact logic we use for printing the summary/diff to the user is dependent upon whether they are creating
|
|
||||||
* a changelog file, a GitHub release, or both.
|
|
||||||
*/
|
|
||||||
let printSummary = () => {};
|
|
||||||
const noDiffInChangelogMessage = chalk.yellow(
|
|
||||||
`NOTE: There was no diff detected for the changelog entry. Maybe you intended to pass alternative git references via --from and --to?`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (interpolatedTreePath) {
|
if (interpolatedTreePath) {
|
||||||
let changelogContents = tree.exists(interpolatedTreePath)
|
let changelogContents = tree.exists(interpolatedTreePath)
|
||||||
? tree.read(interpolatedTreePath).toString()
|
? tree.read(interpolatedTreePath).toString()
|
||||||
@ -903,113 +892,24 @@ async function generateChangelogForProjects(
|
|||||||
|
|
||||||
tree.write(interpolatedTreePath, changelogContents);
|
tree.write(interpolatedTreePath, changelogContents);
|
||||||
|
|
||||||
printSummary = () =>
|
printAndFlushChanges(
|
||||||
printAndFlushChanges(
|
tree,
|
||||||
tree,
|
!!dryRun,
|
||||||
!!dryRun,
|
3,
|
||||||
3,
|
false,
|
||||||
false,
|
noDiffInChangelogMessage,
|
||||||
noDiffInChangelogMessage,
|
// Only print the change for the current changelog file at this point
|
||||||
// Only print the change for the current changelog file at this point
|
(f) => f.path === interpolatedTreePath
|
||||||
(f) => f.path === interpolatedTreePath
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.createRelease === 'github') {
|
projectChangelogs[project.name] = {
|
||||||
if (!githubRepoSlug) {
|
releaseVersion,
|
||||||
output.error({
|
contents,
|
||||||
title: `Unable to create a GitHub release because the GitHub repo slug could not be determined.`,
|
};
|
||||||
bodyLines: [
|
|
||||||
`Please ensure you have a valid GitHub remote configured. You can run \`git remote -v\` to list your current remotes.`,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await resolveGithubToken();
|
|
||||||
const githubRequestConfig: GithubRequestConfig = {
|
|
||||||
repo: githubRepoSlug,
|
|
||||||
token,
|
|
||||||
};
|
|
||||||
|
|
||||||
let existingGithubReleaseForVersion: GithubRelease;
|
|
||||||
try {
|
|
||||||
existingGithubReleaseForVersion = await getGithubReleaseByTag(
|
|
||||||
githubRequestConfig,
|
|
||||||
releaseVersion.gitTag
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.response?.status === 401) {
|
|
||||||
output.error({
|
|
||||||
title: `Unable to resolve data via the GitHub API. You can use any of the following options to resolve this:`,
|
|
||||||
bodyLines: [
|
|
||||||
'- Set the `GITHUB_TOKEN` or `GH_TOKEN` environment variable to a valid GitHub token with `repo` scope',
|
|
||||||
'- Have an active session via the official gh CLI tool (https://cli.github.com) in your current terminal',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (err.response?.status === 404) {
|
|
||||||
// No existing release found, this is fine
|
|
||||||
} else {
|
|
||||||
// Rethrow unknown errors for now
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let existingPrintSummaryFn = printSummary;
|
|
||||||
printSummary = () => {
|
|
||||||
const logTitle = `https://github.com/${githubRepoSlug}/releases/tag/${releaseVersion.gitTag}`;
|
|
||||||
if (existingGithubReleaseForVersion) {
|
|
||||||
console.error(
|
|
||||||
`${chalk.white('UPDATE')} ${logTitle}${
|
|
||||||
dryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
`${chalk.green('CREATE')} ${logTitle}${
|
|
||||||
dryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Only print the diff here if we are not already going to be printing changes from the Tree
|
|
||||||
if (!interpolatedTreePath) {
|
|
||||||
console.log('');
|
|
||||||
printDiff(
|
|
||||||
existingGithubReleaseForVersion
|
|
||||||
? existingGithubReleaseForVersion.body
|
|
||||||
: '',
|
|
||||||
contents,
|
|
||||||
3,
|
|
||||||
noDiffInChangelogMessage
|
|
||||||
);
|
|
||||||
}
|
|
||||||
existingPrintSummaryFn();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only schedule the actual GitHub update when not in dry-run mode
|
|
||||||
if (!dryRun) {
|
|
||||||
postGitTasks.push(async (latestCommit) => {
|
|
||||||
// Before we can create/update the release we need to ensure the commit exists on the remote
|
|
||||||
await gitPush(gitRemote);
|
|
||||||
|
|
||||||
await createOrUpdateGithubRelease(
|
|
||||||
githubRequestConfig,
|
|
||||||
{
|
|
||||||
version: releaseVersion.gitTag,
|
|
||||||
prerelease: releaseVersion.isPrerelease,
|
|
||||||
body: contents,
|
|
||||||
commit: latestCommit,
|
|
||||||
},
|
|
||||||
existingGithubReleaseForVersion
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
printSummary();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return projectChangelogs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkChangelogFilesEnabled(nxReleaseConfig: NxReleaseConfig): boolean {
|
function checkChangelogFilesEnabled(nxReleaseConfig: NxReleaseConfig): boolean {
|
||||||
@ -1040,3 +940,14 @@ async function getCommits(fromSHA: string, toSHA: string) {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldCreateGitHubRelease(
|
||||||
|
changelogConfig: NxReleaseChangelogConfiguration | false | undefined,
|
||||||
|
createReleaseArg: ChangelogOptions['createRelease'] | undefined = undefined
|
||||||
|
): boolean {
|
||||||
|
if (createReleaseArg !== undefined) {
|
||||||
|
return createReleaseArg === 'github';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (changelogConfig || {}).createRelease === 'github';
|
||||||
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export type ChangelogOptions = NxReleaseArgs &
|
|||||||
from?: string;
|
from?: string;
|
||||||
interactive?: string;
|
interactive?: string;
|
||||||
gitRemote?: string;
|
gitRemote?: string;
|
||||||
|
createRelease?: false | 'github';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PublishOptions = NxReleaseArgs &
|
export type PublishOptions = NxReleaseArgs &
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { readNxJson } from '../../config/nx-json';
|
|||||||
import { output } from '../../devkit-exports';
|
import { output } from '../../devkit-exports';
|
||||||
import { createProjectGraphAsync } from '../../project-graph/project-graph';
|
import { createProjectGraphAsync } from '../../project-graph/project-graph';
|
||||||
import { handleErrors } from '../../utils/params';
|
import { handleErrors } from '../../utils/params';
|
||||||
import { releaseChangelog } from './changelog';
|
import { releaseChangelog, shouldCreateGitHubRelease } from './changelog';
|
||||||
import { ReleaseOptions, VersionOptions } from './command-object';
|
import { ReleaseOptions, VersionOptions } from './command-object';
|
||||||
import {
|
import {
|
||||||
createNxReleaseConfig,
|
createNxReleaseConfig,
|
||||||
@ -11,7 +11,8 @@ import {
|
|||||||
} from './config/config';
|
} from './config/config';
|
||||||
import { filterReleaseGroups } from './config/filter-release-groups';
|
import { filterReleaseGroups } from './config/filter-release-groups';
|
||||||
import { releasePublish } from './publish';
|
import { releasePublish } from './publish';
|
||||||
import { gitCommit, gitTag } from './utils/git';
|
import { getCommitHash, gitCommit, gitPush, gitTag } from './utils/git';
|
||||||
|
import { createOrUpdateGithubRelease } from './utils/github';
|
||||||
import { resolveNxJsonConfigErrorMessage } from './utils/resolve-nx-json-error-message';
|
import { resolveNxJsonConfigErrorMessage } from './utils/resolve-nx-json-error-message';
|
||||||
import {
|
import {
|
||||||
createCommitMessageValues,
|
createCommitMessageValues,
|
||||||
@ -74,13 +75,14 @@ export async function release(
|
|||||||
gitTag: false,
|
gitTag: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
await releaseChangelog({
|
const changelogResult = await releaseChangelog({
|
||||||
...args,
|
...args,
|
||||||
versionData: versionResult.projectsVersionData,
|
versionData: versionResult.projectsVersionData,
|
||||||
version: versionResult.workspaceVersion,
|
version: versionResult.workspaceVersion,
|
||||||
stageChanges: shouldStage,
|
stageChanges: shouldStage,
|
||||||
gitCommit: false,
|
gitCommit: false,
|
||||||
gitTag: false,
|
gitTag: false,
|
||||||
|
createRelease: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -140,6 +142,82 @@ export async function release(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldCreateWorkspaceRelease = shouldCreateGitHubRelease(
|
||||||
|
nxReleaseConfig.changelog.workspaceChangelog
|
||||||
|
);
|
||||||
|
|
||||||
|
let hasPushedChanges = false;
|
||||||
|
let latestCommit: string | undefined;
|
||||||
|
|
||||||
|
if (shouldCreateWorkspaceRelease && changelogResult.workspaceChangelog) {
|
||||||
|
output.logSingleLine(`Pushing to git remote`);
|
||||||
|
|
||||||
|
// Before we can create/update the release we need to ensure the commit exists on the remote
|
||||||
|
await gitPush({
|
||||||
|
dryRun: args.dryRun,
|
||||||
|
verbose: args.verbose,
|
||||||
|
});
|
||||||
|
|
||||||
|
hasPushedChanges = true;
|
||||||
|
|
||||||
|
output.logSingleLine(`Creating GitHub Release`);
|
||||||
|
|
||||||
|
latestCommit = await getCommitHash('HEAD');
|
||||||
|
await createOrUpdateGithubRelease(
|
||||||
|
changelogResult.workspaceChangelog.releaseVersion,
|
||||||
|
changelogResult.workspaceChangelog.contents,
|
||||||
|
latestCommit,
|
||||||
|
{ dryRun: args.dryRun }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const releaseGroup of releaseGroups) {
|
||||||
|
const shouldCreateProjectReleases = shouldCreateGitHubRelease(
|
||||||
|
releaseGroup.changelog
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldCreateProjectReleases && changelogResult.projectChangelogs) {
|
||||||
|
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
|
||||||
|
Array.from(releaseGroupToFilteredProjects.get(releaseGroup))
|
||||||
|
: // Otherwise, we use the full list of projects within the release group
|
||||||
|
releaseGroup.projects;
|
||||||
|
const projectNodes = projects.map((name) => projectGraph.nodes[name]);
|
||||||
|
|
||||||
|
for (const project of projectNodes) {
|
||||||
|
const changelog = changelogResult.projectChangelogs[project.name];
|
||||||
|
if (!changelog) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPushedChanges) {
|
||||||
|
output.logSingleLine(`Pushing to git remote`);
|
||||||
|
|
||||||
|
// Before we can create/update the release we need to ensure the commit exists on the remote
|
||||||
|
await gitPush({
|
||||||
|
dryRun: args.dryRun,
|
||||||
|
verbose: args.verbose,
|
||||||
|
});
|
||||||
|
|
||||||
|
hasPushedChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.logSingleLine(`Creating GitHub Release`);
|
||||||
|
|
||||||
|
if (!latestCommit) {
|
||||||
|
latestCommit = await getCommitHash('HEAD');
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOrUpdateGithubRelease(
|
||||||
|
changelog.releaseVersion,
|
||||||
|
changelog.contents,
|
||||||
|
latestCommit,
|
||||||
|
{ dryRun: args.dryRun }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let hasNewVersion = false;
|
let hasNewVersion = false;
|
||||||
// null means that all projects are versioned together but there were no changes
|
// null means that all projects are versioned together but there were no changes
|
||||||
if (versionResult.workspaceVersion !== null) {
|
if (versionResult.workspaceVersion !== null) {
|
||||||
|
|||||||
@ -249,17 +249,40 @@ export async function gitTag({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function gitPush(gitRemote?: string) {
|
export async function gitPush({
|
||||||
|
gitRemote,
|
||||||
|
dryRun,
|
||||||
|
verbose,
|
||||||
|
}: {
|
||||||
|
gitRemote?: string;
|
||||||
|
dryRun?: boolean;
|
||||||
|
verbose?: boolean;
|
||||||
|
}) {
|
||||||
|
const commandArgs = [
|
||||||
|
'push',
|
||||||
|
// NOTE: It's important we use --follow-tags, and not --tags, so that we are precise about what we are pushing
|
||||||
|
'--follow-tags',
|
||||||
|
'--no-verify',
|
||||||
|
'--atomic',
|
||||||
|
// Set custom git remote if provided
|
||||||
|
...(gitRemote ? [gitRemote] : []),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
dryRun
|
||||||
|
? `Would push the current branch to the remote with the following command, but --dry-run was set:`
|
||||||
|
: `Pushing the current branch to the remote with the following command:`
|
||||||
|
);
|
||||||
|
console.log(`git ${commandArgs.join(' ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await execCommand('git', [
|
await execCommand('git', commandArgs);
|
||||||
'push',
|
|
||||||
// NOTE: It's important we use --follow-tags, and not --tags, so that we are precise about what we are pushing
|
|
||||||
'--follow-tags',
|
|
||||||
'--no-verify',
|
|
||||||
'--atomic',
|
|
||||||
// Set custom git remote if provided
|
|
||||||
...(gitRemote ? [gitRemote] : []),
|
|
||||||
]);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`Unexpected git push error: ${err}`);
|
throw new Error(`Unexpected git push error: ${err}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import { homedir } from 'node:os';
|
|||||||
import { output } from '../../../utils/output';
|
import { output } from '../../../utils/output';
|
||||||
import { joinPathFragments } from '../../../utils/path';
|
import { joinPathFragments } from '../../../utils/path';
|
||||||
import { Reference } from './git';
|
import { Reference } from './git';
|
||||||
|
import { printDiff } from './print-changes';
|
||||||
|
import { ReleaseVersion, noDiffInChangelogMessage } from './shared';
|
||||||
|
|
||||||
// axios types and values don't seem to match
|
// axios types and values don't seem to match
|
||||||
import _axios = require('axios');
|
import _axios = require('axios');
|
||||||
@ -56,6 +58,91 @@ export function getGitHubRepoSlug(remoteName = 'origin'): RepoSlug {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createOrUpdateGithubRelease(
|
||||||
|
releaseVersion: ReleaseVersion,
|
||||||
|
changelogContents: string,
|
||||||
|
latestCommit: string,
|
||||||
|
{ dryRun }: { dryRun: boolean }
|
||||||
|
): Promise<void> {
|
||||||
|
const githubRepoSlug = getGitHubRepoSlug();
|
||||||
|
if (!githubRepoSlug) {
|
||||||
|
output.error({
|
||||||
|
title: `Unable to create a GitHub release because the GitHub repo slug could not be determined.`,
|
||||||
|
bodyLines: [
|
||||||
|
`Please ensure you have a valid GitHub remote configured. You can run \`git remote -v\` to list your current remotes.`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await resolveGithubToken();
|
||||||
|
const githubRequestConfig: GithubRequestConfig = {
|
||||||
|
repo: githubRepoSlug,
|
||||||
|
token,
|
||||||
|
};
|
||||||
|
|
||||||
|
let existingGithubReleaseForVersion: GithubRelease;
|
||||||
|
try {
|
||||||
|
existingGithubReleaseForVersion = await getGithubReleaseByTag(
|
||||||
|
githubRequestConfig,
|
||||||
|
releaseVersion.gitTag
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response?.status === 401) {
|
||||||
|
output.error({
|
||||||
|
title: `Unable to resolve data via the GitHub API. You can use any of the following options to resolve this:`,
|
||||||
|
bodyLines: [
|
||||||
|
'- Set the `GITHUB_TOKEN` or `GH_TOKEN` environment variable to a valid GitHub token with `repo` scope',
|
||||||
|
'- Have an active session via the official gh CLI tool (https://cli.github.com) in your current terminal',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (err.response?.status === 404) {
|
||||||
|
// No existing release found, this is fine
|
||||||
|
} else {
|
||||||
|
// Rethrow unknown errors for now
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logTitle = `https://github.com/${githubRepoSlug}/releases/tag/${releaseVersion.gitTag}`;
|
||||||
|
if (existingGithubReleaseForVersion) {
|
||||||
|
console.error(
|
||||||
|
`${chalk.white('UPDATE')} ${logTitle}${
|
||||||
|
dryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`${chalk.green('CREATE')} ${logTitle}${
|
||||||
|
dryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
printDiff(
|
||||||
|
existingGithubReleaseForVersion ? existingGithubReleaseForVersion.body : '',
|
||||||
|
changelogContents,
|
||||||
|
3,
|
||||||
|
noDiffInChangelogMessage
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
await createOrUpdateGithubReleaseInternal(
|
||||||
|
githubRequestConfig,
|
||||||
|
{
|
||||||
|
version: releaseVersion.gitTag,
|
||||||
|
prerelease: releaseVersion.isPrerelease,
|
||||||
|
body: changelogContents,
|
||||||
|
commit: latestCommit,
|
||||||
|
},
|
||||||
|
existingGithubReleaseForVersion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface GithubReleaseOptions {
|
interface GithubReleaseOptions {
|
||||||
version: string;
|
version: string;
|
||||||
body: string;
|
body: string;
|
||||||
@ -63,7 +150,7 @@ interface GithubReleaseOptions {
|
|||||||
commit: string;
|
commit: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createOrUpdateGithubRelease(
|
async function createOrUpdateGithubReleaseInternal(
|
||||||
githubRequestConfig: GithubRequestConfig,
|
githubRequestConfig: GithubRequestConfig,
|
||||||
release: GithubReleaseOptions,
|
release: GithubReleaseOptions,
|
||||||
existingGithubReleaseForVersion?: GithubRelease
|
existingGithubReleaseForVersion?: GithubRelease
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import * as chalk from 'chalk';
|
||||||
import { prerelease } from 'semver';
|
import { prerelease } from 'semver';
|
||||||
import { ProjectGraph } from '../../../config/project-graph';
|
import { ProjectGraph } from '../../../config/project-graph';
|
||||||
import { Tree } from '../../../generators/tree';
|
import { Tree } from '../../../generators/tree';
|
||||||
@ -7,6 +8,10 @@ import { output } from '../../../utils/output';
|
|||||||
import type { ReleaseGroupWithName } from '../config/filter-release-groups';
|
import type { ReleaseGroupWithName } from '../config/filter-release-groups';
|
||||||
import { GitCommit, gitAdd, gitCommit } from './git';
|
import { GitCommit, gitAdd, gitCommit } from './git';
|
||||||
|
|
||||||
|
export const noDiffInChangelogMessage = chalk.yellow(
|
||||||
|
`NOTE: There was no diff detected for the changelog entry. Maybe you intended to pass alternative git references via --from and --to?`
|
||||||
|
);
|
||||||
|
|
||||||
export type ReleaseVersionGeneratorResult = {
|
export type ReleaseVersionGeneratorResult = {
|
||||||
data: VersionData;
|
data: VersionData;
|
||||||
callback: (
|
callback: (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user