octokit client should follow proxy settings (#314)

This commit is contained in:
eric sciple
2020-01-18 14:28:37 -05:00
committed by GitHub
parent e69833ed16
commit ab5bd9d696
18 changed files with 473 additions and 68 deletions

View File

@@ -4,7 +4,7 @@
## Usage
Returns an Octokit client. See https://octokit.github.io/rest.js for the API.
Returns an authenticated Octokit client that follows the machine [proxy settings](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-self-hosted-runners#using-a-proxy-server-with-self-hosted-runners). See https://octokit.github.io/rest.js for the API.
```js
const github = require('@actions/github');
@@ -34,7 +34,7 @@ async function run() {
run();
```
You can pass client options (except `auth`, which is handled by the token argument), as specified by [Octokit](https://octokit.github.io/rest.js/), as a second argument to the `GitHub` constructor.
You can pass client options, as specified by [Octokit](https://octokit.github.io/rest.js/), as a second argument to the `GitHub` constructor.
You can also make GraphQL requests. See https://github.com/octokit/graphql.js for the API.

View File

@@ -0,0 +1,173 @@
import * as http from 'http'
import proxy from 'proxy'
import {GitHub} from '../src/github'
describe('@actions/github', () => {
const proxyUrl = 'http://127.0.0.1:8080'
const originalProxyUrl = process.env['https_proxy']
let proxyConnects: string[]
let proxyServer: http.Server
let first = true
beforeAll(async () => {
// Start proxy server
proxyServer = proxy() as http.Server
await new Promise(resolve => {
const port = Number(proxyUrl.split(':')[2])
proxyServer.listen(port, () => resolve())
})
proxyServer.on('connect', req => {
proxyConnects.push(req.url)
})
})
beforeEach(() => {
delete process.env['https_proxy']
proxyConnects = []
})
afterAll(async () => {
// Stop proxy server
await new Promise(resolve => {
proxyServer.once('close', () => resolve())
proxyServer.close()
})
if (originalProxyUrl) {
process.env['https_proxy'] = originalProxyUrl
}
})
it('basic REST client', async () => {
const token = getToken()
if (!token) {
return
}
const octokit = new GitHub(token)
const branch = await octokit.repos.getBranch({
owner: 'actions',
repo: 'toolkit',
branch: 'master'
})
expect(branch.data.name).toBe('master')
expect(proxyConnects).toHaveLength(0)
})
it('basic REST client with custom auth', async () => {
const token = getToken()
if (!token) {
return
}
// Valid token
let octokit = new GitHub({auth: `token ${token}`})
const branch = await octokit.repos.getBranch({
owner: 'actions',
repo: 'toolkit',
branch: 'master'
})
expect(branch.data.name).toBe('master')
expect(proxyConnects).toHaveLength(0)
// Invalid token
octokit = new GitHub({auth: `token asdf`})
let failed = false
try {
await octokit.repos.getBranch({
owner: 'actions',
repo: 'toolkit',
branch: 'master'
})
} catch (err) {
failed = true
}
expect(failed).toBeTruthy()
})
it('basic REST client with proxy', async () => {
const token = getToken()
if (!token) {
return
}
process.env['https_proxy'] = proxyUrl
const octokit = new GitHub(token)
const branch = await octokit.repos.getBranch({
owner: 'actions',
repo: 'toolkit',
branch: 'master'
})
expect(branch.data.name).toBe('master')
expect(proxyConnects).toEqual(['api.github.com:443'])
})
it('basic GraphQL client', async () => {
const token = getToken()
if (!token) {
return
}
const octokit = new GitHub(token)
const repository = await octokit.graphql(
'{repository(owner:"actions", name:"toolkit"){name}}'
)
expect(repository).toEqual({repository: {name: 'toolkit'}})
expect(proxyConnects).toHaveLength(0)
})
it('basic GraphQL client with custom auth', async () => {
const token = getToken()
if (!token) {
return
}
// Valid token
let octokit = new GitHub(token)
const repository = await octokit.graphql(
'{repository(owner:"actions", name:"toolkit"){name}}'
)
expect(repository).toEqual({repository: {name: 'toolkit'}})
expect(proxyConnects).toHaveLength(0)
// Invalid token
octokit = new GitHub({auth: `token asdf`})
let failed = false
try {
await octokit.graphql(
'{repository(owner:"actions", name:"toolkit"){name}}'
)
} catch (err) {
failed = true
}
expect(failed).toBeTruthy()
})
it('basic GraphQL client with proxy', async () => {
const token = getToken()
if (!token) {
return
}
process.env['https_proxy'] = proxyUrl
const octokit = new GitHub(token)
const repository = await octokit.graphql(
'{repository(owner:"actions", name:"toolkit"){name}}'
)
expect(repository).toEqual({repository: {name: 'toolkit'}})
expect(proxyConnects).toEqual(['api.github.com:443'])
})
function getToken(): string {
const token = process.env['GITHUB_TOKEN'] || ''
if (!token && first) {
/* eslint-disable-next-line no-console */
console.warn(
'Skipping GitHub tests. Set $GITHUB_TOKEN to run REST client and GraphQL client tests'
)
first = false
}
return token
}
})

5
packages/github/__tests__/proxy.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module 'proxy' {
import * as http from 'http'
function internal(): http.Server
export = internal
}

View File

@@ -1,9 +1,17 @@
{
"name": "@actions/github",
"version": "2.0.1",
"version": "2.0.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@actions/http-client": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.2.tgz",
"integrity": "sha512-ngdGx7aXM7i9BFT+7e3RWWAEt3bX4tKrdI5w5hf0wYpHz66u5Nw6AFSFXG5wzQyUQbkgeNRnJZyK2zciGqXgrQ==",
"requires": {
"tunnel": "0.0.6"
}
},
"@babel/code-frame": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
@@ -686,6 +694,67 @@
"normalize-path": "^2.1.1"
}
},
"args": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/args/-/args-3.0.2.tgz",
"integrity": "sha1-hQu46IHzE5IDpeTLF2QxCStWLC0=",
"dev": true,
"requires": {
"camelcase": "4.1.0",
"chalk": "1.1.3",
"minimist": "1.2.0",
"pkginfo": "0.4.0",
"string-similarity": "1.1.0"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
},
"ansi-styles": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
"dev": true
},
"camelcase": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
"dev": true
},
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
"ansi-styles": "^2.2.1",
"escape-string-regexp": "^1.0.2",
"has-ansi": "^2.0.0",
"strip-ansi": "^3.0.0",
"supports-color": "^2.0.0"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"requires": {
"ansi-regex": "^2.0.0"
}
},
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
"dev": true
}
}
},
"arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -886,6 +955,12 @@
}
}
},
"basic-auth-parser": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/basic-auth-parser/-/basic-auth-parser-0.0.2.tgz",
"integrity": "sha1-zp5xp38jwSee7NJlmypGJEwVbkE=",
"dev": true
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@@ -2401,6 +2476,23 @@
"function-bind": "^1.1.1"
}
},
"has-ansi": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
"integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
"dev": true,
"requires": {
"ansi-regex": "^2.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
}
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -4046,6 +4138,12 @@
"find-up": "^3.0.0"
}
},
"pkginfo": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.0.tgz",
"integrity": "sha1-NJ27f/04CB/K3AhT32h/DHdEzWU=",
"dev": true
},
"pn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz",
@@ -4092,6 +4190,34 @@
"sisteransi": "^1.0.0"
}
},
"proxy": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/proxy/-/proxy-1.0.1.tgz",
"integrity": "sha512-mM9Hl6Mbw2Iiw4WLzjtPObtxX3xdsv0Fr07Kqm+GXg0eVObKBD7mc+TMQwkv2zztk5EtyLdv0+eFNXhBfPiU8A==",
"dev": true,
"requires": {
"args": "3.0.2",
"basic-auth-parser": "0.0.2",
"debug": "^4.1.1"
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}
}
},
"psl": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz",
@@ -4728,6 +4854,15 @@
}
}
},
"string-similarity": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-1.1.0.tgz",
"integrity": "sha1-PGZJiFikZex8QMfYFzm72ZWQSRQ=",
"dev": true,
"requires": {
"lodash": "^4.13.1"
}
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
@@ -4896,6 +5031,11 @@
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
"dev": true
},
"tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@actions/github",
"version": "2.0.1",
"version": "2.0.2",
"description": "Actions github lib",
"keywords": [
"github",
@@ -37,10 +37,12 @@
"url": "https://github.com/actions/toolkit/issues"
},
"dependencies": {
"@actions/http-client": "^1.0.2",
"@octokit/graphql": "^4.3.1",
"@octokit/rest": "^16.15.0"
},
"devDependencies": {
"jest": "^24.7.1"
"jest": "^24.7.1",
"proxy": "^1.0.1"
}
}

