Skip to content
32 changes: 26 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,40 @@ Draft Example
## Getting Started
Chronicler is a simple express.js app that receives GitHub Webhook events via its `/webhooks` route. You'll need to clone and set the app up on a server or cloud service (e.x. Google Cloud Platform, AWS, Digital Ocean) to use it for your own projects.

### Setup

#### Environment Variables
### Environment Variables
The following variables must be set up and available to Chronicler via the node.js `process.env` object.
It is possible to run both as a Github app or as an individual user.
To run as a Github app: SET AUTH_AS_APP=true, APP_IDENTIFIER and SECRET to their respective values. GH_TOKEN can then be omitted. **Also make sure to set the private key for the app (listed under basic information about the app) in /run/secrets/private-key**
To run the service as a user: SET GH_TOKEN and SECRET to their respective values. AUTH_AS_APP and PRIVATE_KEY, APP_IDENTIFIER can then be omitted.

**Variable Name** | **Description** | **Default**
--- | --- | :---:
`GH_TOKEN` | The Github [personal access token](https://github.com/settings/tokens) to use for this app. Used for authentication when making calls to the GitHub API. | -
`SECRET` | The GitHub Webhook secret passed along with every Webhooks request. Allows your app to authenticate the request and make sure the request is coming from a trusted source. Generate a [random string with high entropy](https://developer.github.com/webhooks/securing/#setting-your-secret-token) for your secure secret or create one using an online [generator](https://randomkeygen.com/). | -
`SECRET` (required) | The GitHub Webhook secret passed along with every Webhooks request. Allows your app to authenticate the request and make sure the request is coming from a trusted source. Generate a [random string with high entropy](https://developer.github.com/webhooks/securing/#setting-your-secret-token) for your secure secret or create one using an online [generator](https://randomkeygen.com/). | -
`AUTH_AS_APP` (required if github app)| Whether to authenticate as a Github app | false
`APP_IDENTIFIER` (required if github app)| The unique identifier for the Github app, listed under basic information about the app | -
`GH_TOKEN` (required if set as a user)| The Github [personal access token](https://github.com/settings/tokens) to use for this app. Used for authentication when making calls to the GitHub API. | -
`APP_NAME` (optional) | Name of the app to send as the `User-Agent` value in the API requests. | `Chronicler`
`PORT` (optional) | App port. | `8080`

### Setup as Github app

#### Creating the app
A Github app can be created on both a user and an organization. The latter is recommended when working together with a team whose members might change. Thus the service is uncoupled from a specific user that might leave.
To set it up, go to https://github.com/organizations/`<organization>`/settings/apps and create new. Fill out the required fields and give it the following permissions:

1. Repository contents **Read & Write**
2. Repository metadata **Read**
3. Pull requests **Read**
- Subscribe to events: Pull Requests

When created, go to the general-tab for the app and collect set the **Webhook secret (SECRET)** the **PRIVATE_KEY** and **APP_IDENTIFIER** to be set in your environment.

Lastly. Install the app on your organization/repository through the organization/repository settings

### Setup as User

##### A Note on Personal Access Tokens
Chronicler requires a personal access token (PAT) to create or edit a release draft via the GitHub API. PATs are tied to a user's account. For GitHub teams or organizations using Chronicler we reccommend creating a dedicated GitHub account that owns the PAT. By creating the PAT with a dedicated GitHub account instead of with a team member's account, you can avoid interuptions to Chronicler if the team member leaves or is removed from the organization.
When setting up Chronicler as a user it requires a personal access token (PAT) to create or edit a release draft via the GitHub API. PATs are tied to a user's account. For GitHub teams or organizations using Chronicler we reccommend creating a dedicated GitHub account that owns the PAT. By creating the PAT with a dedicated GitHub account instead of with a team member's account, you can avoid interuptions to Chronicler if the team member leaves or is removed from the organization.

To generate a new PAT for Chronicler, go to your [account settings](https://github.com/settings/tokens/new). Add a "token description" (e.x "chronicler-app") and grant it `repo` scope.

Expand Down
2 changes: 1 addition & 1 deletion __tests__/auth.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from 'ava'
import auth from '../src/helpers/auth'
import { auth } from '../src/auth/webhook'
import { mockRequest } from './fixtures/webhook-event'

// fake request object for auth test
Expand Down
37 changes: 37 additions & 0 deletions __tests__/githubApp.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import test from 'ava'
import sinon from 'sinon'
import proxyquire from 'proxyquire'

let githubApp, signStub, momentStub
test.beforeEach(() => {
process.env.APP_IDENTIFIER = 'MOCKED_APP_IDENTIFIER'
const fs = {
readFileSync: sinon.stub().returns('MOCKED_PRIVATE_KEY')
}
signStub = sinon.stub().returns('jwt123')
momentStub = {
unix: sinon.stub()
}
momentStub.add = () => momentStub
githubApp = proxyquire('../src/auth/githubApp.js', {
'jsonwebtoken': {
sign: signStub
},
'moment': () => momentStub,
'fs': fs
})
})

test('calls jwt encode with iat, exp, iss', t => {
momentStub.unix.onFirstCall().returns(1)
momentStub.unix.onSecondCall().returns(2)
t.is(githubApp.auth(), 'jwt123')

t.true(signStub.calledWith({
iss: 'MOCKED_APP_IDENTIFIER',
iat: 1,
exp: 2,
}, 'MOCKED_PRIVATE_KEY', {
algorithm: 'RS256'
}))
})
30 changes: 30 additions & 0 deletions __tests__/installation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import test from 'ava'
import sinon from 'sinon'
import proxyquire from 'proxyquire'

let installation, axiosStub
test.beforeEach(() => {
axiosStub = sinon.stub().resolves({
data: {
token: 'accesstokenfromgithub'
}
})
installation = proxyquire('../src/auth/installation.js', {
'axios': axiosStub
})
})

test('requests bearer token with installation ID and jwt', async t => {
const id = 5
const req = { body: { installation: { id } } }

t.is(await installation.auth(req, 'jwttokenstring'), 'accesstokenfromgithub')
t.true(axiosStub.calledWith({
url: `https://api.github.com/app/installations/${id}/access_tokens`,
headers: {
Authorization: 'Bearer ' + 'jwttokenstring',
Accept: 'application/vnd.github.machine-man-preview+json'
},
method: 'POST'
}))
})
28 changes: 11 additions & 17 deletions __tests__/pr.test.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,43 @@
import test from 'ava'
import webhookData from './fixtures/webhook-event'
import { drafts } from './fixtures/releases'
import {
isTooOld,
getReleasesUrl,
getSingleReleaseUrl,
getPrDesc,
getPrData,
updateReleaseDraft
} from '../src/helpers/pr'

const pr = getPrData(webhookData)
const draft = drafts[0]
import WebhookHandler from '../src/helpers/pr'

const draft = drafts[0]
let prHelper, pr
test.beforeEach(() => {
process.env.GH_TOKEN = 'MOCK_TOKEN'
prHelper = new WebhookHandler('MOCK_TOKEN')
pr = prHelper.getPrData(webhookData)
})

test('isTooOld returns true if PR was merged more than 5 minutes ago', t => {
t.true(isTooOld(pr.merged_at))
t.true(prHelper.isTooOld(pr.merged_at))
})

test('isTooOld returns false if PR was merged within 5 minutes', t => {
// the current time
t.false(isTooOld(new Date()))
t.false(prHelper.isTooOld(new Date()))
})

test('getReleasesUrl returns the url for the github repo releases endpoint', t => {
const expected =
'https://api.github.com/repos/NYTimes/Chronicler/releases?access_token=MOCK_TOKEN'

t.is(getReleasesUrl(pr), expected)
t.is(prHelper.getReleasesUrl(pr), expected)
})

test('getSingleReleaseUrl returns the github release url for a given release id', t => {
const expected =
'https://api.github.com/repos/NYTimes/Chronicler/releases/9797693?access_token=MOCK_TOKEN'

t.is(getSingleReleaseUrl(pr, draft), expected)
t.is(prHelper.getSingleReleaseUrl(pr, draft), expected)
})

test('getPrDesc should return the formatted description for a pull request with title and number', t => {
const expected = '- Update README.md (#16)'

t.is(getPrDesc(pr), expected)
t.is(prHelper.getPrDesc(pr), expected)
})

test('getPrData should return a pull request object with the repository url', t => {
Expand All @@ -57,7 +51,7 @@ test('getPrData should return a pull request object with the repository url', t
test('updateReleaseDraft should append the pull request title and number to existing draft', t => {
const expect =
'- Title Change (#4) - Give Props (#3) - Test permissions (#6) - Another Permissions test (#7) - Update README.md (#10) - Update README.md (#12) - Update README.md (#13) - Update README.md (#14) - Update README.md (#15) - Update README.md (#16) - Update README.md (#16) - Add webhook url to readme (#5)\n- Update README.md (#16)'
t.is(updateReleaseDraft(pr, draft), expect)
t.is(prHelper.updateReleaseDraft(pr, draft), expect)
})

test.todo(
Expand Down
Loading