diff --git a/src/monorepo-workflow-operations.ts b/src/monorepo-workflow-operations.ts index 5409ba5..969161a 100644 --- a/src/monorepo-workflow-operations.ts +++ b/src/monorepo-workflow-operations.ts @@ -10,12 +10,13 @@ import { determineEditor } from './editor'; import { ReleaseType } from './initial-parameters'; import { Project } from './project'; import { planRelease, executeReleasePlan } from './release-plan'; -import { captureChangesInReleaseBranch } from './repo'; +import { createReleaseBranch, captureChangesInReleaseBranch } from './repo'; import { generateReleaseSpecificationTemplateForMonorepo, waitForUserToEditReleaseSpecification, validateReleaseSpecification, } from './release-specification'; +import { updateChangedPackagesChangelog } from './package'; /** * For a monorepo, the process works like this: @@ -64,6 +65,15 @@ export async function followMonorepoWorkflow({ stdout: Pick; stderr: Pick; }) { + const { version: newReleaseVersion, firstRun } = await createReleaseBranch({ + project, + releaseType, + }); + + if (firstRun) { + await updateChangedPackagesChangelog({ project, stderr }); + } + const releaseSpecificationPath = path.join( tempDirectoryPath, 'RELEASE_SPEC.yml', @@ -115,11 +125,11 @@ export async function followMonorepoWorkflow({ const releasePlan = await planRelease({ project, releaseSpecification, - releaseType, + newReleaseVersion, }); await executeReleasePlan(project, releasePlan, stderr); await removeFile(releaseSpecificationPath); await captureChangesInReleaseBranch(project.directoryPath, { - releaseVersion: releasePlan.newVersion, + releaseVersion: newReleaseVersion, }); } diff --git a/src/package.ts b/src/package.ts index 3e76566..f3ca17b 100644 --- a/src/package.ts +++ b/src/package.ts @@ -229,6 +229,66 @@ export async function readMonorepoWorkspacePackage({ }; } +/** + * Updates the changelog file of the given package using + * `@metamask/auto-changelog`. Assumes that the changelog file is located at the + * package root directory and named "CHANGELOG.md". + * + * @param args - The arguments. + * @param args.project - The project. + * @param args.stderr - A stream that can be used to write to standard error. + * @returns The result of writing to the changelog. + */ +export async function updateChangedPackagesChangelog({ + project: { repositoryUrl, workspacePackages }, + stderr, +}: { + project: Pick< + Project, + 'directoryPath' | 'repositoryUrl' | 'workspacePackages' + >; + stderr: Pick; +}): Promise { + await Promise.all( + Object.values(workspacePackages) + .filter( + ({ hasChangesSinceLatestRelease }) => hasChangesSinceLatestRelease, + ) + .map(async (pkg) => { + let changelogContent; + + try { + changelogContent = await readFile(pkg.changelogPath); + } catch (error) { + if (isErrorWithCode(error) && error.code === 'ENOENT') { + stderr.write( + `${pkg.validatedManifest.name} does not seem to have a changelog. Skipping.\n`, + ); + return; + } + + throw error; + } + + const newChangelogContent = await updateChangelog({ + changelogContent, + isReleaseCandidate: false, + projectRootDirectory: pkg.directoryPath, + repoUrl: repositoryUrl, + tagPrefixes: [`${pkg.validatedManifest.name}@`, 'v'], + }); + + if (newChangelogContent) { + await writeFile(pkg.changelogPath, newChangelogContent); + } else { + stderr.write( + `Changelog for ${pkg.validatedManifest.name} was not updated as there were no updates to make.`, + ); + } + }), + ); +} + /** * Updates the changelog file of the given package using * `@metamask/auto-changelog`. Assumes that the changelog file is located at the diff --git a/src/release-plan.ts b/src/release-plan.ts index 0ee5223..389b661 100644 --- a/src/release-plan.ts +++ b/src/release-plan.ts @@ -1,6 +1,5 @@ import type { WriteStream } from 'fs'; import { SemVer } from 'semver'; -import { ReleaseType } from './initial-parameters'; import { debug } from './misc-utils'; import { Package, updatePackage } from './package'; import { Project } from './project'; @@ -48,33 +47,25 @@ export type PackageReleasePlan = { /** * Uses the release specification to calculate the final versions of all of the - * packages that we want to update, as well as a new release name. + * packages that we want to update. * * @param args - The arguments. * @param args.project - Information about the whole project (e.g., names of * packages and where they can found). * @param args.releaseSpecification - A parsed version of the release spec * entered by the user. - * @param args.releaseType - The type of release ("ordinary" or "backport"), - * which affects how the version is bumped. + * @param args.newReleaseVersion - The new release version. * @returns A promise for information about the new release. */ export async function planRelease({ project, releaseSpecification, - releaseType, + newReleaseVersion, }: { project: Project; releaseSpecification: ReleaseSpecification; - releaseType: ReleaseType; + newReleaseVersion: string; }): Promise { - const newReleaseVersion = - releaseType === 'backport' - ? `${project.releaseVersion.ordinaryNumber}.${ - project.releaseVersion.backportNumber + 1 - }.0` - : `${project.releaseVersion.ordinaryNumber + 1}.0.0`; - const rootReleasePlan: PackageReleasePlan = { package: project.rootPackage, newVersion: newReleaseVersion, diff --git a/src/repo.ts b/src/repo.ts index 2741101..b80343a 100644 --- a/src/repo.ts +++ b/src/repo.ts @@ -1,9 +1,12 @@ import path from 'path'; import { + debug, runCommand, getStdoutFromCommand, getLinesFromCommand, } from './misc-utils'; +import { ReleaseType } from './initial-parameters'; +import { Project } from './project'; const CHANGED_FILE_PATHS_BY_TAG_NAME: Record = {}; @@ -170,13 +173,8 @@ export async function getRepositoryHttpsUrl( } /** - * This function does three things: - * - * 1. Stages all of the changes which have been made to the repo thus far and + * This function stages all of the changes which have been made to the repo thus far and * creates a new Git commit which carries the name of the new release. - * 2. Creates a new branch pointed to that commit (which also carries the name - * of the new release). - * 3. Switches to that branch. * * @param repositoryDirectoryPath - The path to the repository directory. * @param args - The arguments. @@ -186,10 +184,6 @@ export async function captureChangesInReleaseBranch( repositoryDirectoryPath: string, { releaseVersion }: { releaseVersion: string }, ) { - await getStdoutFromGitCommandWithin(repositoryDirectoryPath, 'checkout', [ - '-b', - `release/${releaseVersion}`, - ]); await getStdoutFromGitCommandWithin(repositoryDirectoryPath, 'add', ['-A']); await getStdoutFromGitCommandWithin(repositoryDirectoryPath, 'commit', [ '-m', @@ -197,6 +191,78 @@ export async function captureChangesInReleaseBranch( ]); } +/** + * This function does create the release branch. + * + * @param args - The arguments. + * @param args.project - Information about the whole project (e.g., names of + * packages and where they can found). + * @param args.releaseType - The type of release ("ordinary" or "backport"), + * which affects how the version is bumped. + * @returns A promise for the newReleaseVersion. + */ +export async function createReleaseBranch({ + project, + releaseType, +}: { + project: Project; + releaseType: ReleaseType; +}): Promise<{ + version: string; + firstRun: boolean; +}> { + const newReleaseVersion = + releaseType === 'backport' + ? `${project.releaseVersion.ordinaryNumber}.${ + project.releaseVersion.backportNumber + 1 + }.0` + : `${project.releaseVersion.ordinaryNumber + 1}.0.0`; + + const releaseBranchName = `release/${newReleaseVersion}`; + + const currentBranchName = await getStdoutFromGitCommandWithin( + project.directoryPath, + 'rev-parse', + ['--abbrev-ref', 'HEAD'], + ); + + if (currentBranchName === releaseBranchName) { + debug(`Already on ${releaseBranchName} branch.`); + return { + version: newReleaseVersion, + firstRun: false, + }; + } + + if ( + await getStdoutFromGitCommandWithin(project.directoryPath, 'branch', [ + '--list', + releaseBranchName, + ]) + ) { + debug( + `Current release branch already exists. Checking out the existing branch.`, + ); + await getStdoutFromGitCommandWithin(project.directoryPath, 'checkout', [ + releaseBranchName, + ]); + return { + version: newReleaseVersion, + firstRun: false, + }; + } + + await getStdoutFromGitCommandWithin(project.directoryPath, 'checkout', [ + '-b', + releaseBranchName, + ]); + + return { + version: newReleaseVersion, + firstRun: true, + }; +} + /** * Retrieves the names of the tags in the given repo, sorted by ascending * semantic version order. As this fetches tags from the remote first, you are