View File

@@ -4,10 +4,15 @@ import {graphql} from '@octokit/graphql'
// we need this type to set up a property on the GitHub object
// that has token authorization
// (it is not exported from octokit by default)
import {graphql as GraphQL} from '@octokit/graphql/dist-types/types'
import {
graphql as GraphQL,
RequestParameters as GraphQLRequestParameters
} from '@octokit/graphql/dist-types/types'
import Octokit from '@octokit/rest'
import * as Context from './context'
import * as http from 'http'
import {HttpClient} from '@actions/http-client'
// We need this in order to extend Octokit
Octokit.prototype = new Octokit()
@@ -17,11 +22,113 @@ export const context = new Context.Context()
export class GitHub extends Octokit {
graphql: GraphQL
constructor(token: string, opts: Omit<Octokit.Options, 'auth'> = {}) {
super({...opts, auth: `token ${token}`})
/* eslint-disable no-dupe-class-members */
// Disable no-dupe-class-members due to false positive for method overload
// https://github.com/typescript-eslint/typescript-eslint/issues/291
this.graphql = graphql.defaults({
headers: {authorization: `token ${token}`}
})
/**
* Sets up the REST client and GraphQL client with auth and proxy support.
* The parameter `token` or `opts.auth` must be supplied. The GraphQL client
* authorization is not setup when `opts.auth` is a function or object.
*
* @param token Auth token
* @param opts Octokit options
*/
constructor(token: string, opts?: Omit<Octokit.Options, 'auth'>)
constructor(opts: Octokit.Options)
constructor(token: string | Octokit.Options, opts?: Octokit.Options) {
super(GitHub.getOctokitOptions(GitHub.disambiguate(token, opts)))
this.graphql = GitHub.getGraphQL(GitHub.disambiguate(token, opts))
}
/**
* Disambiguates the constructor overload parameters
*/
private static disambiguate(
token: string | Octokit.Options,
opts?: Octokit.Options
): [string, Octokit.Options] {
return [
typeof token === 'string' ? token : '',
typeof token === 'object' ? token : opts || {}
]
}
private static getOctokitOptions(
args: [string, Octokit.Options]
): Octokit.Options {
const token = args[0]
const options = {...args[1]} // Shallow clone - don't mutate the object provided by the caller
// Auth
const auth = GitHub.getAuthString(token, options)
if (auth) {
options.auth = auth
}
// Proxy
const agent = GitHub.getProxyAgent(options)
if (agent) {
// Shallow clone - don't mutate the object provided by the caller
options.request = options.request ? {...options.request} : {}
// Set the agent
options.request.agent = agent
}
return options
}
private static getGraphQL(args: [string, Octokit.Options]): GraphQL {
const defaults: GraphQLRequestParameters = {}
const token = args[0]
const options = args[1]
// Authorization
const auth = this.getAuthString(token, options)
if (auth) {
defaults.headers = {
authorization: auth
}
}
// Proxy
const agent = GitHub.getProxyAgent(options)
if (agent) {
defaults.request = {agent}
}
return graphql.defaults(defaults)
}
private static getAuthString(
token: string,
options: Octokit.Options
): string | undefined {
// Validate args
if (!token && !options.auth) {
throw new Error('Parameter token or opts.auth is required')
} else if (token && options.auth) {
throw new Error(
'Parameters token and opts.auth may not both be specified'
)
}
return typeof options.auth === 'string' ? options.auth : `token ${token}`
}
private static getProxyAgent(
options: Octokit.Options
): http.Agent | undefined {
if (!options.request?.agent) {
const proxyUrl = process.env['https_proxy'] || process.env['HTTPS_PROXY']
if (proxyUrl) {
const httpClient = new HttpClient()
return httpClient.getAgent('https://api.github.com')
}
}
return undefined
}
}

View File

@@ -2,7 +2,6 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"esModuleInterop": true,
"outDir": "./lib",
"rootDir": "./src"
},