mirror of
https://github.com/grafana/grafana.git
synced 2025-01-27 00:37:04 -06:00
Improvement: Grafana release process minor improvements (#17661)
* Don't display changelog category title when no items The output of the changelog is meant to be copy/pasted with ease. When a changelog category does not contain items is better to not display title at all thus avoiding having the manually modify the output as we include it in the steps of the process. * Introduce a CLI task to close milestones whilst doing a Grafana release As part of a Grafana release, we need to eventually close the GitHub milestone to indicate is done and remove all the cherry-pick labels from issues/prs within the milestone to avoid our cherry-pick CLI command to pick them up on the next release. * Abstract the GitHub client into a module * Introduce `GitHubClient` to all CLI tasks
This commit is contained in:
parent
0412a28d2e
commit
bc94f85dee
@ -8,7 +8,8 @@ module.exports = {
|
||||
"roots": [
|
||||
"<rootDir>/public/app",
|
||||
"<rootDir>/public/test",
|
||||
"<rootDir>/packages"
|
||||
"<rootDir>/packages",
|
||||
"<rootDir>/scripts",
|
||||
],
|
||||
"testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$",
|
||||
"moduleFileExtensions": [
|
||||
|
@ -6,6 +6,7 @@ import { buildTask } from './tasks/grafanaui.build';
|
||||
import { releaseTask } from './tasks/grafanaui.release';
|
||||
import { changelogTask } from './tasks/changelog';
|
||||
import { cherryPickTask } from './tasks/cherrypick';
|
||||
import { closeMilestoneTask } from './tasks/closeMilestone';
|
||||
import { precommitTask } from './tasks/precommit';
|
||||
import { searchTestDataSetupTask } from './tasks/searchTestDataSetup';
|
||||
|
||||
@ -66,6 +67,21 @@ program
|
||||
await execTask(cherryPickTask)({});
|
||||
});
|
||||
|
||||
program
|
||||
.command('close-milestone')
|
||||
.option('-m, --milestone <milestone>', 'Specify milestone')
|
||||
.description('Helps ends a milestone by removing the cherry-pick label and closing it')
|
||||
.action(async cmd => {
|
||||
if (!cmd.milestone) {
|
||||
console.log('Please specify milestone, example: -m <milestone id from github milestone URL>');
|
||||
return;
|
||||
}
|
||||
|
||||
await execTask(closeMilestoneTask)({
|
||||
milestone: cmd.milestone,
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('precommit')
|
||||
.description('Executes checks')
|
||||
|
@ -1,18 +1,14 @@
|
||||
import axios from 'axios';
|
||||
import _ from 'lodash';
|
||||
import { Task, TaskRunner } from './task';
|
||||
|
||||
const githubGrafanaUrl = 'https://github.com/grafana/grafana';
|
||||
import GithubClient from '../utils/githubClient';
|
||||
|
||||
interface ChangelogOptions {
|
||||
milestone: string;
|
||||
}
|
||||
|
||||
const changelogTaskRunner: TaskRunner<ChangelogOptions> = async ({ milestone }) => {
|
||||
const client = axios.create({
|
||||
baseURL: 'https://api.github.com/repos/grafana/grafana',
|
||||
timeout: 10000,
|
||||
});
|
||||
const githubClient = new GithubClient();
|
||||
const client = githubClient.client;
|
||||
|
||||
if (!/^\d+$/.test(milestone)) {
|
||||
console.log('Use milestone number not title, find number in milestone url');
|
||||
@ -45,13 +41,20 @@ const changelogTaskRunner: TaskRunner<ChangelogOptions> = async ({ milestone })
|
||||
|
||||
const notBugs = _.sortBy(issues.filter(item => !bugs.find(bug => bug === item)), 'title');
|
||||
|
||||
let markdown = '### Features / Enhancements\n';
|
||||
let markdown = '';
|
||||
|
||||
if (notBugs.length > 0) {
|
||||
markdown = '### Features / Enhancements\n';
|
||||
}
|
||||
|
||||
for (const item of notBugs) {
|
||||
markdown += getMarkdownLineForIssue(item);
|
||||
}
|
||||
|
||||
markdown += '\n### Bug Fixes\n';
|
||||
if (bugs.length > 0) {
|
||||
markdown += '\n### Bug Fixes\n';
|
||||
}
|
||||
|
||||
for (const item of bugs) {
|
||||
markdown += getMarkdownLineForIssue(item);
|
||||
}
|
||||
@ -60,6 +63,7 @@ const changelogTaskRunner: TaskRunner<ChangelogOptions> = async ({ milestone })
|
||||
};
|
||||
|
||||
function getMarkdownLineForIssue(item: any) {
|
||||
const githubGrafanaUrl = 'https://github.com/grafana/grafana';
|
||||
let markdown = '';
|
||||
const title = item.title.replace(/^([^:]*)/, (match, g1) => {
|
||||
return `**${g1}**`;
|
||||
|
@ -1,17 +1,11 @@
|
||||
import { Task, TaskRunner } from './task';
|
||||
import axios from 'axios';
|
||||
import GithubClient from '../utils/githubClient';
|
||||
|
||||
interface CherryPickOptions {}
|
||||
|
||||
const cherryPickRunner: TaskRunner<CherryPickOptions> = async () => {
|
||||
let client = axios.create({
|
||||
baseURL: 'https://api.github.com/repos/grafana/grafana',
|
||||
timeout: 10000,
|
||||
// auth: {
|
||||
// username: '<username>',
|
||||
// password: '<personal access token>',
|
||||
// },
|
||||
});
|
||||
const githubClient = new GithubClient();
|
||||
const client = githubClient.client;
|
||||
|
||||
const res = await client.get('/issues', {
|
||||
params: {
|
||||
|
75
scripts/cli/tasks/closeMilestone.ts
Normal file
75
scripts/cli/tasks/closeMilestone.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Task, TaskRunner } from './task';
|
||||
import GithubClient from '../utils/githubClient';
|
||||
|
||||
interface CloseMilestoneOptions {
|
||||
milestone: string;
|
||||
}
|
||||
|
||||
const closeMilestoneTaskRunner: TaskRunner<CloseMilestoneOptions> = async ({ milestone }) => {
|
||||
const githubClient = new GithubClient(true);
|
||||
|
||||
const cherryPickLabel = 'cherry-pick needed';
|
||||
const client = githubClient.client;
|
||||
|
||||
if (!/^\d+$/.test(milestone)) {
|
||||
console.log('Use milestone number not title, find number in milestone url');
|
||||
return;
|
||||
}
|
||||
|
||||
const milestoneRes = await client.get(`/milestones/${milestone}`, {});
|
||||
|
||||
const milestoneState = milestoneRes.data.state;
|
||||
|
||||
if (milestoneState === 'closed') {
|
||||
console.log('milestone already closed. ✅');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('fetching issues/PRs of the milestone ⏬');
|
||||
|
||||
// Get all the issues/PRs with the label cherry-pick
|
||||
// Every pull request is actually an issue
|
||||
const issuesRes = await client.get('/issues', {
|
||||
params: {
|
||||
state: 'closed',
|
||||
labels: cherryPickLabel,
|
||||
per_page: 100,
|
||||
milestone: milestone,
|
||||
},
|
||||
});
|
||||
|
||||
if (issuesRes.data.length < 1) {
|
||||
console.log('no issues to remove label from');
|
||||
} else {
|
||||
console.log(`found ${issuesRes.data.length} issues to remove the cherry-pick label from 🔎`);
|
||||
}
|
||||
|
||||
for (const issue of issuesRes.data) {
|
||||
// the reason for using stdout.write is for achieving 'action -> result' on
|
||||
// the same line
|
||||
process.stdout.write(`🔧removing label from issue #${issue.number} 🗑...`);
|
||||
const resDelete = await client.delete(`/issues/${issue.number}/labels/${cherryPickLabel}`, {});
|
||||
if (resDelete.status === 200) {
|
||||
process.stdout.write('done ✅\n');
|
||||
} else {
|
||||
console.log('failed ❌');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`cleaned up ${issuesRes.data.length} issues/prs ⚡️`);
|
||||
|
||||
const resClose = await client.patch(`/milestones/${milestone}`, {
|
||||
state: 'closed',
|
||||
});
|
||||
|
||||
if (resClose.status === 200) {
|
||||
console.log('milestone closed 🙌');
|
||||
} else {
|
||||
console.log('failed to close the milestone, response:');
|
||||
console.log(resClose);
|
||||
}
|
||||
};
|
||||
|
||||
export const closeMilestoneTask = new Task<CloseMilestoneOptions>();
|
||||
closeMilestoneTask.setName('Close Milestone generator task');
|
||||
closeMilestoneTask.setRunner(closeMilestoneTaskRunner);
|
66
scripts/cli/utils/githubClient.test.ts
Normal file
66
scripts/cli/utils/githubClient.test.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import GithubClient from './githubClient';
|
||||
|
||||
const fakeClient = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.GITHUB_USERNAME;
|
||||
delete process.env.GITHUB_ACCESS_TOKEN;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.GITHUB_USERNAME;
|
||||
delete process.env.GITHUB_ACCESS_TOKEN;
|
||||
});
|
||||
|
||||
describe('GithubClient', () => {
|
||||
it('should initialise a GithubClient', () => {
|
||||
const github = new GithubClient();
|
||||
expect(github).toBeInstanceOf(GithubClient);
|
||||
});
|
||||
|
||||
describe('#client', () => {
|
||||
it('it should contain a client', () => {
|
||||
const spy = jest.spyOn(GithubClient.prototype, 'createClient').mockImplementation(() => fakeClient);
|
||||
|
||||
const github = new GithubClient();
|
||||
const client = github.client;
|
||||
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
baseURL: 'https://api.github.com/repos/grafana/grafana',
|
||||
timeout: 10000,
|
||||
});
|
||||
expect(client).toEqual(fakeClient);
|
||||
});
|
||||
|
||||
describe('when the credentials are required', () => {
|
||||
it('should create the client when the credentials are defined', () => {
|
||||
const username = 'grafana';
|
||||
const token = 'averysecureaccesstoken';
|
||||
|
||||
process.env.GITHUB_USERNAME = username;
|
||||
process.env.GITHUB_ACCESS_TOKEN = token;
|
||||
|
||||
const spy = jest.spyOn(GithubClient.prototype, 'createClient').mockImplementation(() => fakeClient);
|
||||
|
||||
const github = new GithubClient(true);
|
||||
const client = github.client;
|
||||
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
baseURL: 'https://api.github.com/repos/grafana/grafana',
|
||||
timeout: 10000,
|
||||
auth: { username, password: token },
|
||||
});
|
||||
|
||||
expect(client).toEqual(fakeClient);
|
||||
});
|
||||
|
||||
describe('when the credentials are not defined', () => {
|
||||
it('should throw an error', () => {
|
||||
expect(() => {
|
||||
new GithubClient(true);
|
||||
}).toThrow(/operation needs a GITHUB_USERNAME and GITHUB_ACCESS_TOKEN environment variables/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
41
scripts/cli/utils/githubClient.ts
Normal file
41
scripts/cli/utils/githubClient.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
|
||||
const baseURL = 'https://api.github.com/repos/grafana/grafana';
|
||||
|
||||
// Encapsulates the creation of a client for the Github API
|
||||
//
|
||||
// Two key things:
|
||||
// 1. You can specify whenever you want the credentials to be required or not when imported.
|
||||
// 2. If the the credentials are available as part of the environment, even if
|
||||
// they're not required - the library will use them. This allows us to overcome
|
||||
// any API rate limiting imposed without authentication.
|
||||
|
||||
class GithubClient {
|
||||
client: AxiosInstance;
|
||||
|
||||
constructor(required = false) {
|
||||
const username = process.env.GITHUB_USERNAME;
|
||||
const token = process.env.GITHUB_ACCESS_TOKEN;
|
||||
|
||||
const clientConfig: AxiosRequestConfig = {
|
||||
baseURL: baseURL,
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
if (required && !username && !token) {
|
||||
throw new Error('operation needs a GITHUB_USERNAME and GITHUB_ACCESS_TOKEN environment variables');
|
||||
}
|
||||
|
||||
if (username && token) {
|
||||
clientConfig.auth = { username: username, password: token };
|
||||
}
|
||||
|
||||
this.client = this.createClient(clientConfig);
|
||||
}
|
||||
|
||||
private createClient(clientConfig: AxiosRequestConfig) {
|
||||
return axios.create(clientConfig);
|
||||
}
|
||||
}
|
||||
|
||||
export default GithubClient;
|
Loading…
Reference in New Issue
Block a user