import * as fs from 'fs'; import * as path from 'path'; import * as O from 'fp-ts/Option'; import { gitP, CheckRepoActions, SimpleGit } from 'simple-git'; import * as PromiseUtils from '../utils/PromiseUtils'; import { showStringOrUndefined } from '../utils/StringUtils'; import * as Git from '../utils/Git'; import { writeBuildPropertiesFile } from './BuildProperties'; import * as Version from './Version'; import * as PreRelease from './PreRelease'; import * as PackageJson from './PackageJson'; import * as YarnWorkspaces from './YarnWorkspaces'; type MajorMinorVersion = Version.MajorMinorVersion; type Version = Version.Version; type Option = O.Option; type PackageJson = PackageJson.PackageJson; export const getReleaseBranchName = ({ major, minor }: MajorMinorVersion): string => `release/${major}.${minor}`; // eslint-disable-next-line no-shadow export const enum BranchType { Main = 'main', Feature = 'feature', Hotfix = 'hotfix', Spike = 'spike', Release = 'release', Dependabot = 'dependabot' } // eslint-disable-next-line no-shadow export const enum BranchState { Feature = 'feature', Hotfix = 'hotfix', Spike = 'spike', ReleaseReady = 'releaseReady', ReleaseCandidate = 'releaseCandidate' } interface ModuleChangelog { readonly changelogFile: string; readonly changelogFormat: 'keepachangelog' | 'none'; } export interface Module extends ModuleChangelog { readonly packageJson: PackageJson; readonly packageJsonFile: string; } export interface BranchDetails { readonly currentBranch: string; readonly version: Version; readonly rootModule: Module; readonly branchType: BranchType; readonly branchState: BranchState; readonly workspacesEnabled: boolean; readonly modules: Record; } export const versionFromReleaseBranch = async (branchName: string): Promise => { const regexp = /^release\/(?0|[1-9]\d*)\.(?0|[1-9]\d*)$/; const r = regexp.exec(branchName); if (r === null || r.groups === undefined) { return PromiseUtils.fail('Could not parse major.minor version from branch name'); } else { const g = r.groups; const major = parseInt(g.major, 10); const minor = parseInt(g.minor, 10); return { major, minor }; } }; export const mainBranchName = 'main'; export const getBranchType = (branchName: string): Option => { if (branchName === mainBranchName) { return O.some(BranchType.Main); } else { const parts = branchName.split('/'); switch (parts[0]) { case 'feature': return O.some(BranchType.Feature); case 'hotfix': return O.some(BranchType.Hotfix); case 'spike': return O.some(BranchType.Spike); case 'release': return O.some(BranchType.Release); case 'dependabot': return O.some(BranchType.Dependabot); default: return O.none; } } }; export const isValidPrerelease = (actual: string | undefined, expected: string): boolean => actual !== undefined && (actual === expected || actual.startsWith(`${expected}.`)); const findChangelog = (dir: string): ModuleChangelog => { const changelogFile = path.join(dir, 'CHANGELOG.md'); if (fs.existsSync(changelogFile)) { return { changelogFile, changelogFormat: 'keepachangelog' }; } else { return { changelogFile: '', changelogFormat: 'none' }; } }; const readModule = async (dir: string): Promise => { const packageJsonFile = path.join(dir, 'package.json'); const packageJson = await PackageJson.parsePackageJsonFile(packageJsonFile); const changelog = findChangelog(dir); return { packageJson, packageJsonFile, ...changelog }; }; export const readModules = async (dir: string): Promise> => { const ws = await YarnWorkspaces.info(dir); return PromiseUtils.parMapRecord(ws, (w) => readModule(path.join(dir, w.location))); }; export const readModulesIfEnabled = async ( dir: string, packageJson: { workspaces?: string[] } ): Promise<{ workspacesEnabled: boolean; modules: Record }> => { const workspacesEnabled = packageJson.workspaces !== undefined; if (workspacesEnabled) { return { workspacesEnabled, modules: await readModules(dir) }; } else { return { workspacesEnabled, modules: {}}; } }; export const getBranchDetails = async (dir: string): Promise => { const fail = PromiseUtils.fail; const git = gitP(dir); await git.checkIsRepo(CheckRepoActions.IS_REPO_ROOT); const currentBranch = await Git.currentBranch(git); const rootModule = await readModule(dir); const { workspacesEnabled, modules } = await readModulesIfEnabled(dir, rootModule.packageJson); const version = rootModule.packageJson.version; if (version === undefined) { throw new Error('Version missing in package.json file'); } const baseState = { currentBranch, version, rootModule }; const loc = `${currentBranch} branch: package.json version`; const sPackageVersion = Version.versionToString(version); const sPre = showStringOrUndefined(version.preRelease); const rcOrReleaseReady = async (): Promise => { if (version.preRelease === undefined) { return BranchState.ReleaseReady; } else if (isValidPrerelease(version.preRelease, PreRelease.releaseCandidate)) { return BranchState.ReleaseCandidate; } else { const rc = PreRelease.releaseCandidate; return fail(`${loc}: prerelease version part should be either "${rc}" or start with "${rc}." or not be set, but it is "${sPre}"`); } }; const validateMainBranch = rcOrReleaseReady; const validateReleaseBranch = async (): Promise => { const branchVersion = await versionFromReleaseBranch(currentBranch); const sBranchVersion = Version.majorMinorVersionToString(branchVersion); if (version.major !== branchVersion.major || version.minor !== branchVersion.minor) { return fail(`${loc}: major.minor of branch (${sBranchVersion}) is not consistent with package version (${sPackageVersion})`); } else { return rcOrReleaseReady(); } }; const detect = async (branchType: BranchType): Promise => { switch (branchType) { case BranchType.Main: return validateMainBranch(); case BranchType.Feature: case BranchType.Dependabot: return BranchState.Feature; case BranchType.Hotfix: return BranchState.Hotfix; case BranchType.Spike: return BranchState.Spike; case BranchType.Release: return validateReleaseBranch(); } }; if (version.buildMetaData !== undefined) { return fail(`package.json version has an unexpected buildMetaData part`); } else { const obt = getBranchType(currentBranch); if (O.isNone(obt)) { return fail('Invalid branch name. beehive-flow is strict about branch names. Valid names: main, feature/*, hotfix/*, spike/*, release/x.y'); } else { const branchType = obt.value; const branchState = await detect(branchType); return { ...baseState, branchState, branchType, workspacesEnabled, modules }; } } }; export const createReleaseBranch = async (releaseBranchName: string, git: SimpleGit, dir: string): Promise => { console.log(`Creating ${releaseBranchName} branch`); await Git.checkoutNewBranch(git, releaseBranchName); const buildPropertiesFile = await writeBuildPropertiesFile(dir, releaseBranchName); await git.add([ buildPropertiesFile ]); await git.commit(`Creating release branch: ${releaseBranchName}`); };