Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support github app flow, pave way for checks API #11

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
9 changes: 9 additions & 0 deletions infrastructure/testing/jestSetupFramework.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const moxios = require('moxios')

beforeEach(() => {
moxios.install()
})

afterEach(() => {
moxios.uninstall()
})
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"setupFiles": [
"./infrastructure/testing/jestSetupFile.js"
],
"setupTestFrameworkScriptFile": "./infrastructure/testing/jestSetupFramework.js",
"collectCoverage": true,
"coverageDirectory": "artifacts/test_results/jest/coverage"
},
Expand All @@ -30,7 +31,8 @@
"lodash.merge": "^4.6.1",
"mustache-express": "^1.2.5",
"serverless-dynamodb-local": "^0.2.30",
"serverless-http": "^1.5.5"
"serverless-http": "^1.5.5",
"uuid": "^3.2.1"
},
"devDependencies": {
"coveralls": "^3.0.1",
Expand All @@ -44,6 +46,7 @@
"jest": "^22.4.3",
"jest-junit": "^4.0.0",
"lint-staged": "^7.0.0",
"moxios": "^0.4.0",
"prettier": "^1.10.2",
"serverless": "^1.26.1",
"serverless-domain-manager": "^2.3.6",
Expand Down
20 changes: 10 additions & 10 deletions serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@ custom:
port: 8000
migrate: true
inMemory: true
githubClientId:
dev: '04fcf325dd26ca2a159f'
stage: '04fcf325dd26ca2a159f'
prod: '04fcf325dd26ca2a159f'
githubClientSecret:
dev: ${env:GITHUB_CLIENT_SECRET}
stage: ${env:GITHUB_CLIENT_SECRET}
prod: ${env:GITHUB_CLIENT_SECRET}
customDomain:
domainName: service.bundlewatch.io
basePath: ''
Expand All @@ -48,8 +40,10 @@ provider:
- { "Fn::GetAtt": ["StoreTable", "Arn" ] }
environment:
STORE_TABLE: ${self:custom.storeTable}
GITHUB_CLIENT_ID: ${self:custom.githubClientId.${self:custom.stage}}
GITHUB_CLIENT_SECRET: ${self:custom.githubClientSecret.${self:custom.stage}}
GITHUB_CLIENT_ID: '04fcf325dd26ca2a159f'
GITHUB_CLIENT_SECRET: ${env:GITHUB_CLIENT_SECRET}
GITHUB_APP_CLIENT_ID: 'Iv1.3392d0790b8f8334'
GITHUB_APP_CLIENT_SECRET: ${env:GITHUB_APP_CLIENT_SECRET}

functions:
expressRouter:
Expand All @@ -76,9 +70,15 @@ functions:
- http:
path: /static/setup-github-styles.css
method: get
- http:
path: /static/manage-styles.css
method: get
- http:
path: /analyze
method: post
- http:
path: /manage
method: get



Expand Down
28 changes: 28 additions & 0 deletions src/app/authentication/getRepositoryTokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const uuid = require('uuid')

const { getRepoToken, saveRepoToken } = require('../../models/storeUtils')

const getTokenForRepo = async repoFullName => {
const details = await getRepoToken(repoFullName)
let token = details.token
if (!token) {
token = uuid.v4()
await saveRepoToken(repoFullName)
}
return token
}


const getRepositoryTokens = async repositories => {
return repositories.map(async repoFullName => {
const repoToken = await getTokenForRepo(repoFullName)
return {
repoFullName,
repoToken,
}
})
}

module.exports = {
getRepositoryTokens,
}
13 changes: 0 additions & 13 deletions src/app/getBranchFileDetails.js

This file was deleted.

16 changes: 16 additions & 0 deletions src/app/getEnv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const getEnv = key => {
const value = process.env[key]
if (
!value ||
value.length === 0 ||
value == 'undefined' || // eslint-disable-line eqeqeq
value == 'null' // eslint-disable-line eqeqeq
) {
throw new Error(`Env var ${key} is missing`)
}
return value
}

module.exports = {
getEnv,
}
25 changes: 25 additions & 0 deletions src/app/github/app/getAppJWT.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const fs = require('fs')
const jwt = require('jsonwebtoken')

const PRIVATE_KEY_PATH = 'github-key.pem'
const TEN_MINUTES = 10 * 60
const GITHUB_APP_IDENTIFIER = 12145

const getAppJWT = () => {
const privateKey = fs.readFileSync(PRIVATE_KEY_PATH)
const nowSeconds = Date.now() / 1000
const issuedAt = nowSeconds
const expiration = nowSeconds + TEN_MINUTES
const token = jwt.encode(
{
iat: issuedAt,
exp: expiration,
iss: GITHUB_APP_IDENTIFIER,
},
privateKey,
'RS256',
)
return token
}

module.exports = getAppJWT
28 changes: 28 additions & 0 deletions src/app/github/getRepositoriesForUser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// const logger = require('../../logger')

const {
generateUserAccessTokenWithCode,
} = require('./user/generateUserAccessTokenWithCode')
const { Installations } = require('./user/Installations')

const getRepositoriesForUser = async code => {
const githubUserAccessToken = await generateUserAccessTokenWithCode(code)
const installationService = new Installations({ githubUserAccessToken })
const installations = await installationService.getInstallations()
const repositories = []
const repoFetchPromises = installations.map(installationId => {
return installationService
.getRepositoriesForInstallation(installationId)
.then(installationRepos => {
installationRepos.forEach(repo => {
repositories.push(repo)
})
})
})
await Promise.all(repoFetchPromises)
return repositories
}

module.exports = {
getRepositoriesForUser,
}
61 changes: 61 additions & 0 deletions src/app/github/user/Installations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const axios = require('axios')

const logger = require('../../../logger')

class Installations {
constructor({ githubUserAccessToken }) {
this.githubUserAccessToken = githubUserAccessToken
}

getInstallations() {
return axios({
method: 'GET',
url: `https://api.github.com/user/installations`,
responseType: 'json',
timeout: 3000,
headers: {
Accept: `application/vnd.github.machine-man-preview+json`,
Authorization: `token ${this.githubUserAccessToken}`,
},
})
.then(response => {
const installationIds = response.data.installations.map(
installation => {
return installation.id
},
)
return installationIds
})
.catch(error => {
logger.debug(error)
throw error
})
}

getRepositoriesForInstallation(installationId) {
return axios({
method: 'GET',
url: `https://api.github.com/user/installations/${installationId}/repositories`,
responseType: 'json',
timeout: 3000,
headers: {
Accept: `application/vnd.github.machine-man-preview+json`,
Authorization: `token ${this.githubUserAccessToken}`,
},
})
.then(response => {
const repoFullNames = response.data.repositories.map(repo => {
return repo.full_name
})
return repoFullNames
})
.catch(error => {
logger.debug(error)
throw error
})
}
}

module.exports = {
Installations,
}
41 changes: 41 additions & 0 deletions src/app/github/user/generateUserAccessTokenWithCode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const axios = require('axios')

const logger = require('../../../logger')
const { getEnv } = require('../../getEnv')

const generateUserAccessTokenWithCode = code => {
const clientId = getEnv('GITHUB_APP_CLIENT_ID')
const clientSecret = getEnv('GITHUB_APP_CLIENT_SECRET')

return axios({
method: 'POST',
url: 'https://github.com/login/oauth/access_token',
headers: {
Accept: 'application/json',
'Content-type': 'application/json',
},
data: {
code,
client_id: clientId,
client_secret: clientSecret,
},
timeout: 10000,
}).then(response => {
if (response.data.error) {
logger.debug(response.data)
throw new Error(response.data.error)
}

if (response.data.access_token) {
logger.debug(`Token: ${response.data.access_token}`)
return response.data.access_token
}

logger.debug(response)
throw new Error('Could not get token')
})
}

module.exports = {
generateUserAccessTokenWithCode,
}
89 changes: 89 additions & 0 deletions src/app/github/user/installations.mockdata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const getInstallationsResponse = {
total_count: 2,
installations: [
{
id: 1,
account: {
login: 'github',
id: 1,
url: 'https://api.github.com/orgs/github',
repos_url: 'https://api.github.com/orgs/github/repos',
events_url: 'https://api.github.com/orgs/github/events',
hooks_url: 'https://api.github.com/orgs/github/hooks',
issues_url: 'https://api.github.com/orgs/github/issues',
members_url:
'https://api.github.com/orgs/github/members{/member}',
public_members_url:
'https://api.github.com/orgs/github/public_members{/member}',
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
description: 'A great organization',
},
access_tokens_url:
'https://api.github.com/installations/1/access_tokens',
repositories_url:
'https://api.github.com/installation/repositories',
html_url:
'https://github.com/organizations/github/settings/installations/1',
app_id: 1,
target_id: 1,
target_type: 'Organization',
permissions: {
metadata: 'read',
contents: 'read',
issues: 'write',
single_file: 'write',
},
events: ['push', 'pull_request'],
single_file_name: 'config.yml',
},
{
id: 3,
account: {
login: 'octocat',
id: 2,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/octocat',
html_url: 'https://github.com/octocat',
followers_url: 'https://api.github.com/users/octocat/followers',
following_url:
'https://api.github.com/users/octocat/following{/other_user}',
gists_url:
'https://api.github.com/users/octocat/gists{/gist_id}',
starred_url:
'https://api.github.com/users/octocat/starred{/owner}{/repo}',
subscriptions_url:
'https://api.github.com/users/octocat/subscriptions',
organizations_url: 'https://api.github.com/users/octocat/orgs',
repos_url: 'https://api.github.com/users/octocat/repos',
events_url:
'https://api.github.com/users/octocat/events{/privacy}',
received_events_url:
'https://api.github.com/users/octocat/received_events',
type: 'User',
site_admin: false,
},
access_tokens_url:
'https://api.github.com/installations/1/access_tokens',
repositories_url:
'https://api.github.com/installation/repositories',
html_url:
'https://github.com/organizations/github/settings/installations/1',
app_id: 1,
target_id: 1,
target_type: 'Organization',
permissions: {
metadata: 'read',
contents: 'read',
issues: 'write',
single_file: 'write',
},
events: ['push', 'pull_request'],
single_file_name: 'config.yml',
},
],
}

module.exports = {
getInstallationsResponse,
}
Loading