| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							|  |  |  |  * Copyright Google LLC All Rights Reserved. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Use of this source code is governed by an MIT-style license that can be | 
					
						
							|  |  |  |  * found in the LICENSE file at https://angular.io/license
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import {ListChoiceOptions, prompt} from 'inquirer'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-28 17:10:24 +03:00
										 |  |  | import {spawnWithDebugOutput} from '../../utils/child-process'; | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  | import {GithubConfig} from '../../utils/config'; | 
					
						
							| 
									
										
										
										
											2021-01-19 08:29:46 -08:00
										 |  |  | import {debug, error, info, log, promptConfirm, red, yellow} from '../../utils/console'; | 
					
						
							| 
									
										
										
										
											2021-06-03 15:59:20 +02:00
										 |  |  | import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; | 
					
						
							| 
									
										
										
										
											2021-04-01 16:28:17 -07:00
										 |  |  | import {ReleaseConfig} from '../config/index'; | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  | import {ActiveReleaseTrains, fetchActiveReleaseTrains, nextBranchName} from '../versioning/active-release-trains'; | 
					
						
							| 
									
										
										
										
											2021-01-19 08:29:46 -08:00
										 |  |  | import {npmIsLoggedIn, npmLogin, npmLogout} from '../versioning/npm-publish'; | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  | import {printActiveReleaseTrains} from '../versioning/print-active-trains'; | 
					
						
							|  |  |  | import {GithubRepoWithApi} from '../versioning/version-branches'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import {ReleaseAction} from './actions'; | 
					
						
							|  |  |  | import {FatalReleaseActionError, UserAbortedReleaseActionError} from './actions-error'; | 
					
						
							|  |  |  | import {actions} from './actions/index'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export enum CompletionState { | 
					
						
							|  |  |  |   SUCCESS, | 
					
						
							|  |  |  |   FATAL_ERROR, | 
					
						
							|  |  |  |   MANUALLY_ABORTED, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export class ReleaseTool { | 
					
						
							| 
									
										
										
										
											2021-06-03 15:59:20 +02:00
										 |  |  |   /** The singleton instance of the authenticated git client. */ | 
					
						
							|  |  |  |   private _git = AuthenticatedGitClient.get(); | 
					
						
							| 
									
										
										
										
											2021-01-19 08:29:46 -08:00
										 |  |  |   /** The previous git commit to return back to after the release tool runs. */ | 
					
						
							|  |  |  |   private previousGitBranchOrRevision = this._git.getCurrentBranchOrRevision(); | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |   constructor( | 
					
						
							|  |  |  |       protected _config: ReleaseConfig, protected _github: GithubConfig, | 
					
						
							| 
									
										
										
										
											2021-04-18 12:46:57 +03:00
										 |  |  |       protected _projectRoot: string) {} | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |   /** Runs the interactive release tool. */ | 
					
						
							|  |  |  |   async run(): Promise<CompletionState> { | 
					
						
							|  |  |  |     log(); | 
					
						
							|  |  |  |     log(yellow('--------------------------------------------')); | 
					
						
							|  |  |  |     log(yellow('  Angular Dev-Infra release staging script')); | 
					
						
							|  |  |  |     log(yellow('--------------------------------------------')); | 
					
						
							|  |  |  |     log(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-21 10:46:17 -07:00
										 |  |  |     if (!await this._verifyEnvironmentHasPython3Symlink() || | 
					
						
							|  |  |  |         !await this._verifyNoUncommittedChanges() || !await this._verifyRunningFromNextBranch()) { | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  |       return CompletionState.FATAL_ERROR; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-19 08:29:46 -08:00
										 |  |  |     if (!await this._verifyNpmLoginState()) { | 
					
						
							|  |  |  |       return CompletionState.MANUALLY_ABORTED; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  |     const {owner, name} = this._github; | 
					
						
							|  |  |  |     const repo: GithubRepoWithApi = {owner, name, api: this._git.github}; | 
					
						
							|  |  |  |     const releaseTrains = await fetchActiveReleaseTrains(repo); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Print the active release trains so that the caretaker can access
 | 
					
						
							|  |  |  |     // the current project branching state without switching context.
 | 
					
						
							|  |  |  |     await printActiveReleaseTrains(releaseTrains, this._config); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const action = await this._promptForReleaseAction(releaseTrains); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       await action.perform(); | 
					
						
							|  |  |  |     } catch (e) { | 
					
						
							|  |  |  |       if (e instanceof UserAbortedReleaseActionError) { | 
					
						
							|  |  |  |         return CompletionState.MANUALLY_ABORTED; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       // Only print the error message and stack if the error is not a known fatal release
 | 
					
						
							|  |  |  |       // action error (for which we print the error gracefully to the console with colors).
 | 
					
						
							|  |  |  |       if (!(e instanceof FatalReleaseActionError) && e instanceof Error) { | 
					
						
							| 
									
										
										
										
											2020-10-10 22:02:47 +03:00
										 |  |  |         console.error(e); | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  |       } | 
					
						
							|  |  |  |       return CompletionState.FATAL_ERROR; | 
					
						
							|  |  |  |     } finally { | 
					
						
							| 
									
										
										
										
											2021-01-19 08:29:46 -08:00
										 |  |  |       await this.cleanup(); | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return CompletionState.SUCCESS; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-19 08:29:46 -08:00
										 |  |  |   /** Run post release tool cleanups. */ | 
					
						
							|  |  |  |   private async cleanup(): Promise<void> { | 
					
						
							|  |  |  |     // Return back to the git state from before the release tool ran.
 | 
					
						
							|  |  |  |     this._git.checkout(this.previousGitBranchOrRevision, true); | 
					
						
							|  |  |  |     // Ensure log out of NPM.
 | 
					
						
							|  |  |  |     await npmLogout(this._config.publishRegistry); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  |   /** Prompts the caretaker for a release action that should be performed. */ | 
					
						
							|  |  |  |   private async _promptForReleaseAction(activeTrains: ActiveReleaseTrains) { | 
					
						
							|  |  |  |     const choices: ListChoiceOptions[] = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Find and instantiate all release actions which are currently valid.
 | 
					
						
							|  |  |  |     for (let actionType of actions) { | 
					
						
							| 
									
										
										
										
											2021-05-17 19:02:39 +02:00
										 |  |  |       if (await actionType.isActive(activeTrains, this._config)) { | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  |         const action: ReleaseAction = | 
					
						
							|  |  |  |             new actionType(activeTrains, this._git, this._config, this._projectRoot); | 
					
						
							|  |  |  |         choices.push({name: await action.getDescription(), value: action}); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-10 22:02:47 +03:00
										 |  |  |     info('Please select the type of release you want to perform.'); | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     const {releaseAction} = await prompt<{releaseAction: ReleaseAction}>({ | 
					
						
							|  |  |  |       name: 'releaseAction', | 
					
						
							|  |  |  |       message: 'Please select an action:', | 
					
						
							|  |  |  |       type: 'list', | 
					
						
							|  |  |  |       choices, | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return releaseAction; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Verifies that there are no uncommitted changes in the project. | 
					
						
							|  |  |  |    * @returns a boolean indicating success or failure. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private async _verifyNoUncommittedChanges(): Promise<boolean> { | 
					
						
							|  |  |  |     if (this._git.hasUncommittedChanges()) { | 
					
						
							| 
									
										
										
										
											2020-10-10 22:02:47 +03:00
										 |  |  |       error(red('  ✘   There are changes which are not committed and should be discarded.')); | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-21 10:46:17 -07:00
										 |  |  |   /** | 
					
						
							| 
									
										
										
										
											2021-05-04 16:02:54 +02:00
										 |  |  |    * Verifies that Python can be resolved within scripts and points to a compatible version. Python | 
					
						
							|  |  |  |    * is required in Bazel actions as there can be tools (such as `skydoc`) that rely on it. | 
					
						
							| 
									
										
										
										
											2021-04-21 10:46:17 -07:00
										 |  |  |    * @returns a boolean indicating success or failure. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private async _verifyEnvironmentHasPython3Symlink(): Promise<boolean> { | 
					
						
							|  |  |  |     try { | 
					
						
							| 
									
										
										
										
											2021-05-04 16:02:54 +02:00
										 |  |  |       // Note: We do not rely on `/usr/bin/env` but rather access the `env` binary directly as it
 | 
					
						
							|  |  |  |       // should be part of the shell's `$PATH`. This is necessary for compatibility with Windows.
 | 
					
						
							| 
									
										
										
										
											2021-04-21 10:46:17 -07:00
										 |  |  |       const pyVersion = | 
					
						
							| 
									
										
										
										
											2021-05-04 16:02:54 +02:00
										 |  |  |           await spawnWithDebugOutput('env', ['python', '--version'], {mode: 'silent'}); | 
					
						
							| 
									
										
										
										
											2021-04-21 10:46:17 -07:00
										 |  |  |       const version = pyVersion.stdout.trim() || pyVersion.stderr.trim(); | 
					
						
							|  |  |  |       if (version.startsWith('Python 3.')) { | 
					
						
							|  |  |  |         debug(`Local python version: ${version}`); | 
					
						
							|  |  |  |         return true; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       error(red(`  ✘   \`/usr/bin/python\` is currently symlinked to "${version}", please update`)); | 
					
						
							|  |  |  |       error(red('      the symlink to link instead to Python3')); | 
					
						
							|  |  |  |       error(); | 
					
						
							|  |  |  |       error(red('      Googlers: please run the following command to symlink python to python3:')); | 
					
						
							|  |  |  |       error(red('        sudo ln -s /usr/bin/python3 /usr/bin/python')); | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } catch { | 
					
						
							|  |  |  |       error(red('  ✘   `/usr/bin/python` does not exist, please ensure `/usr/bin/python` is')); | 
					
						
							|  |  |  |       error(red('      symlinked to Python3.')); | 
					
						
							|  |  |  |       error(); | 
					
						
							|  |  |  |       error(red('      Googlers: please run the following command to symlink python to python3:')); | 
					
						
							|  |  |  |       error(red('        sudo ln -s /usr/bin/python3 /usr/bin/python')); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * Verifies that the next branch from the configured repository is checked out. | 
					
						
							|  |  |  |    * @returns a boolean indicating success or failure. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private async _verifyRunningFromNextBranch(): Promise<boolean> { | 
					
						
							|  |  |  |     const headSha = this._git.run(['rev-parse', 'HEAD']).stdout.trim(); | 
					
						
							|  |  |  |     const {data} = | 
					
						
							|  |  |  |         await this._git.github.repos.getBranch({...this._git.remoteParams, branch: nextBranchName}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (headSha !== data.commit.sha) { | 
					
						
							| 
									
										
										
										
											2020-10-10 22:02:47 +03:00
										 |  |  |       error(red('  ✘   Running release tool from an outdated local branch.')); | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  |       error(red(`      Please make sure you are running from the "${nextBranchName}" branch.`)); | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-01-19 08:29:46 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Verifies that the user is logged into NPM at the correct registry, if defined for the release. | 
					
						
							|  |  |  |    * @returns a boolean indicating whether the user is logged into NPM. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private async _verifyNpmLoginState(): Promise<boolean> { | 
					
						
							|  |  |  |     const registry = `NPM at the ${this._config.publishRegistry ?? 'default NPM'} registry`; | 
					
						
							| 
									
										
										
										
											2021-04-01 15:34:14 -07:00
										 |  |  |     // TODO(josephperrott): remove wombat specific block once wombot allows `npm whoami` check to
 | 
					
						
							|  |  |  |     // check the status of the local token in the .npmrc file.
 | 
					
						
							|  |  |  |     if (this._config.publishRegistry?.includes('wombat-dressing-room.appspot.com')) { | 
					
						
							|  |  |  |       info('Unable to determine NPM login state for wombat proxy, requiring login now.'); | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         await npmLogin(this._config.publishRegistry); | 
					
						
							|  |  |  |       } catch { | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return true; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-01-19 08:29:46 -08:00
										 |  |  |     if (await npmIsLoggedIn(this._config.publishRegistry)) { | 
					
						
							|  |  |  |       debug(`Already logged into ${registry}.`); | 
					
						
							|  |  |  |       return true; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     error(red(`  ✘   Not currently logged into ${registry}.`)); | 
					
						
							|  |  |  |     const shouldLogin = await promptConfirm('Would you like to log into NPM now?'); | 
					
						
							|  |  |  |     if (shouldLogin) { | 
					
						
							|  |  |  |       debug('Starting NPM login.'); | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         await npmLogin(this._config.publishRegistry); | 
					
						
							|  |  |  |       } catch { | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return true; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2020-09-09 15:01:18 +02:00
										 |  |  | } |