From 1e4193cd89729e217b824fdefb6cedc2bbc05534 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Fri, 10 May 2019 10:13:16 -0700 Subject: [PATCH] feat: checkbox based releases (#77) --- src/bin/release-please.ts | 4 +-- src/candidate-issue.ts | 63 ++++++++++++++++++++++++++++----------- src/github.ts | 33 ++++++++------------ src/mint-release.ts | 16 ++++++---- system-test/github.ts | 2 ++ 5 files changed, 74 insertions(+), 44 deletions(-) diff --git a/src/bin/release-please.ts b/src/bin/release-please.ts index a9b837625..c936b0dfc 100644 --- a/src/bin/release-please.ts +++ b/src/bin/release-please.ts @@ -19,7 +19,7 @@ 'use strict'; import {MintRelease, MintReleaseOptions} from '../mint-release'; -import {CandidateIssue, CandidateIssueOptions} from '../candidate-issue'; +import {CandidateIssue} from '../candidate-issue'; const yargs = require('yargs'); @@ -33,7 +33,7 @@ yargs .command( 'candidate-issue', 'create an issue that\'s an example of the next release', () => {}, - async (argv: CandidateIssueOptions) => { + async (argv: MintReleaseOptions) => { const ci = new CandidateIssue(argv); await ci.run(); }) diff --git a/src/candidate-issue.ts b/src/candidate-issue.ts index a5167e07f..39aa91ae1 100644 --- a/src/candidate-issue.ts +++ b/src/candidate-issue.ts @@ -14,35 +14,33 @@ * limitations under the License. */ +import {IssuesListResponseItem} from '@octokit/rest'; import * as semver from 'semver'; import {checkpoint, CheckpointType} from './checkpoint'; import {ConventionalCommits} from './conventional-commits'; import {GitHub, GitHubTag} from './github'; import {ReleaseCandidate, ReleaseType} from './mint-release'; +import {MintRelease, MintReleaseOptions} from './mint-release'; const parseGithubRepoUrl = require('parse-github-repo-url'); -export interface CandidateIssueOptions { - bumpMinorPreMajor?: boolean; - token?: string; - repoUrl: string; - packageName: string; - releaseType: ReleaseType; -} - const ISSUE_TITLE = 'chore(release): proposal for next release'; +const CHECKBOX = '* [ ] **Should I create this release for you :robot:?**'; +const CHECK_REGEX = /\[x]/; export class CandidateIssue { - bumpMinorPreMajor?: boolean; + label: string; gh: GitHub; + bumpMinorPreMajor?: boolean; repoUrl: string; token: string|undefined; packageName: string; releaseType: ReleaseType; - constructor(options: CandidateIssueOptions) { + constructor(options: MintReleaseOptions) { this.bumpMinorPreMajor = options.bumpMinorPreMajor || false; + this.label = options.label; this.repoUrl = options.repoUrl; this.token = options.token; this.packageName = options.packageName; @@ -50,6 +48,7 @@ export class CandidateIssue { this.gh = this.gitHubInstance(); } + async run() { switch (this.releaseType) { case ReleaseType.Node: @@ -78,7 +77,37 @@ export class CandidateIssue { previousTag: candidate.previousTag }); - await this.gh.openIssue(ISSUE_TITLE, this.bodyTemplate(changelogEntry)); + const issue: IssuesListResponseItem|undefined = + await this.gh.findExistingReleaseIssue(ISSUE_TITLE); + let body: string = this.bodyTemplate(changelogEntry); + + if (issue) { + if (CHECK_REGEX.test(issue.body)) { + // if the checkox has been clicked for a release + // mint the release. + checkpoint( + `release checkbox was checked, creating release`, + CheckpointType.Success); + const mr = new MintRelease({ + bumpMinorPreMajor: this.bumpMinorPreMajor, + label: this.label, + token: this.token, + repoUrl: this.repoUrl, + packageName: this.packageName, + releaseType: this.releaseType + }); + const prNumber = await mr.run(); + body = body.replace(CHECKBOX, `**release created at #${prNumber}**`); + } else if (issue.body === body) { + // don't update the issue if the content is the same for the release. + checkpoint( + `skipping update to #${issue.number}, no change to body`, + CheckpointType.Failure); + return; + } + } + + await this.gh.openIssue(ISSUE_TITLE, body, issue); } private async coerceReleaseCandidate( @@ -115,16 +144,16 @@ export class CandidateIssue { } private bodyTemplate(changelogEntry: string): string { - return `_:robot: This issue was created by robots! :robot:._ - -Its purpose is to show you what the next release of **${ - this.packageName}** would look like... _If we published it right now._ - -If you're a maintainer, and would like create a PR for this release, simply comment on this issue. + return `_:robot: Here's what the next release of **${ + this.packageName}** would look like._ --- ${changelogEntry} + +--- + +${CHECKBOX} `; } } diff --git a/src/github.ts b/src/github.ts index 55e1622f3..229e88b80 100644 --- a/src/github.ts +++ b/src/github.ts @@ -37,6 +37,8 @@ export interface GitHubTag { interface GitHubPR { branch: string; version: string; + title: string; + body: string; sha: string; updates: Update[]; } @@ -125,17 +127,8 @@ export class GitHub { }); } - async openIssue(title: string, body: string) { - const issue: IssuesListResponseItem|undefined = - await this.findExistingReleaseIssue(title); + async openIssue(title: string, body: string, issue?: IssuesListResponseItem) { if (issue) { - // don't update the issue if the content is the same for the release. - if (issue.body === body) { - checkpoint( - `skipping update to #${issue.number}, no change to body`, - CheckpointType.Failure); - return; - } checkpoint(`updating issue #${issue.number}`, CheckpointType.Success); this.octokit.issues.update({ owner: this.owner, @@ -150,20 +143,19 @@ export class GitHub { } } - async findExistingReleaseIssue(title: string): + async findExistingReleaseIssue(title: string, perPage = 100): Promise { - let paged = 0; - const MAX_PAGED_ISSUES = - 256; // why 256? seemed like it won't be the first page. + const paged = 0; try { - for await (const response of this.octokit.paginate.iterator( - {method: 'GET', url: `/repos/${this.owner}/${this.repo}/issues`})) { + for await (const response of this.octokit.paginate.iterator({ + method: 'GET', + url: `/repos/${this.owner}/${this.repo}/issues?per_page=${perPage}` + })) { for (let i = 0, issue; response.data[i] !== undefined; i++) { const issue: IssuesListResponseItem = response.data[i]; if (issue.title.indexOf(title) !== -1 && issue.state === 'open') { return issue; } - if ((paged++) > MAX_PAGED_ISSUES) return undefined; } } } catch (err) { @@ -230,14 +222,15 @@ export class GitHub { await this.updateFiles(options.updates, options.branch, refName); - const title = `chore: release ${options.version}`; checkpoint( - `open pull-request: ${chalk.yellow(title)}`, CheckpointType.Success); + `open pull-request: ${chalk.yellow(options.title)}`, + CheckpointType.Success); const resp: Response = await this.octokit.pulls.create({ owner: this.owner, repo: this.repo, - title, + title: options.title, + body: options.body, head: options.branch, base: 'master' }); diff --git a/src/mint-release.ts b/src/mint-release.ts index 2086cef6f..19f29c0ef 100644 --- a/src/mint-release.ts +++ b/src/mint-release.ts @@ -66,16 +66,15 @@ export class MintRelease { this.gh = this.gitHubInstance(); } - async run() { + async run(): Promise { switch (this.releaseType) { case ReleaseType.Node: - await this.nodeRelease(); - break; + return await this.nodeRelease(); default: throw Error('unknown release type'); } } - private async nodeRelease() { + private async nodeRelease(): Promise { const latestTag: GitHubTag|undefined = await this.gh.latestTag(); const commits: string[] = await this.commits(latestTag ? latestTag.sha : undefined); @@ -117,13 +116,20 @@ export class MintRelease { })); const sha = this.shaFromCommits(commits); + const title = `chore: release ${candidate.version}`; + const body = + `:robot: I have created a release \\*beep\\* \\*boop\\* \n---\n${ + changelogEntry}`; const pr: number = await this.gh.openPR({ branch: `release-v${candidate.version}`, version: candidate.version, sha, - updates + updates, + title, + body }); await this.gh.addLabel(pr, this.label); + return pr; } private async coerceReleaseCandidate( cc: ConventionalCommits, diff --git a/system-test/github.ts b/system-test/github.ts index 66bb49e7b..1ee626ccd 100644 --- a/system-test/github.ts +++ b/system-test/github.ts @@ -123,6 +123,8 @@ describe('GitHub', () => { branch: 'greenkeeper/@types/node-10.10.0', sha: 'abc123', version: '1.3.0', + title: 'version 1.3.0', + body: 'my PR body', updates: [] }) .catch(err => {