mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
This commit is contained in:
parent
f851891647
commit
6478af9d4d
5 changed files with 382 additions and 0 deletions
21
scripts/update_prs.js
Normal file
21
scripts/update_prs.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
require('../src/setup_node_env');
|
||||
require('../src/dev/prs/run_update_prs_cli');
|
81
src/dev/prs/github_api.ts
Normal file
81
src/dev/prs/github_api.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import axios, { AxiosError, AxiosResponse } from 'axios';
|
||||
|
||||
import { createFailError } from '../run';
|
||||
|
||||
interface ResponseError extends AxiosError {
|
||||
request: any;
|
||||
response: AxiosResponse;
|
||||
}
|
||||
const isResponseError = (error: any): error is ResponseError =>
|
||||
error && error.response && error.response.status;
|
||||
|
||||
const isRateLimitError = (error: any) =>
|
||||
isResponseError(error) &&
|
||||
error.response.status === 403 &&
|
||||
`${error.response.headers['X-RateLimit-Remaining']}` === '0';
|
||||
|
||||
export class GithubApi {
|
||||
private api = axios.create({
|
||||
baseURL: 'https://api.github.com/',
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'kibana/update_prs_cli',
|
||||
...(this.accessToken ? { Authorization: `token ${this.accessToken} ` } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
constructor(private accessToken?: string) {}
|
||||
|
||||
async getPrInfo(prNumber: number) {
|
||||
try {
|
||||
const resp = await this.api.get(`repos/elastic/kibana/pulls/${prNumber}`);
|
||||
const targetRef: string = resp.data.base && resp.data.base.ref;
|
||||
if (!targetRef) {
|
||||
throw new Error('unable to read base ref from pr info');
|
||||
}
|
||||
|
||||
const owner: string = resp.data.head && resp.data.head.user && resp.data.head.user.login;
|
||||
if (!owner) {
|
||||
throw new Error('unable to read owner info from pr info');
|
||||
}
|
||||
|
||||
const sourceBranch: string = resp.data.head.ref;
|
||||
if (!sourceBranch) {
|
||||
throw new Error('unable to read source branch name from pr info');
|
||||
}
|
||||
|
||||
return {
|
||||
targetRef,
|
||||
owner,
|
||||
sourceBranch,
|
||||
};
|
||||
} catch (error) {
|
||||
if (!isRateLimitError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw createFailError(
|
||||
'github rate limit exceeded, please specify the `--access-token` command line flag and try again'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
58
src/dev/prs/helpers.ts
Normal file
58
src/dev/prs/helpers.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Readable } from 'stream';
|
||||
import * as Rx from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* Convert a Readable stream to an observable of lines
|
||||
*/
|
||||
export const getLine$ = (stream: Readable) => {
|
||||
return new Rx.Observable<string>(subscriber => {
|
||||
let buffer = '';
|
||||
return Rx.fromEvent(stream, 'data')
|
||||
.pipe(takeUntil(Rx.fromEvent(stream, 'close')))
|
||||
.subscribe({
|
||||
next(chunk) {
|
||||
buffer += chunk;
|
||||
while (true) {
|
||||
const i = buffer.indexOf('\n');
|
||||
if (i === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
subscriber.next(buffer.slice(0, i));
|
||||
buffer = buffer.slice(i + 1);
|
||||
}
|
||||
},
|
||||
error(error) {
|
||||
subscriber.error(error);
|
||||
},
|
||||
complete() {
|
||||
if (buffer.length) {
|
||||
subscriber.next(buffer);
|
||||
buffer = '';
|
||||
}
|
||||
|
||||
subscriber.complete();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
43
src/dev/prs/pr.ts
Normal file
43
src/dev/prs/pr.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { createFlagError } from '../run';
|
||||
|
||||
const isNum = (input: string) => {
|
||||
return /^\d+$/.test(input);
|
||||
};
|
||||
|
||||
export class Pr {
|
||||
static parseInput(input: string) {
|
||||
if (!isNum(input)) {
|
||||
throw createFlagError(`invalid pr number [${input}], expected a number`);
|
||||
}
|
||||
|
||||
return parseInt(input, 10);
|
||||
}
|
||||
|
||||
public readonly remoteRef = `pull/${this.number}/head`;
|
||||
|
||||
constructor(
|
||||
public readonly number: number,
|
||||
public readonly targetRef: string,
|
||||
public readonly owner: string,
|
||||
public readonly sourceBranch: string
|
||||
) {}
|
||||
}
|
179
src/dev/prs/run_update_prs_cli.ts
Normal file
179
src/dev/prs/run_update_prs_cli.ts
Normal file
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import execa from 'execa';
|
||||
import chalk from 'chalk';
|
||||
import { first, tap } from 'rxjs/operators';
|
||||
import dedent from 'dedent';
|
||||
|
||||
import { getLine$ } from './helpers';
|
||||
import { run, createFlagError } from '../run';
|
||||
import { Pr } from './pr';
|
||||
import { GithubApi } from './github_api';
|
||||
|
||||
const UPSTREAM_URL = 'git@github.com:elastic/kibana.git';
|
||||
|
||||
run(
|
||||
async ({ flags, log }) => {
|
||||
/**
|
||||
* Start off by consuming the necessary flags so that errors from invalid
|
||||
* flags can be thrown before anything serious is done
|
||||
*/
|
||||
const accessToken = flags['access-token'];
|
||||
if (typeof accessToken !== 'string' && accessToken !== undefined) {
|
||||
throw createFlagError('invalid --access-token, expected a single string');
|
||||
}
|
||||
|
||||
const repoDir = flags['repo-dir'];
|
||||
if (typeof repoDir !== 'string') {
|
||||
throw createFlagError('invalid --repo-dir, expected a single string');
|
||||
}
|
||||
|
||||
const prNumbers = flags._.map(arg => Pr.parseInput(arg));
|
||||
|
||||
/**
|
||||
* Call the Gitub API once for each PR to get the targetRef so we know which branch to pull
|
||||
* into that pr
|
||||
*/
|
||||
const api = new GithubApi(accessToken);
|
||||
const prs = await Promise.all(
|
||||
prNumbers.map(async prNumber => {
|
||||
const { targetRef, owner, sourceBranch } = await api.getPrInfo(prNumber);
|
||||
return new Pr(prNumber, targetRef, owner, sourceBranch);
|
||||
})
|
||||
);
|
||||
|
||||
const execInDir = async (cmd: string, args: string[]) => {
|
||||
log.debug(`$ ${cmd} ${args.join(' ')}`);
|
||||
|
||||
const proc = execa(cmd, args, {
|
||||
cwd: repoDir,
|
||||
stdio: ['inherit', 'pipe', 'pipe'],
|
||||
} as any);
|
||||
|
||||
await Promise.all([
|
||||
proc.then(() => log.debug(` - ${cmd} exited with 0`)),
|
||||
Rx.merge(getLine$(proc.stdout), getLine$(proc.stderr))
|
||||
.pipe(tap(line => log.debug(line)))
|
||||
.toPromise(),
|
||||
]);
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
// ensure local repo is initialized
|
||||
await execa('git', ['init', repoDir]);
|
||||
|
||||
try {
|
||||
// attempt to init upstream remote
|
||||
await execInDir('git', ['remote', 'add', 'upstream', UPSTREAM_URL]);
|
||||
} catch (error) {
|
||||
if (error.code !== 128) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// remote already exists, update its url
|
||||
await execInDir('git', ['remote', 'set-url', 'upstream', UPSTREAM_URL]);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePr = async (pr: Pr) => {
|
||||
log.info('Fetching...');
|
||||
await execInDir('git', [
|
||||
'fetch',
|
||||
'upstream',
|
||||
'-fun',
|
||||
`pull/${pr.number}/head:${pr.sourceBranch}`,
|
||||
]);
|
||||
await execInDir('git', ['reset', '--hard']);
|
||||
await execInDir('git', ['clean', '-fd']);
|
||||
|
||||
log.info('Checking out %s:%s locally', pr.owner, pr.sourceBranch);
|
||||
await execInDir('git', ['checkout', pr.sourceBranch]);
|
||||
|
||||
try {
|
||||
log.info('Pulling in changes from elastic:%s', pr.targetRef);
|
||||
await execInDir('git', ['pull', 'upstream', pr.targetRef, '--no-edit']);
|
||||
} catch (error) {
|
||||
if (!error.stdout.includes('Automatic merge failed;')) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const resolveConflicts = async () => {
|
||||
log.error(chalk.red('Conflict resolution required'));
|
||||
log.info(
|
||||
dedent(chalk`
|
||||
Please resolve the merge conflicts in ${repoDir} in another terminal window.
|
||||
Once the conflicts are resolved run the following in the other window:
|
||||
|
||||
git commit --no-edit
|
||||
|
||||
{bold hit the enter key when complete}
|
||||
`) + '\n'
|
||||
);
|
||||
|
||||
await getLine$(process.stdin)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
try {
|
||||
await execInDir('git', ['diff-index', '--quiet', 'HEAD', '--']);
|
||||
} catch (_) {
|
||||
log.error(`Uncommitted changes in ${repoDir}`);
|
||||
await resolveConflicts();
|
||||
}
|
||||
};
|
||||
|
||||
await resolveConflicts();
|
||||
}
|
||||
|
||||
log.info('Pushing changes to %s:%s', pr.owner, pr.sourceBranch);
|
||||
await execInDir('git', [
|
||||
'push',
|
||||
`git@github.com:${pr.owner}/kibana.git`,
|
||||
`HEAD:${pr.sourceBranch}`,
|
||||
]);
|
||||
|
||||
log.success('updated');
|
||||
};
|
||||
|
||||
await init();
|
||||
for (const pr of prs) {
|
||||
log.info('pr #%s', pr.number);
|
||||
log.indent(4);
|
||||
try {
|
||||
await updatePr(pr);
|
||||
} finally {
|
||||
log.indent(-4);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
description: 'Update github PRs with the latest changes from their base branch',
|
||||
usage: 'node scripts/update_prs number [...numbers]',
|
||||
flags: {
|
||||
string: ['repo-dir', 'access-token'],
|
||||
default: {
|
||||
'repo-dir': resolve(__dirname, '../../../data/.update_prs'),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue