|
| 1 | +import {bold, cyan, green, italic, red, yellow} from 'chalk'; |
| 2 | +import {existsSync, readFileSync, writeFileSync} from 'fs'; |
| 3 | +import {prompt} from 'inquirer'; |
| 4 | +import {join} from 'path'; |
| 5 | +import {GitClient} from './git/git-client'; |
| 6 | +import {promptForNewVersion} from './prompt/new-version-prompt'; |
| 7 | +import {parseVersionName, Version} from './version-name/parse-version'; |
| 8 | +import {getExpectedPublishBranch} from './version-name/publish-branch'; |
| 9 | + |
| 10 | +/** |
| 11 | + * Class that can be instantiated in order to stage a new release. The tasks requires user |
| 12 | + * interaction/input through command line prompts. |
| 13 | + * |
| 14 | + * Staging a release involves the following the steps: |
| 15 | + * |
| 16 | + * 1) Prompt for release type (with version suggestion) |
| 17 | + * 2) Prompt for version name if no suggestions has been selected |
| 18 | + * 3) Assert that the proper publish branch is checked out (e.g. 6.4.x for patches) |
| 19 | + * 4) Assert that there are no local changes which are uncommitted. |
| 20 | + * 5) Assert that the local branch is up to date with the remote branch. |
| 21 | + * 6) Creates a new branch for the release staging (release-stage/{VERSION}) |
| 22 | + * 7) Switches to the staging branch and updates the package.json |
| 23 | + * 8) Waits for the user to continue (users can generate the changelog in the meanwhile) |
| 24 | + * 9) Create a commit that includes all changes in the staging branch. |
| 25 | + */ |
| 26 | +class StageReleaseTask { |
| 27 | + |
| 28 | + /** Path to the project package JSON. */ |
| 29 | + packageJsonPath: string; |
| 30 | + |
| 31 | + /** Serialized package.json of the specified project. */ |
| 32 | + packageJson: any; |
| 33 | + |
| 34 | + /** Parsed current version of the project. */ |
| 35 | + currentVersion: Version; |
| 36 | + |
| 37 | + /** Instance of a wrapper that can execute Git commands. */ |
| 38 | + git: GitClient; |
| 39 | + |
| 40 | + constructor(public projectDir: string) { |
| 41 | + this.packageJsonPath = join(projectDir, 'package.json'); |
| 42 | + |
| 43 | + console.log(this.projectDir); |
| 44 | + |
| 45 | + if (!existsSync(this.packageJsonPath)) { |
| 46 | + console.error(red(`The specified directory is not referring to a project directory. ` + |
| 47 | + `There must be a ${italic('package.json')} file in the project directory.`)); |
| 48 | + process.exit(1); |
| 49 | + } |
| 50 | + |
| 51 | + this.packageJson = JSON.parse(readFileSync(this.packageJsonPath, 'utf-8')); |
| 52 | + this.currentVersion = parseVersionName(this.packageJson.version); |
| 53 | + |
| 54 | + if (!this.currentVersion) { |
| 55 | + console.error(red(`Cannot parse current version in ${italic('package.json')}. Please ` + |
| 56 | + `make sure "${this.packageJson.version}" is a valid Semver version.`)); |
| 57 | + process.exit(1); |
| 58 | + } |
| 59 | + |
| 60 | + this.git = new GitClient(projectDir, this.packageJson.repository.url); |
| 61 | + } |
| 62 | + |
| 63 | + async run() { |
| 64 | + console.log(); |
| 65 | + console.log(cyan('-----------------------------------------')); |
| 66 | + console.log(cyan(' Angular Material stage release script')); |
| 67 | + console.log(cyan('-----------------------------------------')); |
| 68 | + console.log(); |
| 69 | + |
| 70 | + const newVersion = await promptForNewVersion(this.currentVersion); |
| 71 | + const expectedPublishBranch = getExpectedPublishBranch(newVersion); |
| 72 | + |
| 73 | + // After the prompt for the new version, we print a new line because we want the |
| 74 | + // new log messages to be more in the foreground. |
| 75 | + console.log(); |
| 76 | + |
| 77 | + this.verifyPublishBranch(expectedPublishBranch); |
| 78 | + this.verifyLocalCommitsMatchUpstream(expectedPublishBranch); |
| 79 | + this.verifyNoUncommittedChanges(); |
| 80 | + |
| 81 | + // TODO(devversion): Assert that GitHub statuses succeed for this branch. |
| 82 | + |
| 83 | + const newVersionName = newVersion.format(); |
| 84 | + const stagingBranch = `release-stage/${newVersionName}`; |
| 85 | + |
| 86 | + if (!this.git.checkoutNewBranch(stagingBranch)) { |
| 87 | + console.error(red(`Could not create release staging branch: ${stagingBranch}. Aborting...`)); |
| 88 | + process.exit(1); |
| 89 | + } |
| 90 | + |
| 91 | + this.updatePackageJsonVersion(newVersionName); |
| 92 | + |
| 93 | + console.log(green(` ✓ Updated the version to "${bold(newVersionName)}" inside of the ` + |
| 94 | + `${italic('package.json')}`)); |
| 95 | + |
| 96 | + // TODO(devversion): run changelog script w/prompts in the future. |
| 97 | + // For now, we just let users make modifications and stage the changes. |
| 98 | + |
| 99 | + console.log(yellow(` ⚠ Please generate the ${bold('CHANGELOG')} for the new version. ` + |
| 100 | + `You can also make other unrelated modifications. After the changes have been made, ` + |
| 101 | + `just continue here.`)); |
| 102 | + console.log(); |
| 103 | + |
| 104 | + const {shouldContinue} = await prompt<{shouldContinue: boolean}>({ |
| 105 | + type: 'confirm', |
| 106 | + name: 'shouldContinue', |
| 107 | + message: 'Do you want to proceed and commit the changes?' |
| 108 | + }); |
| 109 | + |
| 110 | + if (!shouldContinue) { |
| 111 | + console.log(); |
| 112 | + console.log(yellow('Aborting release staging...')); |
| 113 | + process.exit(1); |
| 114 | + } |
| 115 | + |
| 116 | + this.git.stageAllChanges(); |
| 117 | + // this.git.createNewCommit(`chore: bump version to ${newVersionName} w/ changelog`); |
| 118 | + |
| 119 | + console.info(); |
| 120 | + console.info(green(` ✓ Created the staging commit for: "${newVersionName}".`)); |
| 121 | + console.info(green(` ✓ Please push the changes and submit a PR on GitHub.`)); |
| 122 | + console.info(); |
| 123 | + |
| 124 | + // TODO(devversion): automatic push and PR open URL shortcut. |
| 125 | + } |
| 126 | + |
| 127 | + /** Verifies that the user is on the specified publish branch. */ |
| 128 | + private verifyPublishBranch(expectedPublishBranch: string) { |
| 129 | + const currentBranchName = this.git.getCurrentBranch(); |
| 130 | + |
| 131 | + // Check if current branch matches the expected publish branch. |
| 132 | + if (expectedPublishBranch !== currentBranchName) { |
| 133 | + console.error(red(`Cannot stage release from "${italic(currentBranchName)}". Please stage ` + |
| 134 | + `the release from "${bold(expectedPublishBranch)}".`)); |
| 135 | + process.exit(1); |
| 136 | + } |
| 137 | + } |
| 138 | + |
| 139 | + /** Verifies that the local branch is up to date with the given publish branch. */ |
| 140 | + private verifyLocalCommitsMatchUpstream(publishBranch: string) { |
| 141 | + const upstreamCommitSha = this.git.getRemoteCommitSha(publishBranch); |
| 142 | + const localCommitSha = this.git.getLocalCommitSha('HEAD'); |
| 143 | + |
| 144 | + // Check if the current branch is in sync with the remote branch. |
| 145 | + if (upstreamCommitSha !== localCommitSha) { |
| 146 | + console.error(red(`Cannot stage release. The current branch is not in sync with the remote ` + |
| 147 | + `branch. Please make sure your local branch "${italic(publishBranch)}" is up to date.`)); |
| 148 | + process.exit(1); |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + /** Verifies that there are no uncommitted changes in the project. */ |
| 153 | + private verifyNoUncommittedChanges() { |
| 154 | + if (this.git.hasUncommittedChanges()) { |
| 155 | + console.error(red(`Cannot stage release. There are changes which are not committed and ` + |
| 156 | + `should be stashed.`)); |
| 157 | + process.exit(1); |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + /** Updates the version of the project package.json and writes the changes to disk. */ |
| 162 | + private updatePackageJsonVersion(newVersionName: string) { |
| 163 | + const newPackageJson = {...this.packageJson, version: newVersionName}; |
| 164 | + writeFileSync(this.packageJsonPath, JSON.stringify(newPackageJson, null, 2)); |
| 165 | + } |
| 166 | +} |
| 167 | + |
| 168 | +/** Entry-point for the release staging script. */ |
| 169 | +async function main() { |
| 170 | + const projectDir = process.argv.slice(2)[0]; |
| 171 | + |
| 172 | + if (!projectDir) { |
| 173 | + console.error(red(`You specified no project directory. Cannot run stage release script.`)); |
| 174 | + console.error(red(`Usage: bazel run //tools/release:stage-release <project-directory>`)); |
| 175 | + process.exit(1); |
| 176 | + } |
| 177 | + |
| 178 | + return new StageReleaseTask(projectDir).run(); |
| 179 | +} |
| 180 | + |
| 181 | +if (require.main === module) { |
| 182 | + main(); |
| 183 | +} |
| 184 | + |
0 commit comments