Skip to content

Commit

Permalink
feat: introduce GitHub release functionality (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcoe committed May 15, 2019
1 parent 02bbbd9 commit df046b4
Show file tree
Hide file tree
Showing 14 changed files with 442 additions and 14 deletions.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

set -e

release-please candidate-issue --token=$GITHUB_TOKEN \
COMMAND = ${RELEASE_PLEASE_COMMAND:-candidate-issue}

release-please $COMMAND --token=$GITHUB_TOKEN \
--repo-url="git@github.com:$GITHUB_REPOSITORY.git" \
--package-name=$PACKAGE_NAME
7 changes: 4 additions & 3 deletions .github/main.workflow
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
workflow "Candidate Issue" {
on = "schedule(*/5 * * * *)"
resolves = ["./.github/action/candidate-issue"]
resolves = ["candidate-issue"]
}

action "./.github/action/candidate-issue" {
uses = "googleapis/release-please/.github/action/candidate-issue@master"
action "candidate-issue" {
uses = "googleapis/release-please/.github/action/release-please@master"
env = {
PACKAGE_NAME = "release-please"
RELEASE_PLEASE_COMMAND = "candidate-issue"
}
secrets = ["GITHUB_TOKEN"]
}
52 changes: 52 additions & 0 deletions __snapshots__/github-release.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
exports['GitHubRelease extractLatestReleaseNotes handles CHANGELOG with new format entries 1'] = `
### Bug Fixes
* candidate issue should only be updated every 15 minutes. ([#70](https://www.github.com/googleapis/release-please/issues/70)) ([edcd1f7](https://www.github.com/googleapis/release-please/commit/edcd1f7))
### Features
* add GitHub action for generating candidate issue ([#69](https://www.github.com/googleapis/release-please/issues/69)) ([6373aed](https://www.github.com/googleapis/release-please/commit/6373aed))
* checkbox based releases ([#77](https://www.github.com/googleapis/release-please/issues/77)) ([1e4193c](https://www.github.com/googleapis/release-please/commit/1e4193c))
`

exports['GitHubRelease extractLatestReleaseNotes handles CHANGELOG with old and new format entries 1'] = `
### Bug Fixes
* **deps:** update dependency google-auth-library to v4 ([#79](https://www.github.com/googleapis/nodejs-rcloadenv/issues/79)) ([1386b78](https://www.github.com/googleapis/nodejs-rcloadenv/commit/1386b78))
### Build System
* upgrade engines field to >=8.10.0 ([#71](https://www.github.com/googleapis/nodejs-rcloadenv/issues/71)) ([542f935](https://www.github.com/googleapis/nodejs-rcloadenv/commit/542f935))
### Miscellaneous Chores
* **deps:** update dependency gts to v1 ([#68](https://www.github.com/googleapis/nodejs-rcloadenv/issues/68)) ([972b473](https://www.github.com/googleapis/nodejs-rcloadenv/commit/972b473))
### BREAKING CHANGES
* **deps:** this will ship async/await with the generated code.
* upgrade engines field to >=8.10.0 (#71)
`

exports['GitHubRelease extractLatestReleaseNotes handles CHANGELOG with old format entries 1'] = `
03-22-2019 10:34 PDT
### New Features
- feat: add additional entity types ([#220](https://github.com/googleapis/nodejs-language/pull/220))
### Internal / Testing Changes
- chore: publish to npm using wombat ([#218](https://github.com/googleapis/nodejs-language/pull/218))
- build: use per-repo npm publish token ([#216](https://github.com/googleapis/nodejs-language/pull/216))
`
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 28 additions & 7 deletions src/bin/release-please.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,56 @@

'use strict';

import {GitHubRelease, GitHubReleaseOptions} from '../github-release';
import {ReleasePR, ReleasePROptions} from '../release-pr';
import {CandidateIssue} from '../candidate-issue';

const yargs = require('yargs');

interface YargsOptions {
describe: string;
demand: boolean;
}

interface YargsOptionsBuilder {
option(opt: string, options: YargsOptions): void;
}

yargs
.command(
'candidate-issue',
'create an issue that\'s an example of the next release', () => {},
'create an issue that\'s an example of the next release',
(yargs: YargsOptionsBuilder) => {
yargs.option('package-name', {
describe: 'name of package release is being minted for',
demand: true
});
},
async (argv: ReleasePROptions) => {
const ci = new CandidateIssue(argv);
await ci.run();
})
.command(
'release-pr', 'create a new release PR from a candidate issue',
() => {},
(yargs: YargsOptionsBuilder) => {
yargs.option('package-name', {
describe: 'name of package release is being minted for',
demand: true
});
},
async (argv: ReleasePROptions) => {
const rp = new ReleasePR(argv);
await rp.run();
})
.command(
'github-release', 'create a GitHub release from am release PR',
() => {}, async (argv: ReleasePROptions) => {})
() => {},
async (argv: GitHubReleaseOptions) => {
const gr = new GitHubRelease(argv);
await gr.createRelease();
})
.option(
'token', {describe: 'GitHub repo token', default: process.env.GH_TOKEN})
.option('package-name', {
describe: 'name of package release is being minted for',
required: true
})
.option(
'repo-url',
{describe: 'GitHub URL to generate release for', required: true})
Expand Down
89 changes: 89 additions & 0 deletions src/github-release.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* Copyright 2019 Google LLC. All Rights Reserved.
*
* Licensed 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 chalk from 'chalk';
import {checkpoint, CheckpointType} from './checkpoint';
// import {ConventionalCommits} from './conventional-commits';
import {GitHub, GitHubReleasePR} from './github';

const parseGithubRepoUrl = require('parse-github-repo-url');

export interface GitHubReleaseOptions {
label: string;
repoUrl: string;
token: string;
}

export class GitHubRelease {
changelogPath: string;
gh: GitHub;
label: string;
repoUrl: string;
token: string|undefined;

constructor(options: GitHubReleaseOptions) {
this.label = options.label;
this.repoUrl = options.repoUrl;
this.token = options.token;

this.changelogPath = 'CHANGELOG.md';

this.gh = this.gitHubInstance();
}

async createRelease() {
const gitHubReleasePR: GitHubReleasePR|undefined =
await this.gh.latestReleasePR(this.label);
if (gitHubReleasePR) {
checkpoint(
`found release branch ${chalk.green(gitHubReleasePR.version)} at ${
chalk.green(gitHubReleasePR.sha)}`,
CheckpointType.Success);

const changelogContents =
await this.gh.getFileContents(this.changelogPath);
const latestReleaseNotes = GitHubRelease.extractLatestReleaseNotes(
changelogContents, gitHubReleasePR.version);
checkpoint(
`found release notes: \n---\n${
chalk.grey(latestReleaseNotes)}\n---\n`,
CheckpointType.Success);

await this.gh.createRelease(
gitHubReleasePR.version, gitHubReleasePR.sha, latestReleaseNotes);
await this.gh.removeLabel(this.label, gitHubReleasePR.number);
} else {
checkpoint('no recent release PRs found', CheckpointType.Failure);
}
}

private gitHubInstance(): GitHub {
const [owner, repo] = parseGithubRepoUrl(this.repoUrl);
return new GitHub({token: this.token, owner, repo});
}

static extractLatestReleaseNotes(changelogContents: string, version: string):
string {
version = version.replace(/^v/, '');
const latestRe = new RegExp(
`## v?\\[?${version}[^\\n]*\\n(.*?)(\\n##\\s|($(?![\r\n])))`, 'ms');
const match = changelogContents.match(latestRe);
if (!match) {
throw Error('could not find changelog entry corresponding to release PR');
}
return match[1];
}
}
69 changes: 67 additions & 2 deletions src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
*/

import * as Octokit from '@octokit/rest';
import {IssuesListResponseItem, PullsCreateResponse, ReposListTagsResponseItem, Response} from '@octokit/rest';
import {IssuesListResponseItem, PullsCreateResponse, PullsListResponseItem, ReposListTagsResponseItem, Response} from '@octokit/rest';
import chalk from 'chalk';
import * as semver from 'semver';

import {checkpoint, CheckpointType} from './checkpoint';
import {Update} from './updaters/update';

const VERSION_FROM_BRANCH_RE = /^.*:[^-]+-(.*)$/;

interface GitHubOptions {
token?: string;
owner: string;
Expand All @@ -34,6 +36,12 @@ export interface GitHubTag {
version: string;
}

export interface GitHubReleasePR {
number: number;
version: string;
sha: string;
}

interface GitHubPR {
branch: string;
version: string;
Expand Down Expand Up @@ -65,7 +73,6 @@ export class GitHub {
})) {
for (let i = 0, commit; response.data[i] !== undefined; i++) {
commit = response.data[i];
console.info(JSON.stringify(commit, null, 2));
if (commit.sha === sha) {
return commits;
} else {
Expand Down Expand Up @@ -98,6 +105,36 @@ export class GitHub {
} as GitHubTag;
}

async latestReleasePR(releaseLabel: string, perPage = 100):
Promise<GitHubReleasePR|undefined> {
const pullsResponse: Response<PullsListResponseItem[]> =
await this.octokit.pulls.list({
owner: this.owner,
repo: this.repo,
state: 'closed',
per_page: perPage
});
for (let i = 0, pull; i < pullsResponse.data.length; i++) {
pull = pullsResponse.data[i];
for (let ii = 0, label; ii < pull.labels.length; ii++) {
label = pull.labels[ii];
if (label.name === releaseLabel) {
// it's expected that a release PR will have a
// HEAD matching the format repo:release-v1.0.0.
if (!pull.head) continue;
const match = pull.head.label.match(VERSION_FROM_BRANCH_RE);
if (!match || !pull.merge_commit_sha) continue;
return {
number: pull.number,
sha: pull.merge_commit_sha,
version: match[1]
} as GitHubReleasePR;
}
}
}
return undefined;
}

private async allTags(perPage = 100):
Promise<{[version: string]: GitHubTag;}> {
const tags: {[version: string]: GitHubTag;} = {};
Expand Down Expand Up @@ -311,6 +348,34 @@ export class GitHub {
}
return ref;
}
async getFileContents(path: string): Promise<string> {
const content = await this.octokit.repos.getContents(
{owner: this.owner, repo: this.repo, path});
return Buffer.from(content.data.content, 'base64').toString('utf8');
}
async createRelease(version: string, sha: string, releaseNotes: string) {
checkpoint(`creating release ${version}`, CheckpointType.Success);
await this.octokit.repos.createRelease({
owner: this.owner,
repo: this.repo,
tag_name: version,
target_commitish: sha,
body: releaseNotes,
name: version
});
}
async removeLabel(label: string, prNumber: number) {
checkpoint(
`removing label ${chalk.green(label)} from ${
chalk.green('' + prNumber)}`,
CheckpointType.Success);
await this.octokit.issues.removeLabel({
owner: this.owner,
repo: this.repo,
issue_number: prNumber,
name: label
});
}
}

class AuthError extends Error {
Expand Down
20 changes: 20 additions & 0 deletions system-test/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,24 @@ describe('GitHub', () => {
issue.number.should.be.gt(0);
});
});

describe('latestReleasePR', () => {
it('returns the latest closed PR with "autorelease: pending" tag',
async () => {
const gh = new GitHub({owner: 'bcoe', repo: 'node-25650-bug'});
const pr = await nockBack('latest-release-pr.json')
.then((nbr: NockBackResponse) => {
return gh.latestReleasePR('autorelease: pending')
.then((res) => {
nbr.nockDone();
return res;
});
});
pr.should.eql({
version: 'v1.0.0',
sha: '5a4cdf2c1370937b363ce10962bf30e2ee84202e',
number: 3
});
});
});
});
18 changes: 18 additions & 0 deletions test/fixtures/CHANGELOG-new.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Changelog

[npm history][1]

[1]: https://www.npmjs.com/package/release-please?activeTab=versions

## [1.2.0](https://www.github.com/googleapis/release-please/compare/v1.1.0...v1.2.0) (2019-05-10)


### Bug Fixes

* candidate issue should only be updated every 15 minutes. ([#70](https://www.github.com/googleapis/release-please/issues/70)) ([edcd1f7](https://www.github.com/googleapis/release-please/commit/edcd1f7))


### Features

* add GitHub action for generating candidate issue ([#69](https://www.github.com/googleapis/release-please/issues/69)) ([6373aed](https://www.github.com/googleapis/release-please/commit/6373aed))
* checkbox based releases ([#77](https://www.github.com/googleapis/release-please/issues/77)) ([1e4193c](https://www.github.com/googleapis/release-please/commit/1e4193c))

0 comments on commit df046b4

Please sign in to comment.