diff --git a/src/backportRun.ts b/src/backportRun.ts index 093339dc..3fdb6186 100755 --- a/src/backportRun.ts +++ b/src/backportRun.ts @@ -1,5 +1,4 @@ import chalk from 'chalk'; -import ora from 'ora'; import yargsParser from 'yargs-parser'; import { ConfigFileOptions } from './options/ConfigOptions'; import { getOptions, ValidConfigOptions } from './options/options'; @@ -12,6 +11,7 @@ import { consoleLog, initLogger, logger } from './services/logger'; import { Commit } from './services/sourceCommit/parseSourceCommit'; import { getCommits } from './ui/getCommits'; import { getTargetBranches } from './ui/getTargetBranches'; +import { ora } from './ui/ora'; export type BackportResponse = | { @@ -23,6 +23,7 @@ export type BackportResponse = status: 'failure'; commits: Commit[]; error: Error | HandledError; + errorMessage: string; }; export async function backportRun( @@ -36,7 +37,7 @@ export async function backportRun( initLogger({ ci, logFilePath }); // don't show spinner for yargs commands that exit the process without stopping the spinner first - const spinner = ora(); + const spinner = ora(ci); if (!argv.help && !argv.version && !argv.v) { spinner.start('Initializing...'); @@ -78,6 +79,7 @@ export async function backportRun( status: 'failure', commits, error: e, + errorMessage: e.message, }; if (options) { diff --git a/src/entrypoint.cli.ts b/src/entrypoint.cli.ts index b670573f..a91e2b90 100644 --- a/src/entrypoint.cli.ts +++ b/src/entrypoint.cli.ts @@ -1,6 +1,15 @@ #!/usr/bin/env node +import yargsParser from 'yargs-parser'; import { backportRun } from './backportRun'; +import { ConfigFileOptions } from './entrypoint.module'; const processArgs = process.argv.slice(2); // this is the entrypoint when running from command line -backportRun(processArgs); +backportRun(processArgs).then((backportResponse) => { + const argv = yargsParser(processArgs) as ConfigFileOptions; + const ci = argv.ci; + if (ci) { + // eslint-disable-next-line no-console + console.log(JSON.stringify(backportResponse)); + } +}); diff --git a/src/options/ConfigOptions.ts b/src/options/ConfigOptions.ts index 0a9c6e3b..52494684 100644 --- a/src/options/ConfigOptions.ts +++ b/src/options/ConfigOptions.ts @@ -71,7 +71,7 @@ export type ConfigFileOptions = Options & version: boolean; v: boolean; - // only allowed in project config. Not allowed in CI and denoted in plural (historicalBranchLabelMappings) in options from Github + // only allowed in project config. Not allowed in CI mode branchLabelMapping: Record; /** diff --git a/src/options/options.test.ts b/src/options/options.test.ts index 4a4c4779..6634aea0 100644 --- a/src/options/options.test.ts +++ b/src/options/options.test.ts @@ -132,6 +132,16 @@ describe('getOptions', () => { }); }); + it('reads options from remote config', async () => { + mockGithubConfigOptions({ hasRemoteConfig: true }); + const options = await getOptions([], {}); + 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(getOptions([], {})).rejects.toThrowError( @@ -158,12 +168,6 @@ describe('getOptions', () => { mockGithubConfigOptions({ viewerLogin: 'john.diller', defaultBranchRef: 'default-branch-from-github', - historicalMappings: [ - { - committedDate: '2022-01-02T20:52:45.173Z', - branchLabelMapping: { foo: 'bar' }, - }, - ], }); const options = await getOptions([], {}); @@ -176,9 +180,6 @@ describe('getOptions', () => { autoMerge: false, autoMergeMethod: 'merge', backportBinary: 'backport', - branchLabelMapping: { - foo: 'bar', - }, cherrypickRef: true, ci: false, commitPaths: [], @@ -190,12 +191,6 @@ describe('getOptions', () => { fork: true, gitHostname: 'github.com', githubApiBaseUrlV4: 'http://localhost/graphql', - historicalBranchLabelMappings: [ - { - branchLabelMapping: { foo: 'bar' }, - committedDate: '2022-01-02T20:52:45.173Z', - }, - ], maxNumber: 10, multipleBranches: true, multipleCommits: false, @@ -373,15 +368,12 @@ function mockGithubConfigOptions({ viewerLogin = 'DO_NOT_USE-sqren', defaultBranchRef = 'DO_NOT_USE-default-branch-name', hasBackportBranch, - historicalMappings = [], + hasRemoteConfig, }: { viewerLogin?: string; defaultBranchRef?: string; hasBackportBranch?: boolean; - historicalMappings?: Array<{ - committedDate: string; - branchLabelMapping: Record; - }>; + hasRemoteConfig?: boolean; }) { return mockGqlRequest({ name: 'GithubConfigOptions', @@ -396,23 +388,26 @@ function mockGithubConfigOptions({ defaultBranchRef: { name: defaultBranchRef, target: { - history: { - edges: historicalMappings.map( - ({ committedDate, branchLabelMapping }) => { - return { - remoteConfig: { - committedDate, - file: { - object: { - text: JSON.stringify({ - branchLabelMapping, - }), + remoteConfigHistory: { + edges: hasRemoteConfig + ? [ + { + remoteConfig: { + committedDate: '2020-08-15T00:00:00.000Z', + file: { + object: { + text: JSON.stringify({ + autoMergeMethod: 'rebase', + branchLabelMapping: { + '^v8.2.0$': 'option-from-remote', + }, + } as ConfigFileOptions), + }, }, }, }, - }; - } - ), + ] + : [], }, }, }, diff --git a/src/runSequentially.test.ts b/src/runSequentially.test.ts index ae1895c6..01b7a81d 100644 --- a/src/runSequentially.test.ts +++ b/src/runSequentially.test.ts @@ -62,7 +62,6 @@ describe('runSequentially', () => { gitHostname: 'github.com', githubApiBaseUrlV3: 'https://api.github.com', githubApiBaseUrlV4: 'http://localhost/graphql', // Using localhost to avoid CORS issues when making requests (related to nock and jsdom) - historicalBranchLabelMappings: [], mainline: undefined, maxNumber: 10, multipleBranches: false, diff --git a/src/services/HandledError.ts b/src/services/HandledError.ts index 30e6aa3f..dc339da8 100644 --- a/src/services/HandledError.ts +++ b/src/services/HandledError.ts @@ -30,9 +30,11 @@ function getMessage(errorContext: ErrorContext | string): string { export class HandledError extends Error { errorContext?: ErrorContext; constructor(errorContext: ErrorContext | string) { - super(getMessage(errorContext)); + const message = getMessage(errorContext); + super(message); Error.captureStackTrace(this, HandledError); this.name = 'HandledError'; + this.message = message; if (typeof errorContext !== 'string') { this.errorContext = errorContext; diff --git a/src/services/git.ts b/src/services/git.ts index 6da945aa..a91d13a4 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -1,7 +1,7 @@ import { resolve as pathResolve } from 'path'; import { uniq, isEmpty } from 'lodash'; -import ora from 'ora'; import { ValidConfigOptions } from '../options/options'; +import { ora } from '../ui/ora'; import { filterNil } from '../utils/filterEmpty'; import { HandledError } from './HandledError'; import { execAsCallback, exec } from './child-process-promisified'; @@ -354,7 +354,7 @@ export async function setCommitAuthor( options: ValidConfigOptions, author: string ) { - const spinner = ora(`Changing author to "${author}"`).start(); + const spinner = ora(options.ci, `Changing author to "${author}"`).start(); try { const res = await exec( `git commit --amend --no-edit --author "${author} <${author}@users.noreply.github.com>"`, @@ -380,7 +380,7 @@ export async function createBackportBranch({ targetBranch: string; backportBranch: string; }) { - const spinner = ora('Pulling latest changes').start(); + const spinner = ora(options.ci, 'Pulling latest changes').start(); try { const res = await exec( @@ -413,7 +413,7 @@ export async function deleteBackportBranch({ options: ValidConfigOptions; backportBranch: string; }) { - const spinner = ora().start(); + const spinner = ora(options.ci).start(); await exec( `git reset --hard && git checkout ${options.sourceBranch} && git branch -D ${backportBranch}`, @@ -439,6 +439,7 @@ export async function pushBackportBranch({ }) { const repoForkOwner = getRepoForkOwner(options); const spinner = ora( + options.ci, `Pushing branch "${repoForkOwner}:${backportBranch}"` ).start(); diff --git a/src/services/github/v3/addAssigneesToPullRequest.ts b/src/services/github/v3/addAssigneesToPullRequest.ts index 89fb6c45..795ab87e 100644 --- a/src/services/github/v3/addAssigneesToPullRequest.ts +++ b/src/services/github/v3/addAssigneesToPullRequest.ts @@ -1,6 +1,6 @@ import { Octokit } from '@octokit/rest'; -import ora from 'ora'; import { ValidConfigOptions } from '../../../options/options'; +import { ora } from '../../../ui/ora'; import { logger } from '../../logger'; export async function addAssigneesToPullRequest( @@ -10,6 +10,7 @@ export async function addAssigneesToPullRequest( repoOwner, accessToken, autoAssign, + ci, }: ValidConfigOptions, pullNumber: number, assignees: string[] @@ -18,7 +19,7 @@ export async function addAssigneesToPullRequest( ? `Self-assigning to #${pullNumber}` : `Adding assignees to #${pullNumber}: ${assignees.join(', ')}`; logger.info(text); - const spinner = ora(text).start(); + const spinner = ora(ci, text).start(); try { const octokit = new Octokit({ diff --git a/src/services/github/v3/addLabelsToPullRequest.ts b/src/services/github/v3/addLabelsToPullRequest.ts index e9a4cc75..de8d3c79 100644 --- a/src/services/github/v3/addLabelsToPullRequest.ts +++ b/src/services/github/v3/addLabelsToPullRequest.ts @@ -1,16 +1,22 @@ import { Octokit } from '@octokit/rest'; -import ora from 'ora'; import { ValidConfigOptions } from '../../../options/options'; +import { ora } from '../../../ui/ora'; import { logger } from '../../logger'; export async function addLabelsToPullRequest( - { githubApiBaseUrlV3, repoName, repoOwner, accessToken }: ValidConfigOptions, + { + githubApiBaseUrlV3, + repoName, + repoOwner, + accessToken, + ci, + }: ValidConfigOptions, pullNumber: number, labels: string[] ): Promise { const text = `Adding labels: ${labels.join(', ')}`; logger.info(text); - const spinner = ora(text).start(); + const spinner = ora(ci, text).start(); try { const octokit = new Octokit({ diff --git a/src/services/github/v3/addReviewersToPullRequest.ts b/src/services/github/v3/addReviewersToPullRequest.ts index 4735da1e..3fcbed84 100644 --- a/src/services/github/v3/addReviewersToPullRequest.ts +++ b/src/services/github/v3/addReviewersToPullRequest.ts @@ -1,16 +1,22 @@ import { Octokit } from '@octokit/rest'; -import ora from 'ora'; import { ValidConfigOptions } from '../../../options/options'; +import { ora } from '../../../ui/ora'; import { logger } from '../../logger'; export async function addReviewersToPullRequest( - { githubApiBaseUrlV3, repoName, repoOwner, accessToken }: ValidConfigOptions, + { + githubApiBaseUrlV3, + repoName, + repoOwner, + accessToken, + ci, + }: ValidConfigOptions, pullNumber: number, reviewers: string[] ) { const text = `Adding reviewers: ${reviewers}`; logger.info(text); - const spinner = ora(text).start(); + const spinner = ora(ci, text).start(); try { const octokit = new Octokit({ diff --git a/src/services/github/v3/createPullRequest.ts b/src/services/github/v3/createPullRequest.ts index 670ab2c2..73d317df 100644 --- a/src/services/github/v3/createPullRequest.ts +++ b/src/services/github/v3/createPullRequest.ts @@ -1,6 +1,6 @@ import { Octokit } from '@octokit/rest'; -import ora from 'ora'; import { ValidConfigOptions } from '../../../options/options'; +import { ora } from '../../../ui/ora'; import { PACKAGE_VERSION } from '../../../utils/packageVersion'; import { HandledError } from '../../HandledError'; import { logger } from '../../logger'; @@ -35,7 +35,7 @@ export async function createPullRequest({ ); const { accessToken, githubApiBaseUrlV3 } = options; - const spinner = ora(`Creating pull request`).start(); + const spinner = ora(options.ci, `Creating pull request`).start(); try { const octokit = new Octokit({ diff --git a/src/services/github/v4/FetchPullRequestId.ts b/src/services/github/v4/FetchPullRequestId.ts index a0013bff..e4e27746 100644 --- a/src/services/github/v4/FetchPullRequestId.ts +++ b/src/services/github/v4/FetchPullRequestId.ts @@ -1,3 +1,4 @@ +import gql from 'graphql-tag'; import { ValidConfigOptions } from '../../../options/options'; import { apiRequestV4 } from './apiRequestV4'; @@ -13,7 +14,7 @@ export async function fetchPullRequestId( ) { const { accessToken, githubApiBaseUrlV4, repoName, repoOwner } = options; - const prQuery = /* GraphQL */ ` + const prQuery = gql` query PullRequestId( $repoOwner: String! $repoName: String! diff --git a/src/services/github/v4/apiRequestV4.test.ts b/src/services/github/v4/apiRequestV4.test.ts index 85f62107..8e6210d6 100644 --- a/src/services/github/v4/apiRequestV4.test.ts +++ b/src/services/github/v4/apiRequestV4.test.ts @@ -1,3 +1,4 @@ +import gql from 'graphql-tag'; import nock from 'nock'; import { mockGqlRequest } from '../../../test/nockHelpers'; import { HandledError } from '../../HandledError'; @@ -15,28 +16,42 @@ describe('apiRequestV4', () => { commitsByAuthorCalls = mockGqlRequest({ name: 'MyQuery', statusCode: 200, - body: { data: { hello: 'world' } }, + body: { data: { viewer: { login: 'sqren' } } }, }); res = await apiRequestV4({ accessToken: 'myAccessToken', githubApiBaseUrlV4: 'http://localhost/graphql', - query: 'query MyQuery{ foo }', + query: gql` + query MyQuery { + viewer { + login + } + } + `, variables: { foo: 'bar' }, }); }); it('should return correct response', async () => { - expect(res).toEqual({ hello: 'world' }); + expect(res).toEqual({ viewer: { login: 'sqren' } }); }); it('should call with correct args', async () => { - expect(commitsByAuthorCalls).toEqual([ - { - query: 'query MyQuery{ foo }', - variables: { foo: 'bar' }, - }, - ]); + expect(commitsByAuthorCalls).toMatchInlineSnapshot(` + Array [ + Object { + "query": "query MyQuery { + viewer { + login + } + }", + "variables": Object { + "foo": "bar", + }, + }, + ] + `); }); }); @@ -56,7 +71,13 @@ describe('apiRequestV4', () => { apiRequestV4({ accessToken: 'myAccessToken', githubApiBaseUrlV4: 'http://localhost/graphql', - query: 'query MyQuery{ foo }', + query: gql` + query MyQuery { + viewer { + login + } + } + `, variables: { foo: 'bar', }, @@ -81,7 +102,13 @@ describe('apiRequestV4', () => { apiRequestV4({ accessToken: 'myAccessToken', githubApiBaseUrlV4: 'http://localhost/graphql', - query: 'query MyQuery{ foo }', + query: gql` + query MyQuery { + viewer { + login + } + } + `, variables: { foo: 'bar', }, diff --git a/src/services/github/v4/apiRequestV4.ts b/src/services/github/v4/apiRequestV4.ts index 7fd4904f..03a98230 100644 --- a/src/services/github/v4/apiRequestV4.ts +++ b/src/services/github/v4/apiRequestV4.ts @@ -1,5 +1,6 @@ import axios, { AxiosResponse } from 'axios'; -import gql from 'graphql-tag'; +import { DocumentNode } from 'graphql'; +import { print } from 'graphql/language/printer'; import { HandledError } from '../../HandledError'; import { logger } from '../../logger'; @@ -28,13 +29,13 @@ export async function apiRequestV4({ }: { githubApiBaseUrlV4?: string; accessToken: string; - query: string; + query: DocumentNode; variables?: Variables; }) { try { const response = await axios.post>( githubApiBaseUrlV4, - { query, variables }, + { query: print(query), variables }, { headers: { 'Content-Type': 'application/json', @@ -87,12 +88,12 @@ function addDebugLogs({ didSucceed, }: { githubApiBaseUrlV4: string; - query: string; + query: DocumentNode; variables?: Variables; axiosResponse: AxiosResponse; didSucceed: boolean; }) { - const gqlQueryName = getGqlQueryName(query); + const gqlQueryName = getQueryName(query); logger.info( `POST ${githubApiBaseUrlV4} (name:${gqlQueryName}, status: ${axiosResponse.status})` ); @@ -120,8 +121,7 @@ export class GithubV4Exception extends Error { } } -function getGqlQueryName(query: string) { - const ast = gql(query); +export function getQueryName(query: DocumentNode): string { //@ts-expect-error - return ast.definitions[0].name.value; + return query.definitions[0].name?.value; } diff --git a/src/services/github/v4/disablePullRequestAutoMerge.ts b/src/services/github/v4/disablePullRequestAutoMerge.ts index cd444c5e..d0036583 100644 --- a/src/services/github/v4/disablePullRequestAutoMerge.ts +++ b/src/services/github/v4/disablePullRequestAutoMerge.ts @@ -1,3 +1,4 @@ +import gql from 'graphql-tag'; import { ValidConfigOptions } from '../../../options/options'; import { fetchPullRequestId } from './FetchPullRequestId'; import { apiRequestV4 } from './apiRequestV4'; @@ -13,7 +14,7 @@ export async function disablePullRequestAutoMerge( const { accessToken, githubApiBaseUrlV4 } = options; const pullRequestId = await fetchPullRequestId(options, pullNumber); - const query = /* GraphQL */ ` + const query = gql` mutation DisablePullRequestAutoMerge($pullRequestId: ID!) { disablePullRequestAutoMerge(input: { pullRequestId: $pullRequestId }) { pullRequest { diff --git a/src/services/github/v4/enablePullRequestAutoMerge.ts b/src/services/github/v4/enablePullRequestAutoMerge.ts index 20ee73f0..3cdb47f5 100644 --- a/src/services/github/v4/enablePullRequestAutoMerge.ts +++ b/src/services/github/v4/enablePullRequestAutoMerge.ts @@ -1,5 +1,6 @@ -import ora from 'ora'; +import gql from 'graphql-tag'; import { ValidConfigOptions } from '../../../options/options'; +import { ora } from '../../../ui/ora'; import { logger } from '../../logger'; import { fetchPullRequestId } from './FetchPullRequestId'; import { apiRequestV4 } from './apiRequestV4'; @@ -19,14 +20,14 @@ export async function enablePullRequestAutoMerge( } = options; const text = `Enabling auto merging via ${options.autoMergeMethod}`; logger.info(text); - const spinner = ora(text).start(); + const spinner = ora(options.ci, text).start(); const pullRequestId = await fetchPullRequestId( options, targetPullRequestNumber ); - const query = /* GraphQL */ ` + const query = gql` mutation EnablePullRequestAutoMerge( $pullRequestId: ID! $mergeMethod: PullRequestMergeMethod! diff --git a/src/services/github/v4/fetchAuthorId.ts b/src/services/github/v4/fetchAuthorId.ts index 943cf83e..d9ed0720 100644 --- a/src/services/github/v4/fetchAuthorId.ts +++ b/src/services/github/v4/fetchAuthorId.ts @@ -1,3 +1,4 @@ +import gql from 'graphql-tag'; import { apiRequestV4 } from './apiRequestV4'; export interface AuthorIdResponse { @@ -17,7 +18,7 @@ export async function fetchAuthorId({ return null; } - const query = /* GraphQL */ ` + const query = gql` query AuthorId($author: String!) { user(login: $author) { id diff --git a/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitByPullNumber.private.test.ts.snap b/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitByPullNumber.private.test.ts.snap index 7f2dd09b..9064b99a 100644 --- a/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitByPullNumber.private.test.ts.snap +++ b/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitByPullNumber.private.test.ts.snap @@ -1,87 +1,70 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`fetchCommitByPullNumber snapshot request/response makes the right queries: Query: CommitByPullNumber 1`] = ` -" - query CommitByPullNumber( - $repoOwner: String! - $repoName: String! - $pullNumber: Int! - ) { - repository(owner: $repoOwner, name: $repoName) { - pullRequest(number: $pullNumber) { - mergeCommit { - ...SourceCommitWithTargetPullRequest - } - } +"query CommitByPullNumber($repoOwner: String!, $repoName: String!, $pullNumber: Int!) { + repository(owner: $repoOwner, name: $repoName) { + pullRequest(number: $pullNumber) { + mergeCommit { + ...SourceCommitWithTargetPullRequest } } + } +} - - fragment SourceCommitWithTargetPullRequest on Commit { - # Source Commit - repository { - name - owner { - login +fragment SourceCommitWithTargetPullRequest on Commit { + ...RemoteConfigHistory + repository { + name + owner { + login + } + } + sha: oid + message + committedDate + associatedPullRequests(first: 1) { + edges { + node { + url + number + labels(first: 50) { + nodes { + name + } } - } - sha: oid - message - committedDate - - # Source pull request: PR where source commit was merged in - associatedPullRequests(first: 1) { - edges { - node { - url - number - labels(first: 50) { - nodes { - name - } - } - baseRefName - - # source merge commit (the commit that actually went into the source branch) - mergeCommit { - sha: oid - message - } - - # (possible) backport pull requests referenced in the source pull request - timelineItems(last: 20, itemTypes: CROSS_REFERENCED_EVENT) { - edges { - node { - ... on CrossReferencedEvent { - targetPullRequest: source { - __typename - - # Target PRs (backport PRs) - ... on PullRequest { - # target merge commit: the backport commit that was merged into the target branch - targetMergeCommit: mergeCommit { - sha: oid - message - } - repository { - name - owner { - login - } - } - url - title - state - baseRefName - number - commits(first: 20) { - edges { - node { - targetCommit: commit { - message - sha: oid - } - } + baseRefName + mergeCommit { + sha: oid + message + } + timelineItems(last: 20, itemTypes: CROSS_REFERENCED_EVENT) { + edges { + node { + ... on CrossReferencedEvent { + targetPullRequest: source { + __typename + ... on PullRequest { + targetMergeCommit: mergeCommit { + sha: oid + message + } + repository { + name + owner { + login + } + } + url + title + state + baseRefName + number + commits(first: 20) { + edges { + node { + targetCommit: commit { + message + sha: oid } } } @@ -94,8 +77,27 @@ exports[`fetchCommitByPullNumber snapshot request/response makes the right queri } } } - - " + } +} + +fragment RemoteConfigHistory on Commit { + remoteConfigHistory: history(first: 1, path: \\".backportrc.json\\") { + edges { + remoteConfig: node { + committedDate + file(path: \\".backportrc.json\\") { + ... on TreeEntry { + object { + ... on Blob { + text + } + } + } + } + } + } + } +}" `; exports[`fetchCommitByPullNumber snapshot request/response returns the correct response 1`] = ` diff --git a/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitBySha.private.test.ts.snap b/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitBySha.private.test.ts.snap index 107f614d..303ff84b 100644 --- a/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitBySha.private.test.ts.snap +++ b/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitBySha.private.test.ts.snap @@ -1,81 +1,68 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`fetchCommitBySha snapshot request/response makes the right queries: Query: CommitsBySha 1`] = ` -" - query CommitsBySha($repoOwner: String!, $repoName: String!, $sha: String!) { - repository(owner: $repoOwner, name: $repoName) { - object(expression: $sha) { - ...SourceCommitWithTargetPullRequest - } - } +"query CommitsBySha($repoOwner: String!, $repoName: String!, $sha: String!) { + repository(owner: $repoOwner, name: $repoName) { + object(expression: $sha) { + ...SourceCommitWithTargetPullRequest } + } +} - - fragment SourceCommitWithTargetPullRequest on Commit { - # Source Commit - repository { - name - owner { - login +fragment SourceCommitWithTargetPullRequest on Commit { + ...RemoteConfigHistory + repository { + name + owner { + login + } + } + sha: oid + message + committedDate + associatedPullRequests(first: 1) { + edges { + node { + url + number + labels(first: 50) { + nodes { + name + } } - } - sha: oid - message - committedDate - - # Source pull request: PR where source commit was merged in - associatedPullRequests(first: 1) { - edges { - node { - url - number - labels(first: 50) { - nodes { - name - } - } - baseRefName - - # source merge commit (the commit that actually went into the source branch) - mergeCommit { - sha: oid - message - } - - # (possible) backport pull requests referenced in the source pull request - timelineItems(last: 20, itemTypes: CROSS_REFERENCED_EVENT) { - edges { - node { - ... on CrossReferencedEvent { - targetPullRequest: source { - __typename - - # Target PRs (backport PRs) - ... on PullRequest { - # target merge commit: the backport commit that was merged into the target branch - targetMergeCommit: mergeCommit { - sha: oid - message - } - repository { - name - owner { - login - } - } - url - title - state - baseRefName - number - commits(first: 20) { - edges { - node { - targetCommit: commit { - message - sha: oid - } - } + baseRefName + mergeCommit { + sha: oid + message + } + timelineItems(last: 20, itemTypes: CROSS_REFERENCED_EVENT) { + edges { + node { + ... on CrossReferencedEvent { + targetPullRequest: source { + __typename + ... on PullRequest { + targetMergeCommit: mergeCommit { + sha: oid + message + } + repository { + name + owner { + login + } + } + url + title + state + baseRefName + number + commits(first: 20) { + edges { + node { + targetCommit: commit { + message + sha: oid } } } @@ -88,8 +75,27 @@ exports[`fetchCommitBySha snapshot request/response makes the right queries: Que } } } - - " + } +} + +fragment RemoteConfigHistory on Commit { + remoteConfigHistory: history(first: 1, path: \\".backportrc.json\\") { + edges { + remoteConfig: node { + committedDate + file(path: \\".backportrc.json\\") { + ... on TreeEntry { + object { + ... on Blob { + text + } + } + } + } + } + } + } +}" `; exports[`fetchCommitBySha snapshot request/response returns the correct response 1`] = ` diff --git a/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitsByAuthor.private.test.ts.snap b/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitsByAuthor.private.test.ts.snap index 2ce3256f..f7dea0b8 100644 --- a/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitsByAuthor.private.test.ts.snap +++ b/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitsByAuthor.private.test.ts.snap @@ -1,116 +1,92 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`fetchCommitsByAuthor snapshot request/response makes the right queries: Query: AuthorId 1`] = ` -" - query AuthorId($author: String!) { - user(login: $author) { - id - } - } - " +"query AuthorId($author: String!) { + user(login: $author) { + id + } +}" `; exports[`fetchCommitsByAuthor snapshot request/response makes the right queries: Query: CommitsByAuthor 1`] = ` -" - query CommitsByAuthor( - $repoOwner: String! - $repoName: String! - $maxNumber: Int! - $sourceBranch: String! - $authorId: ID - $commitPath: String - $dateSince: GitTimestamp - $dateUntil: GitTimestamp - ) { - repository(owner: $repoOwner, name: $repoName) { - ref(qualifiedName: $sourceBranch) { - target { - ... on Commit { - history( - first: $maxNumber - author: { id: $authorId } - path: $commitPath - since: $dateSince - until: $dateUntil - ) { - edges { - node { - ...SourceCommitWithTargetPullRequest - } - } +"query CommitsByAuthor($repoOwner: String!, $repoName: String!, $maxNumber: Int!, $sourceBranch: String!, $authorId: ID, $commitPath: String, $dateSince: GitTimestamp, $dateUntil: GitTimestamp) { + repository(owner: $repoOwner, name: $repoName) { + ref(qualifiedName: $sourceBranch) { + target { + ... on Commit { + history( + first: $maxNumber + author: {id: $authorId} + path: $commitPath + since: $dateSince + until: $dateUntil + ) { + edges { + node { + ...SourceCommitWithTargetPullRequest } } } } } } - - - fragment SourceCommitWithTargetPullRequest on Commit { - # Source Commit - repository { - name - owner { - login + } +} + +fragment SourceCommitWithTargetPullRequest on Commit { + ...RemoteConfigHistory + repository { + name + owner { + login + } + } + sha: oid + message + committedDate + associatedPullRequests(first: 1) { + edges { + node { + url + number + labels(first: 50) { + nodes { + name + } } - } - sha: oid - message - committedDate - - # Source pull request: PR where source commit was merged in - associatedPullRequests(first: 1) { - edges { - node { - url - number - labels(first: 50) { - nodes { - name - } - } - baseRefName - - # source merge commit (the commit that actually went into the source branch) - mergeCommit { - sha: oid - message - } - - # (possible) backport pull requests referenced in the source pull request - timelineItems(last: 20, itemTypes: CROSS_REFERENCED_EVENT) { - edges { - node { - ... on CrossReferencedEvent { - targetPullRequest: source { - __typename - - # Target PRs (backport PRs) - ... on PullRequest { - # target merge commit: the backport commit that was merged into the target branch - targetMergeCommit: mergeCommit { - sha: oid - message - } - repository { - name - owner { - login - } - } - url - title - state - baseRefName - number - commits(first: 20) { - edges { - node { - targetCommit: commit { - message - sha: oid - } - } + baseRefName + mergeCommit { + sha: oid + message + } + timelineItems(last: 20, itemTypes: CROSS_REFERENCED_EVENT) { + edges { + node { + ... on CrossReferencedEvent { + targetPullRequest: source { + __typename + ... on PullRequest { + targetMergeCommit: mergeCommit { + sha: oid + message + } + repository { + name + owner { + login + } + } + url + title + state + baseRefName + number + commits(first: 20) { + edges { + node { + targetCommit: commit { + message + sha: oid } } } @@ -123,8 +99,27 @@ exports[`fetchCommitsByAuthor snapshot request/response makes the right queries: } } } - - " + } +} + +fragment RemoteConfigHistory on Commit { + remoteConfigHistory: history(first: 1, path: \\".backportrc.json\\") { + edges { + remoteConfig: node { + committedDate + file(path: \\".backportrc.json\\") { + ... on TreeEntry { + object { + ... on Blob { + text + } + } + } + } + } + } + } +}" `; exports[`fetchCommitsByAuthor snapshot request/response returns the correct response 1`] = ` diff --git a/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitsByAuthor.test.ts.snap b/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitsByAuthor.test.ts.snap index 31524018..8a08c5a7 100644 --- a/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitsByAuthor.test.ts.snap +++ b/src/services/github/v4/fetchCommits/__snapshots__/fetchCommitsByAuthor.test.ts.snap @@ -3,13 +3,11 @@ exports[`fetchCommitsByAuthor when commit has an associated pull request should call with correct args to fetch author id 1`] = ` Array [ Object { - "query": " - query AuthorId($author: String!) { - user(login: $author) { - id - } - } - ", + "query": "query AuthorId($author: String!) { + user(login: $author) { + id + } +}", "variables": Object { "author": "sqren", }, @@ -20,106 +18,84 @@ Array [ exports[`fetchCommitsByAuthor when commit has an associated pull request should call with correct args to fetch commits 1`] = ` Array [ Object { - "query": " - query CommitsByAuthor( - $repoOwner: String! - $repoName: String! - $maxNumber: Int! - $sourceBranch: String! - $authorId: ID - $commitPath: String - $dateSince: GitTimestamp - $dateUntil: GitTimestamp - ) { - repository(owner: $repoOwner, name: $repoName) { - ref(qualifiedName: $sourceBranch) { - target { - ... on Commit { - history( - first: $maxNumber - author: { id: $authorId } - path: $commitPath - since: $dateSince - until: $dateUntil - ) { - edges { - node { - ...SourceCommitWithTargetPullRequest - } - } + "query": "query CommitsByAuthor($repoOwner: String!, $repoName: String!, $maxNumber: Int!, $sourceBranch: String!, $authorId: ID, $commitPath: String, $dateSince: GitTimestamp, $dateUntil: GitTimestamp) { + repository(owner: $repoOwner, name: $repoName) { + ref(qualifiedName: $sourceBranch) { + target { + ... on Commit { + history( + first: $maxNumber + author: {id: $authorId} + path: $commitPath + since: $dateSince + until: $dateUntil + ) { + edges { + node { + ...SourceCommitWithTargetPullRequest } } } } } } + } +} - - fragment SourceCommitWithTargetPullRequest on Commit { - # Source Commit - repository { - name - owner { - login +fragment SourceCommitWithTargetPullRequest on Commit { + ...RemoteConfigHistory + repository { + name + owner { + login + } + } + sha: oid + message + committedDate + associatedPullRequests(first: 1) { + edges { + node { + url + number + labels(first: 50) { + nodes { + name + } } - } - sha: oid - message - committedDate - - # Source pull request: PR where source commit was merged in - associatedPullRequests(first: 1) { - edges { - node { - url - number - labels(first: 50) { - nodes { - name - } - } - baseRefName - - # source merge commit (the commit that actually went into the source branch) - mergeCommit { - sha: oid - message - } - - # (possible) backport pull requests referenced in the source pull request - timelineItems(last: 20, itemTypes: CROSS_REFERENCED_EVENT) { - edges { - node { - ... on CrossReferencedEvent { - targetPullRequest: source { - __typename - - # Target PRs (backport PRs) - ... on PullRequest { - # target merge commit: the backport commit that was merged into the target branch - targetMergeCommit: mergeCommit { - sha: oid - message - } - repository { - name - owner { - login - } - } - url - title - state - baseRefName - number - commits(first: 20) { - edges { - node { - targetCommit: commit { - message - sha: oid - } - } + baseRefName + mergeCommit { + sha: oid + message + } + timelineItems(last: 20, itemTypes: CROSS_REFERENCED_EVENT) { + edges { + node { + ... on CrossReferencedEvent { + targetPullRequest: source { + __typename + ... on PullRequest { + targetMergeCommit: mergeCommit { + sha: oid + message + } + repository { + name + owner { + login + } + } + url + title + state + baseRefName + number + commits(first: 20) { + edges { + node { + targetCommit: commit { + message + sha: oid } } } @@ -132,8 +108,27 @@ Array [ } } } - - ", + } +} + +fragment RemoteConfigHistory on Commit { + remoteConfigHistory: history(first: 1, path: \\".backportrc.json\\") { + edges { + remoteConfig: node { + committedDate + file(path: \\".backportrc.json\\") { + ... on TreeEntry { + object { + ... on Blob { + text + } + } + } + } + } + } + } +}", "variables": Object { "authorId": "myUserId", "commitPath": null, diff --git a/src/services/github/v4/fetchCommits/allFetchers.private.test.ts b/src/services/github/v4/fetchCommits/allFetchers.private.test.ts index beccd472..2dbbf54c 100644 --- a/src/services/github/v4/fetchCommits/allFetchers.private.test.ts +++ b/src/services/github/v4/fetchCommits/allFetchers.private.test.ts @@ -16,7 +16,6 @@ describe('allFetchers', () => { const commitsByAuthor = await fetchCommitsByAuthor({ accessToken: devAccessToken, author: 'sqren', - historicalBranchLabelMappings: [], maxNumber: 1, repoName: 'kibana', repoOwner: 'elastic', @@ -40,7 +39,6 @@ describe('allFetchers', () => { accessToken: devAccessToken, pullNumber: commitByAuthor.sourcePullRequest.number, sourceBranch: 'master', - historicalBranchLabelMappings: [], }); expect(commitByAuthor).toEqual(commitByPullNumber); @@ -53,7 +51,6 @@ describe('allFetchers', () => { accessToken: devAccessToken, sha: commitByAuthor.sourceCommit.sha, sourceBranch: 'main', - historicalBranchLabelMappings: [], }); expect(commitByAuthor).toEqual(commitBySha); diff --git a/src/services/github/v4/fetchCommits/fetchCommitByPullNumber.private.test.ts b/src/services/github/v4/fetchCommits/fetchCommitByPullNumber.private.test.ts index e7d005f0..78574c4d 100644 --- a/src/services/github/v4/fetchCommits/fetchCommitByPullNumber.private.test.ts +++ b/src/services/github/v4/fetchCommits/fetchCommitByPullNumber.private.test.ts @@ -1,4 +1,4 @@ -import gql from 'graphql-tag'; +import { print } from 'graphql'; import { getDevAccessToken } from '../../../../test/private/getDevAccessToken'; import { Commit } from '../../../sourceCommit/parseSourceCommit'; import * as apiRequestV4Module from '../apiRequestV4'; @@ -24,17 +24,14 @@ describe('fetchCommitByPullNumber', () => { accessToken: devAccessToken, pullNumber: 121633, sourceBranch: 'master', - historicalBranchLabelMappings: [], }); }); it('makes the right queries', () => { const queries = spy.mock.calls.reduce((acc, call) => { const query = call[0].query; - const ast = gql(query); - //@ts-expect-error - const name = ast.definitions[0].name.value; - return { ...acc, [name]: query }; + const name = apiRequestV4Module.getQueryName(query); + return { ...acc, [name]: print(query) }; }, {}); const queryNames = Object.keys(queries); @@ -58,7 +55,6 @@ describe('fetchCommitByPullNumber', () => { repoName: 'backport-e2e', repoOwner: 'backport-org', sourceBranch: 'main', - historicalBranchLabelMappings: [], }; const expectedCommit: Commit = { @@ -112,7 +108,6 @@ describe('fetchCommitByPullNumber', () => { repoName: 'backport-e2e', repoOwner: 'backport-org', sourceBranch: 'main', - historicalBranchLabelMappings: [], }; await expect(fetchCommitByPullNumber(options)).rejects.toThrowError( @@ -129,7 +124,6 @@ describe('fetchCommitByPullNumber', () => { repoName: 'backport-e2e', repoOwner: 'backport-org', sourceBranch: 'main', - historicalBranchLabelMappings: [], }; await expect(fetchCommitByPullNumber(options)).rejects.toThrowError( diff --git a/src/services/github/v4/fetchCommits/fetchCommitByPullNumber.ts b/src/services/github/v4/fetchCommits/fetchCommitByPullNumber.ts index 1d70889b..ad956893 100644 --- a/src/services/github/v4/fetchCommits/fetchCommitByPullNumber.ts +++ b/src/services/github/v4/fetchCommits/fetchCommitByPullNumber.ts @@ -1,3 +1,4 @@ +import gql from 'graphql-tag'; import { ValidConfigOptions } from '../../../../options/options'; import { HandledError } from '../../../HandledError'; import { @@ -12,7 +13,6 @@ export async function fetchCommitByPullNumber(options: { accessToken: string; branchLabelMapping?: ValidConfigOptions['branchLabelMapping']; githubApiBaseUrlV4?: string; - historicalBranchLabelMappings: ValidConfigOptions['historicalBranchLabelMappings']; pullNumber: number; repoName: string; repoOwner: string; @@ -26,7 +26,7 @@ export async function fetchCommitByPullNumber(options: { repoOwner, } = options; - const query = /* GraphQL */ ` + const query = gql` query CommitByPullNumber( $repoOwner: String! $repoName: String! @@ -41,7 +41,7 @@ export async function fetchCommitByPullNumber(options: { } } - ${sourceCommitWithTargetPullRequestFragment.source} + ${sourceCommitWithTargetPullRequestFragment} `; const res = await apiRequestV4({ diff --git a/src/services/github/v4/fetchCommits/fetchCommitBySha.private.test.ts b/src/services/github/v4/fetchCommits/fetchCommitBySha.private.test.ts index 91b8e014..b0372325 100644 --- a/src/services/github/v4/fetchCommits/fetchCommitBySha.private.test.ts +++ b/src/services/github/v4/fetchCommits/fetchCommitBySha.private.test.ts @@ -1,4 +1,4 @@ -import gql from 'graphql-tag'; +import { DocumentNode, print } from 'graphql'; import { getDevAccessToken } from '../../../../test/private/getDevAccessToken'; import { Commit } from '../../../sourceCommit/parseSourceCommit'; import * as apiRequestV4Module from '../apiRequestV4'; @@ -24,17 +24,15 @@ describe('fetchCommitBySha', () => { accessToken: devAccessToken, sha: 'd421ddcf6157150596581c7885afa3690cec6339', sourceBranch: 'main', - historicalBranchLabelMappings: [], }); }); it('makes the right queries', () => { const queries = spy.mock.calls.reduce((acc, call) => { - const query = call[0].query; - const ast = gql(query); + const query = call[0].query as DocumentNode; //@ts-expect-error - const name = ast.definitions[0].name.value; - return { ...acc, [name]: query }; + const name = query.definitions[0].name.value; + return { ...acc, [name]: print(query) }; }, {}); const queryNames = Object.keys(queries); @@ -86,7 +84,6 @@ describe('fetchCommitBySha', () => { accessToken: devAccessToken, sha: 'cb6fbc0', sourceBranch: 'master', - historicalBranchLabelMappings: [], }) ).toEqual(expectedCommit); }); @@ -99,7 +96,6 @@ describe('fetchCommitBySha', () => { accessToken: devAccessToken, sha: 'fc22f59', sourceBranch: 'main', - historicalBranchLabelMappings: [], }) ).rejects.toThrowError( 'No commit found on branch "main" with sha "fc22f59"' @@ -114,7 +110,6 @@ describe('fetchCommitBySha', () => { accessToken: devAccessToken, sha: 'myCommitSha', sourceBranch: 'main', - historicalBranchLabelMappings: [], }) ).rejects.toThrowError( 'No commit found on branch "main" with sha "myCommitSha"' diff --git a/src/services/github/v4/fetchCommits/fetchCommitBySha.ts b/src/services/github/v4/fetchCommits/fetchCommitBySha.ts index 4747fbba..6b4247df 100644 --- a/src/services/github/v4/fetchCommits/fetchCommitBySha.ts +++ b/src/services/github/v4/fetchCommits/fetchCommitBySha.ts @@ -1,3 +1,4 @@ +import gql from 'graphql-tag'; import { ValidConfigOptions } from '../../../../options/options'; import { HandledError } from '../../../HandledError'; import { @@ -12,7 +13,6 @@ export async function fetchCommitBySha(options: { accessToken: string; branchLabelMapping?: ValidConfigOptions['branchLabelMapping']; githubApiBaseUrlV4?: string; - historicalBranchLabelMappings: ValidConfigOptions['historicalBranchLabelMappings']; repoName: string; repoOwner: string; sha: string; @@ -27,7 +27,7 @@ export async function fetchCommitBySha(options: { sourceBranch, } = options; - const query = /* GraphQL */ ` + const query = gql` query CommitsBySha($repoOwner: String!, $repoName: String!, $sha: String!) { repository(owner: $repoOwner, name: $repoName) { object(expression: $sha) { @@ -36,7 +36,7 @@ export async function fetchCommitBySha(options: { } } - ${sourceCommitWithTargetPullRequestFragment.source} + ${sourceCommitWithTargetPullRequestFragment} `; const res = await apiRequestV4({ diff --git a/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.private.test.ts b/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.private.test.ts index 1ef7772d..788d7b07 100644 --- a/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.private.test.ts +++ b/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.private.test.ts @@ -1,4 +1,4 @@ -import gql from 'graphql-tag'; +import { DocumentNode, print } from 'graphql'; import { getDevAccessToken } from '../../../../test/private/getDevAccessToken'; import { Commit } from '../../../sourceCommit/parseSourceCommit'; import * as apiRequestV4Module from '../apiRequestV4'; @@ -20,7 +20,6 @@ describe('fetchCommitsByAuthor', () => { commits = await fetchCommitsByAuthor({ accessToken: devAccessToken, author: 'sqren', - historicalBranchLabelMappings: [], maxNumber: 10, repoName: 'kibana', repoOwner: 'elastic', @@ -33,11 +32,10 @@ describe('fetchCommitsByAuthor', () => { it('makes the right queries', () => { const queries = spy.mock.calls.reduce((acc, call) => { - const query = call[0].query; - const ast = gql(query); + const query = call[0].query as DocumentNode; //@ts-expect-error - const name = ast.definitions[0].name.value; - return { ...acc, [name]: query }; + const name = query.definitions[0].name.value; + return { ...acc, [name]: print(query) }; }, {}); const queryNames = Object.keys(queries); @@ -57,7 +55,6 @@ describe('fetchCommitsByAuthor', () => { const getOptions = () => ({ accessToken: devAccessToken, author: 'sqren', - historicalBranchLabelMappings: [], maxNumber: 10, repoName: 'repo-with-different-commit-paths', repoOwner: 'backport-org', @@ -144,7 +141,6 @@ describe('fetchCommitsByAuthor', () => { res = await fetchCommitsByAuthor({ accessToken: devAccessToken, commitPaths: [], - historicalBranchLabelMappings: [], maxNumber: 10, repoName: 'backport-e2e', repoOwner: 'backport-org', @@ -197,11 +193,13 @@ describe('fetchCommitsByAuthor', () => { ]); }); - it('returns empty if there are no related PRs', async () => { + it('returns missing pull requests', async () => { const commitWithoutPRs = res.find( (commit) => commit.sourcePullRequest?.number === 8 ); - expect(commitWithoutPRs?.expectedTargetPullRequests).toEqual([]); + expect(commitWithoutPRs?.expectedTargetPullRequests).toEqual([ + { branch: '7.x', state: 'MISSING' }, + ]); }); }); }); diff --git a/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.test.ts b/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.test.ts index 7a5f805e..09d97cbd 100644 --- a/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.test.ts +++ b/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.test.ts @@ -12,7 +12,6 @@ const defaultOptions = { accessToken: 'myAccessToken', author: 'sqren', githubApiBaseUrlV4: 'http://localhost/graphql', - historicalBranchLabelMappings: [], maxNumber: 10, repoName: 'kibana', repoOwner: 'elastic', diff --git a/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.ts b/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.ts index db400d4b..72897297 100644 --- a/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.ts +++ b/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.ts @@ -1,3 +1,4 @@ +import gql from 'graphql-tag'; import { isEmpty, uniqBy, orderBy } from 'lodash'; import { ValidConfigOptions } from '../../../../options/options'; import { filterNil } from '../../../../utils/filterEmpty'; @@ -40,7 +41,7 @@ function fetchByCommitPath({ dateUntil, } = options; - const query = /* GraphQL */ ` + const query = gql` query CommitsByAuthor( $repoOwner: String! $repoName: String! @@ -74,7 +75,7 @@ function fetchByCommitPath({ } } - ${sourceCommitWithTargetPullRequestFragment.source} + ${sourceCommitWithTargetPullRequestFragment} `; const variables = { @@ -102,7 +103,6 @@ export async function fetchCommitsByAuthor(options: { branchLabelMapping?: ValidConfigOptions['branchLabelMapping']; commitPaths?: string[]; githubApiBaseUrlV4?: string; - historicalBranchLabelMappings: ValidConfigOptions['historicalBranchLabelMappings']; maxNumber?: number; repoName: string; repoOwner: string; diff --git a/src/services/github/v4/fetchCommits/fetchPullRequestBySearchQuery.ts b/src/services/github/v4/fetchCommits/fetchPullRequestBySearchQuery.ts index 7c6056e3..245bc231 100644 --- a/src/services/github/v4/fetchCommits/fetchPullRequestBySearchQuery.ts +++ b/src/services/github/v4/fetchCommits/fetchPullRequestBySearchQuery.ts @@ -1,3 +1,4 @@ +import gql from 'graphql-tag'; import { isEmpty } from 'lodash'; import { ValidConfigOptions } from '../../../../options/options'; import { HandledError } from '../../../HandledError'; @@ -23,7 +24,7 @@ export async function fetchPullRequestBySearchQuery( author, } = options; - const query = /* GraphQL */ ` + const query = gql` query PullRequestBySearchQuery($query: String!, $maxNumber: Int!) { search(query: $query, type: ISSUE, first: $maxNumber) { nodes { @@ -36,7 +37,7 @@ export async function fetchPullRequestBySearchQuery( } } - ${sourceCommitWithTargetPullRequestFragment.source} + ${sourceCommitWithTargetPullRequestFragment} `; const authorFilter = author ? ` author:${author}` : ''; diff --git a/src/services/github/v4/fetchExistingPullRequest.ts b/src/services/github/v4/fetchExistingPullRequest.ts index 959d87fc..8323c9e3 100644 --- a/src/services/github/v4/fetchExistingPullRequest.ts +++ b/src/services/github/v4/fetchExistingPullRequest.ts @@ -1,3 +1,4 @@ +import gql from 'graphql-tag'; import { ValidConfigOptions } from '../../../options/options'; import { PullRequestPayload } from '../v3/createPullRequest'; import { apiRequestV4 } from './apiRequestV4'; @@ -10,7 +11,7 @@ export async function fetchExistingPullRequest({ prPayload: PullRequestPayload; }) { const { githubApiBaseUrlV4, accessToken } = options; - const query = /* GraphQL */ ` + const query = gql` query ExistingPullRequest( $repoOwner: String! $repoName: String! diff --git a/src/services/github/v4/fetchPullRequestAutoMergeMethod.ts b/src/services/github/v4/fetchPullRequestAutoMergeMethod.ts index ad6b5063..6036a82a 100644 --- a/src/services/github/v4/fetchPullRequestAutoMergeMethod.ts +++ b/src/services/github/v4/fetchPullRequestAutoMergeMethod.ts @@ -1,3 +1,4 @@ +import gql from 'graphql-tag'; import { ValidConfigOptions } from '../../../options/options'; import { apiRequestV4 } from './apiRequestV4'; @@ -11,7 +12,7 @@ export async function fetchPullRequestAutoMergeMethod( ) { const { accessToken, githubApiBaseUrlV4, repoName, repoOwner } = options; - const query = /* GraphQL */ ` + const query = gql` query PullRequestAutoMergeMethod( $repoOwner: String! $repoName: String! diff --git a/src/services/github/v4/fetchRemoteProjectConfig.ts b/src/services/github/v4/fetchRemoteProjectConfig.ts index 39494115..0de8ea48 100644 --- a/src/services/github/v4/fetchRemoteProjectConfig.ts +++ b/src/services/github/v4/fetchRemoteProjectConfig.ts @@ -1,5 +1,10 @@ +import gql from 'graphql-tag'; import { ConfigFileOptions } from '../../../entrypoint.module'; -import { withConfigMigrations } from '../../../options/config/readConfigFile'; +import { + parseRemoteConfig, + RemoteConfigHistory, + RemoteConfigHistoryFragment, +} from '../../remoteConfig'; import { apiRequestV4 } from './apiRequestV4'; export async function fetchRemoteProjectConfig(options: { @@ -8,11 +13,11 @@ export async function fetchRemoteProjectConfig(options: { repoName: string; repoOwner: string; sourceBranch: string; -}): Promise { +}): Promise { const { accessToken, githubApiBaseUrlV4, repoName, repoOwner, sourceBranch } = options; - const query = /* GraphQL */ ` + const query = gql` query ProjectConfig( $repoOwner: String! $repoName: String! @@ -21,27 +26,13 @@ export async function fetchRemoteProjectConfig(options: { repository(owner: $repoOwner, name: $repoName) { ref(qualifiedName: $sourceBranch) { target { - ... on Commit { - history(first: 1, path: ".backportrc.json") { - edges { - remoteConfig: node { - file(path: ".backportrc.json") { - ... on TreeEntry { - object { - ... on Blob { - text - } - } - } - } - } - } - } - } + ...RemoteConfigHistory } } } } + + ${RemoteConfigHistoryFragment} `; try { @@ -56,11 +47,12 @@ export async function fetchRemoteProjectConfig(options: { }, }); - return withConfigMigrations( - JSON.parse( - res.repository.ref.target.history.edges[0].remoteConfig.file.object.text - ) - ) as ConfigFileOptions; + const remoteConfig = + res.repository.ref.target.remoteConfigHistory.edges?.[0].remoteConfig; + + if (remoteConfig) { + return parseRemoteConfig(remoteConfig); + } } catch (e) { throw new Error('Project config does not exist'); } @@ -69,19 +61,7 @@ export async function fetchRemoteProjectConfig(options: { interface GithubProjectConfig { repository: { ref: { - target: { - history: { - edges: Array<{ - remoteConfig: { - file: { - object: { - text: string; - }; - }; - }; - }>; - }; - }; + target: RemoteConfigHistory; }; }; } diff --git a/src/services/github/v4/getOptionsFromGithub/getOptionsFromGithub.private.test.ts b/src/services/github/v4/getOptionsFromGithub/getOptionsFromGithub.private.test.ts index c98e2368..9b256297 100644 --- a/src/services/github/v4/getOptionsFromGithub/getOptionsFromGithub.private.test.ts +++ b/src/services/github/v4/getOptionsFromGithub/getOptionsFromGithub.private.test.ts @@ -46,24 +46,6 @@ describe('getOptionsFromGithub', () => { '^v7.9.0$': '7.x', '^v8.0.0$': 'master', }, - historicalBranchLabelMappings: [ - { - branchLabelMapping: { - '^v(\\d+).(\\d+).\\d+$': '$1.$2', - '^v7.9.0$': '7.x', - '^v8.0.0$': 'master', - }, - committedDate: '2020-08-15T10:33:06Z', - }, - { - branchLabelMapping: { '6.1': '6.1', '6.3': '6.3' }, - committedDate: '2020-08-15T10:20:23Z', - }, - { - branchLabelMapping: { '6.1': '6.1', '6.3': '6.3' }, - committedDate: '2020-08-15T10:17:57Z', - }, - ], sourceBranch: 'master', targetBranchChoices: [ { checked: true, name: 'master' }, @@ -147,7 +129,6 @@ describe('getOptionsFromGithub', () => { expect(options).toEqual({ authenticatedUsername: 'sqren', sourceBranch: 'main', - historicalBranchLabelMappings: [], }); }); }); diff --git a/src/services/github/v4/getOptionsFromGithub/getOptionsFromGithub.ts b/src/services/github/v4/getOptionsFromGithub/getOptionsFromGithub.ts index 620cea0e..b3e1e8a1 100644 --- a/src/services/github/v4/getOptionsFromGithub/getOptionsFromGithub.ts +++ b/src/services/github/v4/getOptionsFromGithub/getOptionsFromGithub.ts @@ -1,6 +1,4 @@ import { ConfigFileOptions } from '../../../../options/ConfigOptions'; -import { withConfigMigrations } from '../../../../options/config/readConfigFile'; -import { filterNil } from '../../../../utils/filterEmpty'; import { HandledError } from '../../../HandledError'; import { getLocalConfigFileCommitDate, @@ -8,9 +6,10 @@ import { isLocalConfigFileModified, } from '../../../git'; import { logger } from '../../../logger'; +import { parseRemoteConfig } from '../../../remoteConfig'; import { apiRequestV4, GithubV4Exception } from '../apiRequestV4'; import { throwOnInvalidAccessToken } from '../throwOnInvalidAccessToken'; -import { GithubConfigOptionsResponse, query, RemoteConfig } from './query'; +import { GithubConfigOptionsResponse, query } from './query'; // fetches the default source branch for the repo (normally "master") // startup checks: @@ -57,44 +56,38 @@ export async function getOptionsFromGithub(options: { ); } - const historicalRemoteConfigs = - res.repository.defaultBranchRef.target.history.edges; - const latestRemoteConfig = historicalRemoteConfigs[0]?.remoteConfig; - const skipRemoteConfig = await getSkipRemoteConfigFile( + const remoteConfig = await getRemoteConfigFileOptions( + res, options.cwd, - options.skipRemoteConfig, - latestRemoteConfig + options.skipRemoteConfig ); - const remoteConfig = skipRemoteConfig - ? {} - : parseRemoteConfig(latestRemoteConfig); - return { authenticatedUsername: options.username ?? res.viewer.login, sourceBranch: res.repository.defaultBranchRef.name, ...remoteConfig, - historicalBranchLabelMappings: skipRemoteConfig - ? [] - : getHistoricalBranchLabelMappings(historicalRemoteConfigs), }; } -async function getSkipRemoteConfigFile( +async function getRemoteConfigFileOptions( + res: GithubConfigOptionsResponse, cwd?: string, - skipRemoteConfig?: boolean, - remoteConfig?: RemoteConfig -) { + skipRemoteConfig?: boolean +): Promise { if (skipRemoteConfig) { logger.info( 'Skipping remote config: `--skip-remote-config` specified via config file or cli' ); - return true; + return; } + const remoteConfig = + res.repository.defaultBranchRef.target.remoteConfigHistory.edges?.[0] + ?.remoteConfig; + if (!remoteConfig) { logger.info("Skipping remote config: remote config doesn't exist"); - return true; + return; } if (cwd) { @@ -107,12 +100,12 @@ async function getSkipRemoteConfigFile( if (isLocalConfigUntracked) { logger.info('Skipping remote config: local config is new'); - return true; + return; } if (isLocalConfigModified) { logger.info('Skipping remote config: local config is modified'); - return true; + return; } if ( @@ -124,68 +117,22 @@ async function getSkipRemoteConfigFile( localCommitDate ).toISOString()} > ${remoteConfig.committedDate}` ); - return true; + return; } } - return false; + return parseRemoteConfig(remoteConfig); } -function parseRemoteConfig(remoteConfig?: RemoteConfig) { - if (!remoteConfig) { - return; - } - - try { - logger.info('Using remote config'); - return withConfigMigrations( - JSON.parse(remoteConfig.file.object.text) - ) as ConfigFileOptions; - } catch (e) { - logger.info('Parsing remote config failed', e); - return; - } -} - -function getHistoricalBranchLabelMappings( - historicalRemoteConfigs: { remoteConfig: RemoteConfig }[] -) { - return historicalRemoteConfigs - .map((edge) => { - try { - const remoteConfig = JSON.parse( - edge.remoteConfig.file.object.text - ) as ConfigFileOptions; - - if (!remoteConfig.branchLabelMapping) { - return; - } - - return { - branchLabelMapping: remoteConfig.branchLabelMapping, - committedDate: edge.remoteConfig.committedDate, - }; - } catch (e) { - logger.info('Could not get historical remote config', e); - return; - } - }) - .filter(filterNil); -} function swallowErrorIfConfigFileIsMissing(error: GithubV4Exception) { const { data, errors } = error.axiosResponse.data; - const wasMissingConfigError = errors?.some((error) => { - const isMatch = - /^repository.defaultBranchRef.target.history.edges.\d+.remoteConfig.file$/.test( - error.path.join('.') - ); - - return isMatch && error.type === 'NOT_FOUND'; + const missingConfigError = errors?.some((error) => { + return error.path.includes('remoteConfig') && error.type === 'NOT_FOUND'; }); // swallow error if it's just the config file that's missing - if (wasMissingConfigError && data != null) { + if (missingConfigError && data != null) { return data; } diff --git a/src/services/github/v4/getOptionsFromGithub/query.ts b/src/services/github/v4/getOptionsFromGithub/query.ts index f65a75d3..42123e42 100644 --- a/src/services/github/v4/getOptionsFromGithub/query.ts +++ b/src/services/github/v4/getOptionsFromGithub/query.ts @@ -1,7 +1,8 @@ -export type RemoteConfig = { - committedDate: string; - file: { object: { text: string } }; -}; +import gql from 'graphql-tag'; +import { + RemoteConfigHistory, + RemoteConfigHistoryFragment, +} from '../../../remoteConfig'; export interface GithubConfigOptionsResponse { viewer: { @@ -11,14 +12,12 @@ export interface GithubConfigOptionsResponse { illegalBackportBranch: { id: string } | null; defaultBranchRef: { name: string; - target: { - history: { edges: Array<{ remoteConfig: RemoteConfig }> }; - }; + target: RemoteConfigHistory; }; }; } -export const query = /* GraphQL */ ` +export const query = gql` query GithubConfigOptions($repoOwner: String!, $repoName: String!) { viewer { login @@ -31,26 +30,11 @@ export const query = /* GraphQL */ ` defaultBranchRef { name target { - ... on Commit { - history(first: 20, path: ".backportrc.json") { - edges { - remoteConfig: node { - committedDate - file(path: ".backportrc.json") { - ... on TreeEntry { - object { - ... on Blob { - text - } - } - } - } - } - } - } - } + ...RemoteConfigHistory } } } } + + ${RemoteConfigHistoryFragment} `; diff --git a/src/services/github/v4/getRepoOwnerAndNameFromGitRemotes.ts b/src/services/github/v4/getRepoOwnerAndNameFromGitRemotes.ts index 475bcabc..a9d4b87a 100644 --- a/src/services/github/v4/getRepoOwnerAndNameFromGitRemotes.ts +++ b/src/services/github/v4/getRepoOwnerAndNameFromGitRemotes.ts @@ -1,3 +1,4 @@ +import gql from 'graphql-tag'; import { maybe } from '../../../utils/maybe'; import { getRepoInfoFromGitRemotes } from '../../git'; import { logger } from '../../logger'; @@ -64,7 +65,7 @@ export interface RepoOwnerAndNameResponse { }; } -const query = /* GraphQL */ ` +const query = gql` query RepoOwnerAndName($repoOwner: String!, $repoName: String!) { repository(owner: $repoOwner, name: $repoName) { isFork diff --git a/src/services/github/v4/mocks/commitsByAuthorMock.ts b/src/services/github/v4/mocks/commitsByAuthorMock.ts index 27aea12c..a2529889 100644 --- a/src/services/github/v4/mocks/commitsByAuthorMock.ts +++ b/src/services/github/v4/mocks/commitsByAuthorMock.ts @@ -8,6 +8,7 @@ export const commitsByAuthorMock: CommitByAuthorResponse = { edges: [ { node: { + remoteConfigHistory: { edges: [] }, repository: { name: 'kibana', owner: { login: 'elastic' }, @@ -22,6 +23,7 @@ export const commitsByAuthorMock: CommitByAuthorResponse = { }, { node: { + remoteConfigHistory: { edges: [] }, repository: { name: 'kibana', owner: { login: 'elastic' }, @@ -54,6 +56,7 @@ export const commitsByAuthorMock: CommitByAuthorResponse = { }, { node: { + remoteConfigHistory: { edges: [] }, repository: { name: 'kibana', owner: { login: 'elastic' }, @@ -123,6 +126,7 @@ export const commitsByAuthorMock: CommitByAuthorResponse = { }, { node: { + remoteConfigHistory: { edges: [] }, repository: { name: 'kibana', owner: { login: 'elastic' }, @@ -137,6 +141,7 @@ export const commitsByAuthorMock: CommitByAuthorResponse = { }, { node: { + remoteConfigHistory: { edges: [] }, repository: { name: 'kibana', owner: { login: 'elastic' }, diff --git a/src/services/logger.ts b/src/services/logger.ts index 036e08f8..97ca71e8 100644 --- a/src/services/logger.ts +++ b/src/services/logger.ts @@ -3,45 +3,15 @@ import { redact } from '../utils/redact'; import { getLogfilePath } from './env'; export let logger: winston.Logger; - -// wrapper around console.log -export function consoleLog(message: string) { - // eslint-disable-next-line no-console - console.log(redactAccessToken(message)); - //process.stdout.write(message); -} - let _accessToken: string | undefined; - -export function updateLogger({ - accessToken, - verbose, -}: { - accessToken: string; - verbose?: boolean; -}) { - // set access token - _accessToken = accessToken; - - // set log level - logger.level = verbose ? 'debug' : 'info'; -} - -function redactAccessToken(str: string) { - // `redactAccessToken` might be called before access token is set - if (_accessToken) { - return redact(_accessToken, str); - } - - return str; -} +let _ci: boolean | undefined; export function initLogger({ ci, accessToken, logFilePath, }: { - ci?: boolean; + ci: boolean | undefined; accessToken?: string; logFilePath?: string; }) { @@ -51,6 +21,8 @@ export function initLogger({ _accessToken = accessToken; } + _ci = ci; + logger = winston.createLogger({ format: format.combine( format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), @@ -59,14 +31,43 @@ export function initLogger({ fillExcept: ['message', 'level', 'timestamp', 'label'], }) ), - transports: ci - ? [fileTransport, new winston.transports.Console()] - : [fileTransport], + transports: fileTransport, }); return logger; } +// wrapper around console.log +export function consoleLog(message: string) { + if (!_ci) { + // eslint-disable-next-line no-console + console.log(redactAccessToken(message)); + } +} + +export function updateLogger({ + accessToken, + verbose, +}: { + accessToken: string; + verbose?: boolean; +}) { + // set access token + _accessToken = accessToken; + + // set log level + logger.level = verbose ? 'debug' : 'info'; +} + +function redactAccessToken(str: string) { + // `redactAccessToken` might be called before access token is set + if (_accessToken) { + return redact(_accessToken, str); + } + + return str; +} + function getFileTransport({ logFilePath }: { logFilePath?: string }) { return new winston.transports.File({ filename: getLogfilePath({ logFilePath }), diff --git a/src/services/remoteConfig.ts b/src/services/remoteConfig.ts new file mode 100644 index 00000000..f0ec45c7 --- /dev/null +++ b/src/services/remoteConfig.ts @@ -0,0 +1,51 @@ +import gql from 'graphql-tag'; +import { ConfigFileOptions } from '../entrypoint.module'; +import { withConfigMigrations } from '../options/config/readConfigFile'; +import { logger } from './logger'; + +export const RemoteConfigHistoryFragment = gql` + fragment RemoteConfigHistory on Commit { + remoteConfigHistory: history(first: 1, path: ".backportrc.json") { + edges { + remoteConfig: node { + committedDate + file(path: ".backportrc.json") { + ... on TreeEntry { + object { + ... on Blob { + text + } + } + } + } + } + } + } + } +`; + +export interface RemoteConfig { + committedDate: string; + file: { + object: { text: string }; + }; +} + +export interface RemoteConfigHistory { + remoteConfigHistory: { + edges: Array<{ + remoteConfig: RemoteConfig; + }> | null; + }; +} + +export function parseRemoteConfig(remoteConfig: RemoteConfig) { + try { + return withConfigMigrations( + JSON.parse(remoteConfig.file.object.text) + ) as ConfigFileOptions; + } catch (e) { + logger.info('Parsing remote config failed', e); + return; + } +} diff --git a/src/services/sourceCommit/getMockSourceCommit.ts b/src/services/sourceCommit/getMockSourceCommit.ts index b26e00db..7707727a 100644 --- a/src/services/sourceCommit/getMockSourceCommit.ts +++ b/src/services/sourceCommit/getMockSourceCommit.ts @@ -6,6 +6,10 @@ export function getMockSourceCommit({ timelineItems = [], }: { sourceCommit: { + remoteConfig?: { + branchLabelMapping: Record; + committedDate: string; + }; sha?: string; message: string; commitedDate?: string; @@ -29,7 +33,26 @@ export function getMockSourceCommit({ 'DO NOT USE: Default Pull Request Title'; const defaultSourceCommitSha = 'DO NOT USE: default-source-commit-sha'; - const baseMockCommit = { + const baseMockCommit: SourceCommitWithTargetPullRequest = { + remoteConfigHistory: sourceCommit.remoteConfig + ? { + edges: [ + { + remoteConfig: { + committedDate: sourceCommit.remoteConfig.committedDate, + file: { + object: { + text: JSON.stringify({ + branchLabelMapping: + sourceCommit.remoteConfig.branchLabelMapping, + }), + }, + }, + }, + }, + ], + } + : { edges: [] }, repository: { name: 'kibana', owner: { login: 'elastic' }, @@ -37,15 +60,11 @@ export function getMockSourceCommit({ committedDate: sourceCommit.commitedDate ?? '2021-12-22T00:00:00Z', sha: sourceCommit.sha ?? defaultSourceCommitSha, message: sourceCommit.message, + associatedPullRequests: { edges: null }, }; if (!sourcePullRequest) { - return { - ...baseMockCommit, - associatedPullRequests: { - edges: null, - }, - }; + return baseMockCommit; } return { diff --git a/src/services/sourceCommit/parseSourceCommit.test.ts b/src/services/sourceCommit/parseSourceCommit.test.ts index 4afacd67..6dc3e624 100644 --- a/src/services/sourceCommit/parseSourceCommit.test.ts +++ b/src/services/sourceCommit/parseSourceCommit.test.ts @@ -86,7 +86,6 @@ describe('parseSourceCommit', () => { '^v6.4.0$': 'main', '^v(\\d+).(\\d+).\\d+$': '$1.$2', }, - historicalBranchLabelMappings: [], } as unknown as ValidConfigOptions, }); @@ -117,6 +116,13 @@ describe('parseSourceCommit', () => { it('uses the historical branchLabelMapping from 2021-02-02', () => { const mockSourceCommit = getMockSourceCommit({ sourceCommit: { + remoteConfig: { + branchLabelMapping: { + '^v6.3.0$': 'main', + '^v(\\d+).(\\d+).\\d+$': '$1.$2', + }, + committedDate: '2021-02-02T00:00:00Z', + }, message: 'My commit message (#66)', commitedDate: '2021-03-03T00:00:00Z', }, @@ -142,22 +148,6 @@ describe('parseSourceCommit', () => { '^v6.4.0$': 'main', '^v(\\d+).(\\d+).\\d+$': '$1.$2', }, - historicalBranchLabelMappings: [ - { - branchLabelMapping: { - '^v6.4.0$': 'main', - '^v(\\d+).(\\d+).\\d+$': '$1.$2', - }, - committedDate: '2021-04-04T00:00:00Z', - }, - { - branchLabelMapping: { - '^v6.3.0$': 'main', - '^v(\\d+).(\\d+).\\d+$': '$1.$2', - }, - committedDate: '2021-02-02T00:00:00Z', - }, - ], } as unknown as ValidConfigOptions, }); diff --git a/src/services/sourceCommit/parseSourceCommit.ts b/src/services/sourceCommit/parseSourceCommit.ts index 5a1076e4..e2879741 100644 --- a/src/services/sourceCommit/parseSourceCommit.ts +++ b/src/services/sourceCommit/parseSourceCommit.ts @@ -1,5 +1,10 @@ -import { isEmpty } from 'lodash'; +import gql from 'graphql-tag'; import { ValidConfigOptions } from '../../options/options'; +import { + parseRemoteConfig, + RemoteConfigHistory, + RemoteConfigHistoryFragment, +} from '../remoteConfig'; import { ExpectedTargetPullRequest, getExpectedTargetPullRequests, @@ -79,6 +84,7 @@ interface TimelineIssueEdge { } export type SourceCommitWithTargetPullRequest = { + remoteConfigHistory: RemoteConfigHistory['remoteConfigHistory']; repository: { name: string; owner: { login: string }; @@ -98,7 +104,6 @@ export function parseSourceCommit({ sourceCommit: SourceCommitWithTargetPullRequest; options: { branchLabelMapping?: ValidConfigOptions['branchLabelMapping']; - historicalBranchLabelMappings: ValidConfigOptions['historicalBranchLabelMappings']; sourceBranch: string; }; }): Commit { @@ -108,11 +113,13 @@ export function parseSourceCommit({ // use info from associated pull request if available. Fall back to commit info const sourceBranch = sourcePullRequest?.baseRefName ?? options.sourceBranch; - const branchLabelMapping = getBranchLabelMappingForCommit( - sourceCommit, - options.branchLabelMapping, - options.historicalBranchLabelMappings - ); + const remoteConfig = + sourceCommit.remoteConfigHistory.edges?.[0]?.remoteConfig; + + const branchLabelMapping = remoteConfig + ? parseRemoteConfig(remoteConfig)?.branchLabelMapping ?? + options.branchLabelMapping + : options.branchLabelMapping; const expectedTargetPullRequests = getExpectedTargetPullRequests( sourceCommit, @@ -140,72 +147,72 @@ export function parseSourceCommit({ }; } -export const sourceCommitWithTargetPullRequestFragment = { - source: /* GraphQL */ ` - fragment SourceCommitWithTargetPullRequest on Commit { - # Source Commit - repository { - name - owner { - login - } +export const sourceCommitWithTargetPullRequestFragment = gql` + fragment SourceCommitWithTargetPullRequest on Commit { + ...RemoteConfigHistory + + # Source Commit + repository { + name + owner { + login } - sha: oid - message - committedDate - - # Source pull request: PR where source commit was merged in - associatedPullRequests(first: 1) { - edges { - node { - url - number - labels(first: 50) { - nodes { - name - } + } + sha: oid + message + committedDate + + # Source pull request: PR where source commit was merged in + associatedPullRequests(first: 1) { + edges { + node { + url + number + labels(first: 50) { + nodes { + name } - baseRefName + } + baseRefName - # source merge commit (the commit that actually went into the source branch) - mergeCommit { - sha: oid - message - } + # source merge commit (the commit that actually went into the source branch) + mergeCommit { + sha: oid + message + } - # (possible) backport pull requests referenced in the source pull request - timelineItems(last: 20, itemTypes: CROSS_REFERENCED_EVENT) { - edges { - node { - ... on CrossReferencedEvent { - targetPullRequest: source { - __typename - - # Target PRs (backport PRs) - ... on PullRequest { - # target merge commit: the backport commit that was merged into the target branch - targetMergeCommit: mergeCommit { - sha: oid - message - } - repository { - name - owner { - login - } + # (possible) backport pull requests referenced in the source pull request + timelineItems(last: 20, itemTypes: CROSS_REFERENCED_EVENT) { + edges { + node { + ... on CrossReferencedEvent { + targetPullRequest: source { + __typename + + # Target PRs (backport PRs) + ... on PullRequest { + # target merge commit: the backport commit that was merged into the target branch + targetMergeCommit: mergeCommit { + sha: oid + message + } + repository { + name + owner { + login } - url - title - state - baseRefName - number - commits(first: 20) { - edges { - node { - targetCommit: commit { - message - sha: oid - } + } + url + title + state + baseRefName + number + commits(first: 20) { + edges { + node { + targetCommit: commit { + message + sha: oid } } } @@ -219,25 +226,7 @@ export const sourceCommitWithTargetPullRequestFragment = { } } } - `, -}; - -function getBranchLabelMappingForCommit( - sourceCommit: SourceCommitWithTargetPullRequest, - branchLabelMapping?: ValidConfigOptions['branchLabelMapping'], - historicalBranchLabelMappings: ValidConfigOptions['historicalBranchLabelMappings'] = [] -): Record | undefined { - if (isEmpty(historicalBranchLabelMappings)) { - return branchLabelMapping; } - const match = historicalBranchLabelMappings.find( - (branchLabelMapping) => - sourceCommit.committedDate > branchLabelMapping.committedDate - ); - - return ( - match?.branchLabelMapping ?? - historicalBranchLabelMappings[0].branchLabelMapping - ); -} + ${RemoteConfigHistoryFragment} +`; diff --git a/src/test/nockHelpers.ts b/src/test/nockHelpers.ts index af6e7e6f..4dd8f313 100644 --- a/src/test/nockHelpers.ts +++ b/src/test/nockHelpers.ts @@ -1,7 +1,10 @@ import { URL } from 'url'; import gql from 'graphql-tag'; +import { disableFragmentWarnings } from 'graphql-tag'; import nock from 'nock'; +disableFragmentWarnings(); + export function mockGqlRequest({ name, statusCode, @@ -21,7 +24,9 @@ export function mockGqlRequest({ ); const scope = nock(origin) - .post(pathname, (body) => getGqlName(body.query) === name) + .post(pathname, (body) => { + return getQueryNameFromString(body.query) === name; + }) .reply(statusCode, body, headers); return listenForCallsToNockScope(scope) as { @@ -30,7 +35,7 @@ export function mockGqlRequest({ }[]; } -function getGqlName(query: string) { +function getQueryNameFromString(query: string) { const obj = gql(query); // @ts-expect-error diff --git a/src/ui/cherrypickAndCreateTargetPullRequest.ts b/src/ui/cherrypickAndCreateTargetPullRequest.ts index 19c71414..b3726ce9 100644 --- a/src/ui/cherrypickAndCreateTargetPullRequest.ts +++ b/src/ui/cherrypickAndCreateTargetPullRequest.ts @@ -1,6 +1,5 @@ import chalk from 'chalk'; import { isEmpty, difference } from 'lodash'; -import ora = require('ora'); import { ValidConfigOptions } from '../options/options'; import { HandledError } from '../services/HandledError'; import { exec } from '../services/child-process-promisified'; @@ -34,6 +33,7 @@ import { confirmPrompt } from '../services/prompts'; import { sequentially } from '../services/sequentially'; import { Commit } from '../services/sourceCommit/parseSourceCommit'; import { getCommitsWithoutBackports } from './getCommitsWithoutBackports'; +import { ora } from './ora'; export async function cherrypickAndCreateTargetPullRequest({ options, @@ -187,7 +187,7 @@ async function waitForCherrypick( (pr) => pr.state === 'MERGED' && pr.branch === targetBranch ); - const cherrypickSpinner = ora(spinnerText).start(); + const cherrypickSpinner = ora(options.ci, spinnerText).start(); let conflictingFiles: ConflictingFiles; let unstagedFiles: string[]; @@ -216,6 +216,7 @@ async function waitForCherrypick( // resolve conflicts automatically if (options.autoFixConflicts) { const autoResolveSpinner = ora( + options.ci, 'Attempting to resolve conflicts automatically' ).start(); @@ -283,7 +284,7 @@ async function waitForCherrypick( }); // Conflicts should be resolved and files staged at this point - const stagingSpinner = ora(`Finalizing cherrypick`).start(); + const stagingSpinner = ora(options.ci, `Finalizing cherrypick`).start(); try { // Run `git commit` await commitChanges(commit, options); diff --git a/src/ui/getCommits.ts b/src/ui/getCommits.ts index 7c8ff8cb..818f2676 100644 --- a/src/ui/getCommits.ts +++ b/src/ui/getCommits.ts @@ -1,5 +1,4 @@ import chalk from 'chalk'; -import ora from 'ora'; import { ValidConfigOptions } from '../options/options'; import { HandledError } from '../services/HandledError'; import { getFirstLine, getShortSha } from '../services/github/commitFormatters'; @@ -8,6 +7,7 @@ import { fetchCommitBySha } from '../services/github/v4/fetchCommits/fetchCommit import { fetchCommitsByAuthor } from '../services/github/v4/fetchCommits/fetchCommitsByAuthor'; import { fetchPullRequestBySearchQuery } from '../services/github/v4/fetchCommits/fetchPullRequestBySearchQuery'; import { promptForCommits } from '../services/prompts'; +import { ora } from './ora'; function getOraPersistsOption(question: string, answer: string) { return { @@ -17,7 +17,7 @@ function getOraPersistsOption(question: string, answer: string) { } export async function getCommits(options: ValidConfigOptions) { - const spinner = ora().start(); + const spinner = ora(options.ci).start(); try { if (options.sha) { diff --git a/src/ui/ora.ts b/src/ui/ora.ts new file mode 100644 index 00000000..47b8dea5 --- /dev/null +++ b/src/ui/ora.ts @@ -0,0 +1,19 @@ +import oraOriginal from 'ora'; + +/* eslint-disable @typescript-eslint/no-empty-function */ +const oraMock = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + start: (text?: string) => oraMock, + succeed: () => {}, + stop: () => {}, + fail: () => {}, + stopAndPersist: () => {}, + set text(value: string) {}, +}; + +export function ora( + ci: boolean | undefined, + options?: string | oraOriginal.Options | undefined +) { + return ci ? oraMock : oraOriginal(options); +} diff --git a/yarn.lock b/yarn.lock index 86357508..79d1d05f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -649,6 +649,11 @@ tslib "~2.3.0" value-or-promise "1.0.11" +"@graphql-typed-document-node/core@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052" + integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg== + "@humanwhocodes/config-array@^0.9.2": version "0.9.2" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914" @@ -1326,6 +1331,14 @@ "@typescript-eslint/types" "5.11.0" eslint-visitor-keys "^3.0.0" +"@urql/core@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.4.1.tgz#bae449dafe98fb4944f3be61eb8e70ba33f8a46d" + integrity sha512-HnS54oNwO4pAACKl/2/tNLbRrxAxKawVJuG9UPiixqeEVekiecUQQnCjb9SpOW4Qr54HYzCMDbr3c5px3hfEEg== + dependencies: + "@graphql-typed-document-node/core" "^3.1.1" + wonka "^4.0.14" + abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -5090,6 +5103,11 @@ winston@^3.5.1: triple-beam "^1.3.0" winston-transport "^4.4.2" +wonka@^4.0.14: + version "4.0.15" + resolved "https://registry.yarnpkg.com/wonka/-/wonka-4.0.15.tgz#9aa42046efa424565ab8f8f451fcca955bf80b89" + integrity sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg== + word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"