backport
Version:
A CLI tool that automates the process of backporting commits
495 lines (491 loc) • 21 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const promises_1 = __importDefault(require("fs/promises"));
const os_1 = __importDefault(require("os"));
const nock_1 = __importDefault(require("nock"));
const git = __importStar(require("../lib/git"));
const logger = __importStar(require("../lib/logger"));
const mockConfigFiles_1 = require("../test/mockConfigFiles");
const nockHelpers_1 = require("../test/nockHelpers");
const options_1 = require("./options");
const defaultConfigs = {
projectConfig: {
// use localhost to avoid CORS issues with nock
githubApiBaseUrlV4: 'http://localhost/graphql',
repoOwner: 'elastic',
repoName: 'kibana',
targetBranchChoices: ['7.9', '8.0'],
},
globalConfig: { accessToken: 'abc', editor: 'code' },
};
describe('getOptions', () => {
afterEach(() => {
jest.clearAllMocks();
nock_1.default.cleanAll();
});
beforeEach(() => {
(0, mockConfigFiles_1.mockConfigFiles)(defaultConfigs);
jest.spyOn(os_1.default, 'homedir').mockReturnValue('/myHomeDir');
jest.spyOn(promises_1.default, 'writeFile').mockResolvedValue();
jest.spyOn(promises_1.default, 'chmod').mockResolvedValue();
});
describe('should throw', () => {
beforeEach(() => {
mockGithubConfigOptions({});
});
it('when accessToken is missing', async () => {
(0, mockConfigFiles_1.mockConfigFiles)({
projectConfig: defaultConfigs.projectConfig,
globalConfig: { accessToken: undefined },
});
await expect(() => (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
})).rejects.toThrowErrorMatchingInlineSnapshot(`
"Please update your config file: "/myHomeDir/.backport/config.json".
It must contain a valid "accessToken".
Read more: https://github.com/sorenlouv/backport/blob/main/docs/config-file-options.md#global-config-backportconfigjson"
`);
});
it('when `targetBranches`, `targetBranchChoices` and `branchLabelMapping` are all empty', async () => {
mockProjectConfig({
targetBranches: undefined,
targetBranchChoices: undefined,
branchLabelMapping: undefined,
});
await expect(() => (0, options_1.getOptions)({ optionsFromCliArgs: {}, optionsFromModule: {} })).rejects.toThrowErrorMatchingInlineSnapshot(`
"Please specify a target branch: "--branch 6.1".
Read more: https://github.com/sorenlouv/backport/blob/main/docs/config-file-options.md#project-config-backportrcjson"
`);
});
describe('whe option is an empty string', () => {
it('throws for "username"', async () => {
await expect(() => (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: { repoForkOwner: '', author: 'sorenlouv' },
})).rejects.toThrowErrorMatchingInlineSnapshot(`""repoForkOwner" cannot be empty!"`);
});
it('throws for "author"', async () => {
await expect(() => (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: { author: '' },
})).rejects.toThrowErrorMatchingInlineSnapshot(`""author" cannot be empty!"`);
});
it('throws for "accessToken"', async () => {
await expect(() => (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: { accessToken: '' },
})).rejects.toThrowErrorMatchingInlineSnapshot(`
"Please update your config file: "/myHomeDir/.backport/config.json".
It must contain a valid "accessToken".
Read more: https://github.com/sorenlouv/backport/blob/main/docs/config-file-options.md#global-config-backportconfigjson"
`);
});
});
describe('when repoName and repoOwner are missing', () => {
beforeEach(() => {
mockProjectConfig({ repoName: undefined, repoOwner: undefined });
});
it('should throw if there are no remotes', async () => {
jest.spyOn(git, 'getRepoInfoFromGitRemotes').mockResolvedValue([]);
await expect(() => (0, options_1.getOptions)({ optionsFromCliArgs: {}, optionsFromModule: {} })).rejects.toThrowErrorMatchingInlineSnapshot(`
"Please specify a repository: "--repo elastic/kibana".
Read more: https://github.com/sorenlouv/backport/blob/main/docs/config-file-options.md#project-config-backportrcjson"
`);
});
it('should get repoName from the remote', async () => {
mockRepoOwnerAndName({
childRepoOwner: 'sorenlouv',
parentRepoOwner: 'elastic',
repoName: 'kibana',
});
jest
.spyOn(git, 'getRepoInfoFromGitRemotes')
.mockResolvedValue([{ repoName: 'kibana', repoOwner: 'sorenlouv' }]);
const options = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(options.repoName).toBe('kibana');
expect(options.repoOwner).toBe('elastic');
});
});
});
it('reads options from remote config', async () => {
mockGithubConfigOptions({ hasRemoteConfig: true });
const options = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(options.branchLabelMapping).toEqual({
'^v8.2.0$': 'option-from-remote',
});
expect(options.autoMergeMethod).toEqual('rebase');
});
it('should ensure that "backport" branch does not exist', async () => {
mockGithubConfigOptions({ hasBackportBranch: true });
await expect((0, options_1.getOptions)({ optionsFromCliArgs: {}, optionsFromModule: {} })).rejects.toThrow('You must delete the branch "backport" to continue. See https://github.com/sorenlouv/backport/issues/155 for details');
});
it('should merge config options and module options', async () => {
mockGithubConfigOptions({});
const myFn = async () => true;
const options = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: { autoFixConflicts: myFn },
});
expect(options.autoFixConflicts).toBe(myFn);
});
it('should call setAccessToken', async () => {
mockGithubConfigOptions({});
await (0, options_1.getOptions)({ optionsFromCliArgs: {}, optionsFromModule: {} });
expect(logger.setAccessToken).toHaveBeenCalledTimes(1);
});
it('should return options', async () => {
mockGithubConfigOptions({
viewerLogin: 'john.diller',
defaultBranchRef: 'default-branch-from-github',
});
const options = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(options).toEqual({
accessToken: 'abc',
assignees: [],
authenticatedUsername: 'john.diller',
author: 'john.diller',
autoAssign: false,
autoMerge: false,
autoMergeMethod: 'merge',
backportBinary: 'backport',
cherrypickRef: true,
commitConflicts: false,
commitPaths: [],
cwd: expect.any(String),
dateSince: null,
dateUntil: null,
details: false,
draft: false,
editor: 'code',
fork: true,
gitHostname: 'github.com',
githubApiBaseUrlV4: 'http://localhost/graphql',
interactive: true,
isRepoPrivate: false,
maxNumber: 10,
multipleBranches: true,
multipleCommits: false,
noUnmergedBackportsHelp: false,
noVerify: true,
publishStatusCommentOnAbort: false,
publishStatusCommentOnFailure: false,
publishStatusCommentOnSuccess: true,
repoForkOwner: 'john.diller',
repoName: 'kibana',
repoOwner: 'elastic',
resetAuthor: false,
reviewers: [],
signoff: false,
sourceBranch: 'default-branch-from-github',
sourcePRLabels: [],
copySourcePRLabels: false,
copySourcePRReviewers: false,
targetBranchChoices: ['7.9', '8.0'],
targetBranches: [],
targetPRLabels: [],
telemetry: true,
});
});
describe('sourceBranch', () => {
beforeEach(() => {
mockGithubConfigOptions({ defaultBranchRef: 'some-default-branch' });
});
it('uses the `defaultBranchRef` as default', async () => {
const options = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(options.sourceBranch).toBe('some-default-branch');
});
it('uses the sourceBranch given via cli instead of `defaultBranchRef`', async () => {
const options = await (0, options_1.getOptions)({
optionsFromCliArgs: { sourceBranch: 'cli-source-branch' },
optionsFromModule: {},
});
expect(options.sourceBranch).toBe('cli-source-branch');
});
});
describe('fork', () => {
beforeEach(() => {
mockGithubConfigOptions({});
});
it('is enabled by default', async () => {
const { fork } = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(fork).toBe(true);
});
it('can be disabled via cli', async () => {
const { fork } = await (0, options_1.getOptions)({
optionsFromCliArgs: { fork: false },
optionsFromModule: {},
});
expect(fork).toBe(false);
});
it('can be disabled via config file', async () => {
mockProjectConfig({ fork: false });
const { fork } = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(fork).toBe(false);
});
});
describe('reviewers', () => {
beforeEach(() => {
mockGithubConfigOptions({});
});
it('can be set via cli', async () => {
const { reviewers } = await (0, options_1.getOptions)({
optionsFromCliArgs: { reviewers: ['peter'] },
optionsFromModule: {},
});
expect(reviewers).toEqual(['peter']);
});
it('can be set via config file', async () => {
mockProjectConfig({ reviewers: ['john'] });
const { reviewers } = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(reviewers).toEqual(['john']);
});
});
describe('mainline', () => {
beforeEach(() => {
mockGithubConfigOptions({});
});
it('is not enabled by default', async () => {
const { mainline } = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(mainline).toBe(undefined);
});
it('can be set via `--mainline` flag', async () => {
const { mainline } = await (0, options_1.getOptions)({
optionsFromCliArgs: { mainline: 1 },
optionsFromModule: {},
});
expect(mainline).toBe(1);
});
it('accepts numeric values', async () => {
const { mainline } = await (0, options_1.getOptions)({
optionsFromCliArgs: { mainline: 2 },
optionsFromModule: {},
});
expect(mainline).toBe(2);
});
});
describe('author', () => {
beforeEach(() => {
mockGithubConfigOptions({ viewerLogin: 'billy.bob' });
});
it('defaults to authenticated user', async () => {
const { author } = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(author).toBe('billy.bob');
});
it('can be overridden via `--author` flag', async () => {
const { author } = await (0, options_1.getOptions)({
optionsFromCliArgs: { author: 'john.doe' },
optionsFromModule: {},
});
expect(author).toBe('john.doe');
});
it('can be reset via config file (similar to `--all` flag)', async () => {
mockProjectConfig({ author: null });
const { author } = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(author).toBe(null);
});
it('can be overridden via config file', async () => {
mockProjectConfig({ author: 'jane.doe' });
const { author } = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(author).toBe('jane.doe');
});
});
describe('access token scopes', () => {
it('throw if no scopes are granted', async () => {
mockGithubConfigOptions({ headers: { 'x-oauth-scopes': '' } });
await expect((0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
})).rejects.toThrow('You must grant the "repo" or "public_repo" scope to your personal access token');
});
it('should throw if only `public_repo` scope is granted but the repo is private', async () => {
mockGithubConfigOptions({
isRepoPrivate: true,
headers: { 'x-oauth-scopes': 'public_repo' },
});
await expect((0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
})).rejects.toThrow('You must grant the "repo" scope to your personal access token');
});
it('should not throw if `public_repo` scope is granted and the repo is public', async () => {
mockGithubConfigOptions({
isRepoPrivate: false,
headers: { 'x-oauth-scopes': 'public_repo' },
});
const options = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(options).toBeDefined();
});
});
describe('cherrypickRef', () => {
beforeEach(() => {
mockGithubConfigOptions({});
});
it('should default to true', async () => {
const { cherrypickRef } = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(cherrypickRef).toBe(true);
});
it('should be settable via config file', async () => {
mockProjectConfig({ cherrypickRef: false });
const { cherrypickRef } = await (0, options_1.getOptions)({
optionsFromCliArgs: {},
optionsFromModule: {},
});
expect(cherrypickRef).toBe(false);
});
it('cli args overwrites config', async () => {
mockProjectConfig({ cherrypickRef: false });
const { cherrypickRef } = await (0, options_1.getOptions)({
optionsFromCliArgs: { cherrypickRef: true },
optionsFromModule: {},
});
expect(cherrypickRef).toBe(true);
});
});
});
function mockProjectConfig(projectConfig) {
return (0, mockConfigFiles_1.mockConfigFiles)({
globalConfig: { accessToken: 'abc' },
projectConfig: {
// use localhost to avoid CORS issues with nock
githubApiBaseUrlV4: 'http://localhost/graphql',
repoOwner: 'elastic',
repoName: 'kibana',
targetBranchChoices: ['7.9', '8.0'],
...projectConfig,
},
});
}
function mockGithubConfigOptions({ viewerLogin = 'DO_NOT_USE-sorenlouv', defaultBranchRef = 'DO_NOT_USE-default-branch-name', hasBackportBranch, hasRemoteConfig, isRepoPrivate = false, headers = { 'x-oauth-scopes': 'repo' }, }) {
return (0, nockHelpers_1.mockUrqlRequest)({
operationName: 'GithubConfigOptions',
headers,
body: {
data: {
viewer: {
login: viewerLogin,
},
repository: {
isPrivate: isRepoPrivate,
illegalBackportBranch: hasBackportBranch ? { id: 'foo' } : null,
defaultBranchRef: {
name: defaultBranchRef,
target: {
__typename: 'Commit',
remoteConfigHistory: {
edges: hasRemoteConfig
? [
{
remoteConfig: {
committedDate: '2020-08-15T00:00:00.000Z',
file: {
__typename: 'TreeEntry',
object: {
__typename: 'Blob',
text: JSON.stringify({
autoMergeMethod: 'rebase',
branchLabelMapping: {
'^v8.2.0$': 'option-from-remote',
},
}),
},
},
},
},
]
: [],
},
},
},
},
},
},
});
}
function mockRepoOwnerAndName({ repoName, parentRepoOwner, childRepoOwner, }) {
return (0, nockHelpers_1.mockUrqlRequest)({
operationName: 'RepoOwnerAndName',
body: {
data: {
repository: {
isFork: true,
name: repoName,
owner: {
login: childRepoOwner,
},
parent: {
owner: {
login: parentRepoOwner,
},
},
},
},
},
});
}
//# sourceMappingURL=options.test.js.map