diff --git a/.github/main.workflow b/.github/main.workflow index 2b497832b..7fd688a53 100644 --- a/.github/main.workflow +++ b/.github/main.workflow @@ -1,5 +1,5 @@ workflow "Candidate Issue" { - on = "schedule(*/5 * * * *)" + on = "schedule(*/8 * * * *)" resolves = ["candidate-issue"] } @@ -11,3 +11,17 @@ action "candidate-issue" { } secrets = ["GITHUB_TOKEN"] } + +workflow "Detect Checked" { + on = "schedule(*/4 * * * *)" + resolves = ["detect-checked"] +} + +action "detect-checked" { + uses = "googleapis/release-please/.github/action/release-please@master" + env = { + PACKAGE_NAME = "release-please" + RELEASE_PLEASE_COMMAND = "detect-checked" + } + secrets = ["GITHUB_TOKEN"] +} diff --git a/package-lock.json b/package-lock.json index 9d4dbd9bc..4350f5728 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "release-please", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/bin/release-please.ts b/src/bin/release-please.ts index 085033624..8af4d586a 100644 --- a/src/bin/release-please.ts +++ b/src/bin/release-please.ts @@ -27,7 +27,8 @@ const yargs = require('yargs'); interface YargsOptions { describe: string; - demand: boolean; + demand?: boolean; + default?: string; } interface YargsOptionsBuilder { @@ -47,11 +48,47 @@ yargs .option('repo-url', { describe: 'GitHub URL to generate release for', demand: true + }) + .option('label', { + default: 'autorelease: pending', + describe: + 'label that will be added to PR created from candidate issue' + }) + .option('issue-label', { + default: 'release-candidate,type: process', + describe: 'label(s) to add to candidate issue' + }); + }, + async (argv: ReleasePROptions) => { + const ci = new CandidateIssue(argv); + await ci.updateOrCreateIssue(); + }) + .command( + 'detect-checked', + 'has the release checkbox been checked on candidate issue? if so create a PR', + (yargs: YargsOptionsBuilder) => { + yargs + .option('package-name', { + describe: 'name of package release is being minted for', + demand: true + }) + .option('repo-url', { + describe: 'GitHub URL to generate release for', + demand: true + }) + .option('label', { + default: 'autorelease: pending', + describe: + 'label that will be added to PR created from candidate issue' + }) + .option('issue-label', { + default: 'release-candidate,type: process', + describe: 'label(s) to add to candidate issue' }); }, async (argv: ReleasePROptions) => { const ci = new CandidateIssue(argv); - await ci.run(); + await ci.detectChecked(); }) .command( 'release-pr', 'create a new release PR from a candidate issue', @@ -64,6 +101,10 @@ yargs .option('repo-url', { describe: 'GitHub URL to generate release for', demand: true + }) + .option('label', { + default: 'autorelease: pending', + describe: 'label(s) to add to generated PR' }); }, async (argv: ReleasePROptions) => { @@ -73,9 +114,15 @@ yargs .command( 'github-release', 'create a GitHub release from am release PR', (yargs: YargsOptionsBuilder) => { - yargs.option( - 'repo-url', - {describe: 'GitHub URL to generate release for', demand: true}); + yargs + .option('repo-url', { + describe: 'GitHub URL to generate release for', + demand: true + }) + .option('label', { + default: 'autorelease: pending', + describe: 'label to remove from release PR' + }); }, async (argv: GitHubReleaseOptions) => { const gr = new GitHubRelease(argv); @@ -94,7 +141,7 @@ yargs console.info(chalk.green( '----- put the content below in .github/main.workflow -----')); console.info(`workflow "Candidate Issue" { - on = "schedule(*/5 * * * *)" + on = "schedule(*/8 * * * *)" resolves = ["candidate-issue"] } @@ -107,6 +154,20 @@ action "candidate-issue" { secrets = ["GITHUB_TOKEN"] } +workflow "Detect Checked" { + on = "schedule(*/4 * * * *)" + resolves = ["detect-checked"] +} + +action "detect-checked" { + uses = "googleapis/release-please/.github/action/release-please@master" + env = { + PACKAGE_NAME = "${argv.packageName}" + RELEASE_PLEASE_COMMAND = "detect-checked" + } + secrets = ["GITHUB_TOKEN"] +} + workflow "GitHub Release" { on = "push" resolves = ["github-release"] @@ -139,10 +200,6 @@ action "github-release" { default: false, type: 'boolean' }) - .option('label', { - default: 'autorelease: pending', - describe: 'label to add to generated PR' - }) .demandCommand(1) .strict(true) .parse(); diff --git a/src/candidate-issue.ts b/src/candidate-issue.ts index be63f680f..d1f383789 100644 --- a/src/candidate-issue.ts +++ b/src/candidate-issue.ts @@ -31,17 +31,25 @@ const CHECKBOX = '* [ ] **Should I create this release for you :robot:?**'; const CHECK_REGEX = /\[x]/; export class CandidateIssue { - label: string; + releaseLabel: string; gh: GitHub; bumpMinorPreMajor?: boolean; repoUrl: string; + issueLabels: string[]; token: string|undefined; packageName: string; releaseType: ReleaseType; constructor(options: ReleasePROptions) { this.bumpMinorPreMajor = options.bumpMinorPreMajor || false; - this.label = options.label; + this.releaseLabel = options.label; + // labels to apply to the candidate issue being + // created or updated. + if (options.issueLabel) { + this.issueLabels = options.issueLabel.split(','); + } else { + this.issueLabels = []; + } this.repoUrl = options.repoUrl; this.token = options.token; this.packageName = options.packageName; @@ -50,17 +58,36 @@ export class CandidateIssue { this.gh = this.gitHubInstance(); } - async run() { + async detectChecked() { + const issue: IssuesListResponseItem|undefined = + await this.gh.findExistingReleaseIssue( + ISSUE_TITLE, this.issueLabels.join(',')); + if (issue) { + checkpoint( + `release candidate #${issue.number} found`, CheckpointType.Success); + if (CHECK_REGEX.test(issue.body)) { + checkpoint('release checkbox was checked', CheckpointType.Success); + await this.updateOrCreateIssue(issue); + } else { + checkpoint( + `candidate #${issue.number} not checked`, CheckpointType.Failure); + } + } else { + checkpoint(`no release candidate found`, CheckpointType.Failure); + } + } + + async updateOrCreateIssue(issue?: IssuesListResponseItem) { switch (this.releaseType) { case ReleaseType.Node: - await this.nodeReleaseCandidate(); + await this.nodeReleaseCandidate(issue); break; default: throw Error('unknown release type'); } } - private async nodeReleaseCandidate() { + private async nodeReleaseCandidate(issue?: IssuesListResponseItem) { const latestTag: GitHubTag|undefined = await this.gh.latestTag(); const commits: string[] = await this.commits(latestTag ? latestTag.sha : undefined); @@ -78,8 +105,9 @@ export class CandidateIssue { previousTag: candidate.previousTag }); - const issue: IssuesListResponseItem|undefined = - await this.gh.findExistingReleaseIssue(ISSUE_TITLE); + issue = issue || + await this.gh.findExistingReleaseIssue( + ISSUE_TITLE, this.issueLabels.join(',')); let body: string = CandidateIssue.bodyTemplate(changelogEntry, this.packageName); @@ -92,7 +120,7 @@ export class CandidateIssue { CheckpointType.Success); const rp = new ReleasePR({ bumpMinorPreMajor: this.bumpMinorPreMajor, - label: this.label, + label: this.releaseLabel, token: this.token, repoUrl: this.repoUrl, packageName: this.packageName, @@ -111,7 +139,7 @@ export class CandidateIssue { } } - await this.gh.openIssue(ISSUE_TITLE, body, issue); + await this.gh.openIssue(ISSUE_TITLE, body, this.issueLabels, issue); } private async coerceReleaseCandidate( diff --git a/src/github.ts b/src/github.ts index f97da47f6..e6d171856 100644 --- a/src/github.ts +++ b/src/github.ts @@ -152,42 +152,42 @@ export class GitHub { return tags; } - async addLabel(pr: number, label: string) { + async addLabels(pr: number, labels: string[]) { checkpoint( - `adding label ${chalk.green(label)} to https://github.com/${ + `adding label ${chalk.green(labels.join(','))} to https://github.com/${ this.owner}/${this.repo}/pull/${pr}`, CheckpointType.Success); - await this.octokit.issues.addLabels({ - owner: this.owner, - repo: this.repo, - issue_number: pr, - labels: [label] - }); + await this.octokit.issues.addLabels( + {owner: this.owner, repo: this.repo, issue_number: pr, labels}); } - async openIssue(title: string, body: string, issue?: IssuesListResponseItem) { + async openIssue( + title: string, body: string, labels: string[], + issue?: IssuesListResponseItem) { if (issue) { checkpoint(`updating issue #${issue.number}`, CheckpointType.Success); this.octokit.issues.update({ owner: this.owner, repo: this.repo, body, - issue_number: issue.number + issue_number: issue.number, + labels }); } else { checkpoint(`creating new release proposal issue`, CheckpointType.Success); this.octokit.issues.create( - {owner: this.owner, repo: this.repo, title, body}); + {owner: this.owner, repo: this.repo, title, body, labels}); } } - async findExistingReleaseIssue(title: string, perPage = 100): + async findExistingReleaseIssue(title: string, label: string, perPage = 100): Promise { const paged = 0; try { for await (const response of this.octokit.paginate.iterator({ method: 'GET', - url: `/repos/${this.owner}/${this.repo}/issues?per_page=${perPage}` + url: `/repos/${this.owner}/${this.repo}/issues?per_page=${ + perPage}&labels=${label}` })) { for (let i = 0, issue; response.data[i] !== undefined; i++) { const issue: IssuesListResponseItem = response.data[i]; diff --git a/src/release-pr.ts b/src/release-pr.ts index 49890532a..2978e63cf 100644 --- a/src/release-pr.ts +++ b/src/release-pr.ts @@ -33,6 +33,7 @@ export enum ReleaseType { export interface ReleasePROptions { bumpMinorPreMajor?: boolean; label: string; + issueLabel?: string; token?: string; repoUrl: string; packageName: string; @@ -128,7 +129,7 @@ export class ReleasePR { title, body }); - await this.gh.addLabel(pr, this.label); + await this.gh.addLabels(pr, [this.label]); return pr; } private async coerceReleaseCandidate( diff --git a/system-test/github.ts b/system-test/github.ts index fb09e8c8b..f64226e16 100644 --- a/system-test/github.ts +++ b/system-test/github.ts @@ -139,15 +139,17 @@ describe('GitHub', () => { describe('findExistingReleaseIssue', () => { it('returns an open issue matching the title provided', async () => { const gh = new GitHub({owner: 'bcoe', repo: 'node-25650-bug'}); - const issue = - await nockBack('find-matching-issue.json') - .then((nbr: NockBackResponse) => { - return gh.findExistingReleaseIssue('this issue is a fixture') - .then((res) => { - nbr.nockDone(); - return res; - }); - }); + const issue = await nockBack('find-matching-issue.json') + .then((nbr: NockBackResponse) => { + return gh + .findExistingReleaseIssue( + 'this issue is a fixture', + 'type: process,release-candidate') + .then((res) => { + nbr.nockDone(); + return res; + }); + }); issue.number.should.be.gt(0); }); });