246 lines
11 KiB
TypeScript
246 lines
11 KiB
TypeScript
/**
|
|
* @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 {writeFileSync} from 'fs';
|
|
import * as nock from 'nock';
|
|
import {join} from 'path';
|
|
import * as semver from 'semver';
|
|
|
|
import {GithubConfig} from '../../../utils/config';
|
|
import * as console from '../../../utils/console';
|
|
import {getBranchPushMatcher, installVirtualGitClientSpies, VirtualGitClient} from '../../../utils/testing';
|
|
import {ReleaseConfig} from '../../config/index';
|
|
import {ActiveReleaseTrains} from '../../versioning/active-release-trains';
|
|
import * as npm from '../../versioning/npm-publish';
|
|
import {_npmPackageInfoCache, NpmDistTag, NpmPackageInfo} from '../../versioning/npm-registry';
|
|
import {ReleaseAction, ReleaseActionConstructor} from '../actions';
|
|
import * as constants from '../constants';
|
|
import * as externalCommands from '../external-commands';
|
|
|
|
import {GithubTestingRepo} from './github-api-testing';
|
|
import {installMockReleaseNotes} from './release-notes/release-notes-utils';
|
|
|
|
/**
|
|
* Temporary directory which will be used as project directory in tests. Note that
|
|
* this environment variable is automatically set by Bazel for tests.
|
|
*/
|
|
export const testTmpDir: string = process.env['TEST_TMPDIR']!;
|
|
|
|
/** Interface describing a test release action. */
|
|
export interface TestReleaseAction<T extends ReleaseAction = ReleaseAction> {
|
|
instance: T;
|
|
gitClient: VirtualGitClient<boolean>;
|
|
repo: GithubTestingRepo;
|
|
fork: GithubTestingRepo;
|
|
testTmpDir: string;
|
|
githubConfig: GithubConfig;
|
|
releaseConfig: ReleaseConfig;
|
|
}
|
|
|
|
/** Gets necessary test mocks for running a release action. */
|
|
export function getTestingMocksForReleaseAction() {
|
|
const githubConfig = {owner: 'angular', name: 'dev-infra-test'};
|
|
const gitClient = VirtualGitClient.getAuthenticatedInstance({github: githubConfig});
|
|
const releaseConfig: ReleaseConfig = {
|
|
npmPackages: [
|
|
'@angular/pkg1',
|
|
'@angular/pkg2',
|
|
],
|
|
releaseNotes: {},
|
|
buildPackages: () => {
|
|
throw Error('Not implemented');
|
|
},
|
|
};
|
|
return {githubConfig, gitClient, releaseConfig};
|
|
}
|
|
|
|
/**
|
|
* Sets up the given release action for testing.
|
|
* @param actionCtor Type of release action to be tested.
|
|
* @param active Fake active release trains for the action,
|
|
* @param isNextPublishedToNpm Whether the next version is published to NPM. True by default.
|
|
*/
|
|
export function setupReleaseActionForTesting<T extends ReleaseAction>(
|
|
actionCtor: ReleaseActionConstructor<T>, active: ActiveReleaseTrains,
|
|
isNextPublishedToNpm = true): TestReleaseAction<T> {
|
|
installVirtualGitClientSpies();
|
|
installMockReleaseNotes();
|
|
|
|
// Reset existing HTTP interceptors.
|
|
nock.cleanAll();
|
|
|
|
const {gitClient, githubConfig, releaseConfig} = getTestingMocksForReleaseAction();
|
|
const repo = new GithubTestingRepo(githubConfig.owner, githubConfig.name);
|
|
const fork = new GithubTestingRepo('some-user', 'fork');
|
|
|
|
// The version for the release-train in the next phase does not necessarily need to be
|
|
// published to NPM. We mock the NPM package request and fake the state of the next
|
|
// version based on the `isNextPublishedToNpm` testing parameter. More details on the
|
|
// special case for the next release train can be found in the next pre-release action.
|
|
fakeNpmPackageQueryRequest(
|
|
releaseConfig.npmPackages[0],
|
|
{versions: {[active.next.version.format()]: isNextPublishedToNpm ? {} : undefined}});
|
|
|
|
const action = new actionCtor(active, gitClient, releaseConfig, testTmpDir);
|
|
|
|
// Fake confirm any prompts. We do not want to make any changelog edits and
|
|
// just proceed with the release action.
|
|
spyOn(console, 'promptConfirm').and.resolveTo(true);
|
|
|
|
// Fake all external commands for the release tool.
|
|
spyOn(npm, 'runNpmPublish').and.resolveTo();
|
|
spyOn(externalCommands, 'invokeSetNpmDistCommand').and.resolveTo();
|
|
spyOn(externalCommands, 'invokeYarnInstallCommand').and.resolveTo();
|
|
spyOn(externalCommands, 'invokeReleaseBuildCommand').and.resolveTo([
|
|
{name: '@angular/pkg1', outputPath: `${testTmpDir}/dist/pkg1`},
|
|
{name: '@angular/pkg2', outputPath: `${testTmpDir}/dist/pkg2`}
|
|
]);
|
|
|
|
// Fake checking the package versions since we don't actually create packages to check against in
|
|
// the publish tests.
|
|
spyOn(ReleaseAction.prototype, '_verifyPackageVersions' as any).and.resolveTo();
|
|
|
|
// Create an empty changelog and a `package.json` file so that file system
|
|
// interactions with the project directory do not cause exceptions.
|
|
writeFileSync(join(testTmpDir, 'CHANGELOG.md'), 'Existing changelog');
|
|
writeFileSync(join(testTmpDir, 'package.json'), JSON.stringify({version: 'unknown'}));
|
|
|
|
// Override the default pull request wait interval to a number of milliseconds that can be
|
|
// awaited in Jasmine tests. The default interval of 10sec is too large and causes a timeout.
|
|
Object.defineProperty(constants, 'waitForPullRequestInterval', {value: 50});
|
|
|
|
return {instance: action, repo, fork, testTmpDir, githubConfig, releaseConfig, gitClient};
|
|
}
|
|
|
|
/** Parses the specified version into Semver. */
|
|
export function parse(version: string): semver.SemVer {
|
|
return semver.parse(version)!;
|
|
}
|
|
|
|
export async function expectStagingAndPublishWithoutCherryPick(
|
|
action: TestReleaseAction, expectedBranch: string, expectedVersion: string,
|
|
expectedNpmDistTag: NpmDistTag) {
|
|
const {repo, fork, gitClient} = action;
|
|
const expectedStagingForkBranch = `release-stage-${expectedVersion}`;
|
|
const expectedTagName = expectedVersion;
|
|
|
|
// We first mock the commit status check for the next branch, then expect two pull
|
|
// requests from a fork that are targeting next and the new feature-freeze branch.
|
|
repo.expectBranchRequest(expectedBranch, 'MASTER_COMMIT_SHA')
|
|
.expectCommitStatusCheck('MASTER_COMMIT_SHA', 'success')
|
|
.expectFindForkRequest(fork)
|
|
.expectPullRequestToBeCreated(expectedBranch, fork, expectedStagingForkBranch, 200)
|
|
.expectPullRequestWait(200)
|
|
.expectBranchRequest(expectedBranch, 'STAGING_COMMIT_SHA')
|
|
.expectCommitRequest(
|
|
'STAGING_COMMIT_SHA', `release: cut the v${expectedVersion} release\n\nPR Close #200.`)
|
|
.expectTagToBeCreated(expectedTagName, 'STAGING_COMMIT_SHA')
|
|
.expectReleaseToBeCreated(`v${expectedVersion}`, expectedTagName);
|
|
|
|
// In the fork, we make the staging branch appear as non-existent,
|
|
// so that the PR can be created properly without collisions.
|
|
fork.expectBranchRequest(expectedStagingForkBranch, null);
|
|
|
|
await action.instance.perform();
|
|
|
|
expect(gitClient.pushed.length).toBe(1);
|
|
expect(gitClient.pushed[0])
|
|
.toEqual(
|
|
getBranchPushMatcher({
|
|
baseBranch: expectedBranch,
|
|
baseRepo: repo,
|
|
targetBranch: expectedStagingForkBranch,
|
|
targetRepo: fork,
|
|
expectedCommits: [{
|
|
message: `release: cut the v${expectedVersion} release`,
|
|
files: ['package.json', 'CHANGELOG.md'],
|
|
}],
|
|
}),
|
|
'Expected release staging branch to be created in fork.');
|
|
|
|
expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1);
|
|
expect(npm.runNpmPublish).toHaveBeenCalledTimes(2);
|
|
expect(npm.runNpmPublish)
|
|
.toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, expectedNpmDistTag, undefined);
|
|
expect(npm.runNpmPublish)
|
|
.toHaveBeenCalledWith(`${testTmpDir}/dist/pkg2`, expectedNpmDistTag, undefined);
|
|
}
|
|
|
|
export async function expectStagingAndPublishWithCherryPick(
|
|
action: TestReleaseAction, expectedBranch: string, expectedVersion: string,
|
|
expectedNpmDistTag: NpmDistTag) {
|
|
const {repo, fork, gitClient, releaseConfig} = action;
|
|
const expectedStagingForkBranch = `release-stage-${expectedVersion}`;
|
|
const expectedCherryPickForkBranch = `changelog-cherry-pick-${expectedVersion}`;
|
|
const expectedTagName = expectedVersion;
|
|
|
|
// We first mock the commit status check for the next branch, then expect two pull
|
|
// requests from a fork that are targeting next and the new feature-freeze branch.
|
|
repo.expectBranchRequest(expectedBranch, 'MASTER_COMMIT_SHA')
|
|
.expectCommitStatusCheck('MASTER_COMMIT_SHA', 'success')
|
|
.expectFindForkRequest(fork)
|
|
.expectPullRequestToBeCreated(expectedBranch, fork, expectedStagingForkBranch, 200)
|
|
.expectPullRequestWait(200)
|
|
.expectBranchRequest(expectedBranch, 'STAGING_COMMIT_SHA')
|
|
.expectCommitRequest(
|
|
'STAGING_COMMIT_SHA', `release: cut the v${expectedVersion} release\n\nPR Close #200.`)
|
|
.expectTagToBeCreated(expectedTagName, 'STAGING_COMMIT_SHA')
|
|
.expectReleaseToBeCreated(`v${expectedVersion}`, expectedTagName)
|
|
.expectPullRequestToBeCreated('master', fork, expectedCherryPickForkBranch, 300)
|
|
.expectPullRequestWait(300);
|
|
|
|
// In the fork, we make the staging and cherry-pick branches appear as
|
|
// non-existent, so that the PRs can be created properly without collisions.
|
|
fork.expectBranchRequest(expectedStagingForkBranch, null)
|
|
.expectBranchRequest(expectedCherryPickForkBranch, null);
|
|
|
|
await action.instance.perform();
|
|
|
|
expect(gitClient.pushed.length).toBe(2);
|
|
expect(gitClient.pushed[0])
|
|
.toEqual(
|
|
getBranchPushMatcher({
|
|
baseBranch: expectedBranch,
|
|
baseRepo: repo,
|
|
targetBranch: expectedStagingForkBranch,
|
|
targetRepo: fork,
|
|
expectedCommits: [{
|
|
message: `release: cut the v${expectedVersion} release`,
|
|
files: ['package.json', 'CHANGELOG.md'],
|
|
}],
|
|
}),
|
|
'Expected release staging branch to be created in fork.');
|
|
|
|
expect(gitClient.pushed[1])
|
|
.toEqual(
|
|
getBranchPushMatcher({
|
|
baseBranch: 'master',
|
|
baseRepo: repo,
|
|
targetBranch: expectedCherryPickForkBranch,
|
|
targetRepo: fork,
|
|
expectedCommits: [{
|
|
message: `docs: release notes for the v${expectedVersion} release`,
|
|
files: ['CHANGELOG.md'],
|
|
}],
|
|
}),
|
|
'Expected cherry-pick branch to be created in fork.');
|
|
|
|
expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1);
|
|
expect(npm.runNpmPublish).toHaveBeenCalledTimes(2);
|
|
expect(npm.runNpmPublish)
|
|
.toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, expectedNpmDistTag, undefined);
|
|
expect(npm.runNpmPublish)
|
|
.toHaveBeenCalledWith(`${testTmpDir}/dist/pkg2`, expectedNpmDistTag, undefined);
|
|
}
|
|
|
|
/** Fakes a NPM package query API request for the given package. */
|
|
export function fakeNpmPackageQueryRequest(pkgName: string, data: Partial<NpmPackageInfo>) {
|
|
_npmPackageInfoCache[pkgName] =
|
|
Promise.resolve({'dist-tags': {}, versions: {}, time: {}, ...data});
|
|
}
|