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}`);
};