Skip to content

Commit

Permalink
feat: add proxy option
Browse files Browse the repository at this point in the history
  • Loading branch information
pvdlg authored and gr2m committed Jul 6, 2018
1 parent 7943600 commit 5714da6
Show file tree
Hide file tree
Showing 19 changed files with 320 additions and 81 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Follow the [Creating a personal access token for the command line](https://help.
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| `githubUrl` | The GitHub Enterprise endpoint. | `GH_URL` or `GITHUB_URL` environment variable. |
| `githubApiPathPrefix` | The GitHub Enterprise API prefix. | `GH_PREFIX` or `GITHUB_PREFIX` environment variable. |
| `proxy` | The proxy to use to access the GitHub API. See [proxy](#proxy). | `HTTP_PROXY` environment variable. |
| `assets` | An array of files to upload to the release. See [assets](#assets). | - |
| `successComment` | The comment added to each issue and pull request resolved by the release. See [successComment](#successcomment). | `:tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitHub release](<github_release_url>)` |
| `failComment` | The content of the issue created when a release fails. See [failComment](#failcomment). | Friendly message with links to **semantic-release** documentation and support, with the list of errors that caused the release to fail. |
Expand All @@ -59,6 +60,24 @@ Follow the [Creating a personal access token for the command line](https://help.

**Note**: If you use a [shareable configuration](https:/semantic-release/semantic-release/blob/caribou/docs/usage/shareable-configurations.md#shareable-configurations) that defines one of these options you can set it to `false` in your [**semantic-release** configuration](https:/semantic-release/semantic-release/blob/caribou/docs/usage/configuration.md#configuration) in order to use the default value.

#### proxy

Can be a the proxy URL or and `Object` with the following properties:

| Property | Description | Default |
|---------------|----------------------------------------------------------------|--------------------------------------|
| `host` | **Required.** Proxy host to connect to. | - |
| `port` | **Required.** Proxy port to connect to. | File name extracted from the `path`. |
| `secureProxy` | If `true`, then use TLS to connect to the proxy. | `false` |
| `headers` | Additional HTTP headers to be sent on the HTTP CONNECT method. | - |

See [node-https-proxy-agent](https:/TooTallNate/node-https-proxy-agent#new-httpsproxyagentobject-options) and [node-http-proxy-agent](https:/TooTallNate/node-http-proxy-agent) for additional details.

##### proxy examples

`'http://168.63.76.32:3128'`: use the proxy running on host `168.63.76.32` and port `3128` for each GitHub API request.
`{host: '168.63.76.32', port: 3128, headers: {Foo: 'bar'}}`: use the proxy running on host `168.63.76.32` and port `3128` for each GitHub API request, setting the `Foo` header value to `bar`.

#### assets

Can be a [glob](https:/isaacs/node-glob#glob-primer) or and `Array` of
Expand Down
8 changes: 8 additions & 0 deletions lib/definitions/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ Your configuration for the \`assignees\` option is \`${stringify(assignees)}\`.`
details: `The **semantic-release** \`repositoryUrl\` option must a valid GitHub URL with the format \`<GitHub_or_GHE_URL>/<owner>/<repo>.git\`.
By default the \`repositoryUrl\` option is retrieved from the \`repository\` property of your \`package.json\` or the [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) of the repository cloned by your CI environment.`,
}),
EINVALIDPROXY: ({proxy}) => ({
message: 'Invalid `proxy` option.',
details: `The [proxy option](${linkify(
'README.md#proxy'
)}) option must be a \`String\` or an \`Objects\` with a \`host\` and a \`port\` property.
Your configuration for the \`proxy\` option is \`${stringify(proxy)}\`.`,
}),
EMISSINGREPO: ({owner, repo}) => ({
message: `The repository ${owner}/${repo} doesn't exist.`,
Expand Down
4 changes: 2 additions & 2 deletions lib/fail.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ const findSRIssues = require('./find-sr-issues');
const getFailComment = require('./get-fail-comment');

module.exports = async (pluginConfig, {options: {branch, repositoryUrl}, errors, logger}) => {
const {githubToken, githubUrl, githubApiPathPrefix, failComment, failTitle, labels, assignees} = resolveConfig(
const {githubToken, githubUrl, githubApiPathPrefix, proxy, failComment, failTitle, labels, assignees} = resolveConfig(
pluginConfig
);
const {name: repo, owner} = parseGithubUrl(repositoryUrl);
const github = getClient({githubToken, githubUrl, githubApiPathPrefix});
const github = getClient({githubToken, githubUrl, githubApiPathPrefix, proxy});
const body = failComment ? template(failComment)({branch, errors}) : getFailComment(branch, errors);
const [srIssue] = await findSRIssues(github, failTitle, owner, repo);

Expand Down
13 changes: 12 additions & 1 deletion lib/get-client.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
const url = require('url');
const {memoize} = require('lodash');
const Octokit = require('@octokit/rest');
const pRetry = require('p-retry');
const Bottleneck = require('bottleneck');
const urljoin = require('url-join');
const HttpProxyAgent = require('http-proxy-agent');
const HttpsProxyAgent = require('https-proxy-agent');

/**
* Default exponential backoff configuration for retries.
Expand Down Expand Up @@ -97,12 +100,20 @@ module.exports = ({
githubToken,
githubUrl,
githubApiPathPrefix,
proxy,
retry = DEFAULT_RETRY,
limit = RATE_LIMITS,
globalLimit = GLOBAL_RATE_LIMIT,
}) => {
const baseUrl = githubUrl && urljoin(githubUrl, githubApiPathPrefix);
const github = new Octokit({baseUrl});
const github = new Octokit({
baseUrl,
agent: proxy
? baseUrl && url.parse(baseUrl).protocol.replace(':', '') === 'http'
? new HttpProxyAgent(proxy)
: new HttpsProxyAgent(proxy)
: undefined,
});
github.authenticate({type: 'token', token: githubToken});
return new Proxy(github, handler(retry, limit, new Bottleneck({minTime: globalLimit})));
};
4 changes: 2 additions & 2 deletions lib/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ const resolveConfig = require('./resolve-config');
const getClient = require('./get-client');

module.exports = async (pluginConfig, {options: {branch, repositoryUrl}, nextRelease: {gitTag, notes}, logger}) => {
const {githubToken, githubUrl, githubApiPathPrefix, assets} = resolveConfig(pluginConfig);
const {githubToken, githubUrl, githubApiPathPrefix, proxy, assets} = resolveConfig(pluginConfig);
const {name: repo, owner} = parseGithubUrl(repositoryUrl);
const github = getClient({githubToken, githubUrl, githubApiPathPrefix});
const github = getClient({githubToken, githubUrl, githubApiPathPrefix, proxy});
const release = {owner, repo, tag_name: gitTag, name: gitTag, target_commitish: branch, body: notes}; // eslint-disable-line camelcase

debug('release owner: %o', owner);
Expand Down
2 changes: 2 additions & 0 deletions lib/resolve-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const {isUndefined, castArray} = require('lodash');
module.exports = ({
githubUrl,
githubApiPathPrefix,
proxy,
assets,
successComment,
failTitle,
Expand All @@ -13,6 +14,7 @@ module.exports = ({
githubToken: process.env.GH_TOKEN || process.env.GITHUB_TOKEN,
githubUrl: githubUrl || process.env.GH_URL || process.env.GITHUB_URL,
githubApiPathPrefix: githubApiPathPrefix || process.env.GH_PREFIX || process.env.GITHUB_PREFIX || '',
proxy: proxy || process.env.HTTP_PROXY,
assets: assets ? castArray(assets) : assets,
successComment,
failTitle: isUndefined(failTitle) || failTitle === false ? 'The automated release is failing 🚨' : failTitle,
Expand Down
4 changes: 2 additions & 2 deletions lib/success.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ module.exports = async (
pluginConfig,
{options: {branch, repositoryUrl}, lastRelease, commits, nextRelease, releases, logger}
) => {
const {githubToken, githubUrl, githubApiPathPrefix, successComment, failTitle} = resolveConfig(pluginConfig);
const {githubToken, githubUrl, githubApiPathPrefix, proxy, successComment, failTitle} = resolveConfig(pluginConfig);
const {name: repo, owner} = parseGithubUrl(repositoryUrl);
const github = getClient({githubToken, githubUrl, githubApiPathPrefix});
const github = getClient({githubToken, githubUrl, githubApiPathPrefix, proxy});
const releaseInfos = releases.filter(release => Boolean(release.name));
const shas = commits.map(commit => commit.hash);
const treeShas = commits.map(commit => commit.tree.long);
Expand Down
12 changes: 7 additions & 5 deletions lib/verify.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const {isString, isPlainObject, isUndefined, isArray} = require('lodash');
const {isString, isPlainObject, isUndefined, isArray, isNumber} = require('lodash');
const parseGithubUrl = require('parse-github-url');
const urlJoin = require('url-join');
const AggregateError = require('aggregate-error');
Expand All @@ -11,6 +11,8 @@ const isStringOrStringArray = value => isNonEmptyString(value) || (isArray(value
const isArrayOf = validator => array => isArray(array) && array.every(value => validator(value));

const VALIDATORS = {
proxy: proxy =>
isNonEmptyString(proxy) || (isPlainObject(proxy) && isNonEmptyString(proxy.host) && isNumber(proxy.port)),
assets: isArrayOf(
asset => isStringOrStringArray(asset) || (isPlainObject(asset) && isStringOrStringArray(asset.path))
),
Expand All @@ -22,9 +24,9 @@ const VALIDATORS = {
};

module.exports = async (pluginConfig, {options: {repositoryUrl}, logger}) => {
const {githubToken, githubUrl, githubApiPathPrefix, ...options} = resolveConfig(pluginConfig);
const {githubToken, githubUrl, githubApiPathPrefix, proxy, ...options} = resolveConfig(pluginConfig);

const errors = Object.entries(options).reduce(
const errors = Object.entries({...options, proxy}).reduce(
(errors, [option, value]) =>
!isUndefined(value) && value !== false && !VALIDATORS[option](value)
? [...errors, getError(`EINVALID${option.toUpperCase()}`, {[option]: value})]
Expand All @@ -41,8 +43,8 @@ module.exports = async (pluginConfig, {options: {repositoryUrl}, logger}) => {
const {name: repo, owner} = parseGithubUrl(repositoryUrl);
if (!owner || !repo) {
errors.push(getError('EINVALIDGITHUBURL'));
} else if (githubToken) {
const github = getClient({githubToken, githubUrl, githubApiPathPrefix});
} else if (githubToken && !errors.find(({code}) => code === 'EINVALIDPROXY')) {
const github = getClient({githubToken, githubUrl, githubApiPathPrefix, proxy});

try {
const {
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"debug": "^3.1.0",
"fs-extra": "^6.0.0",
"globby": "^8.0.0",
"http-proxy-agent": "^2.1.0",
"https-proxy-agent": "^2.2.1",
"issue-parser": "^2.0.0",
"lodash": "^4.17.4",
"mime": "^2.0.3",
Expand All @@ -39,8 +41,10 @@
"cz-conventional-changelog": "^2.0.0",
"nock": "^9.1.0",
"nyc": "^12.0.1",
"proxy": "^0.2.4",
"proxyquire": "^2.0.0",
"semantic-release": "^15.0.0",
"server-destroy": "^1.0.1",
"sinon": "^6.0.0",
"tempy": "^0.2.1",
"xo": "^0.21.0"
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
15 changes: 15 additions & 0 deletions test/fixtures/ssl/ssl-cert-snakeoil.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQCzURxIqzer0ACAbX/lHdsn4Gd9PLKrf7EeDYfIdV0HZKPD8WDr
bBx2/fBu0OW2sjnzv/SVZbJ0DAuPE/p0+eT0qb2qC10iz9iTD7ribd7gxhirVb8y
b3fBjXsxc8V8p4Ny1LcvNSqCjwUbJqdRogfoJeTiqPM58z5sNzuv5iq7iwIDAQAB
AoGAPMQy4olrP0UotlzlJ36bowLP70ffgHCwU+/f4NWs5fF78c3du0oSx1w820Dd
Z7E0JF8bgnlJJTxjumPZz0RUCugrEHBKJmzEz3cxF5E3+7NvteZcjKn9D67RrM5x
1/uSZ9cqKE9cYvY4fSuHx18diyZ4axR/wB1Pea2utjjDM+ECQQDb9ZbmmaWMiRpQ
5Up+loxP7BZNPsEVsm+DVJmEFbaFgGfncWBqSIqnPNjMwTwj0OigTwCAEGPkfRVW
T0pbYWCxAkEA0LK7SCTwzyDmhASUalk0x+3uCAA6ryFdwJf/wd8TRAvVOmkTEldX
uJ7ldLvfrONYO3v56uKTU/SoNdZYzKtO+wJAX2KM4ctXYy5BXztPpr2acz4qHa1N
Bh+vBAC34fOYhyQ76r3b1btHhWZ5jbFuZwm9F2erC94Ps5IaoqcX07DSwQJAPKGw
h2U0EPkd/3zVIZCJJQya+vgWFIs9EZcXVtvYXQyTBkVApTN66MhBIYjzkub5205J
bVQmOV37AKklY1DhwQJAA1wos0cYxro02edzatxd0DIR2r4qqOqLkw6BhYHhq6HJ
ZvIcQkHqdSXzdETFc01I1znDGGIrJHcnvKWgBPoEUg==
-----END RSA PRIVATE KEY-----
12 changes: 12 additions & 0 deletions test/fixtures/ssl/ssl-cert-snakeoil.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIB1TCCAT4CCQDV5mPlzm9+izANBgkqhkiG9w0BAQUFADAvMS0wKwYDVQQDEyQ3
NTI3YmQ3Ny1hYjNlLTQ3NGItYWNlNy1lZWQ2MDUzOTMxZTcwHhcNMTUwNzA2MjI0
NTA3WhcNMjUwNzAzMjI0NTA3WjAvMS0wKwYDVQQDEyQ3NTI3YmQ3Ny1hYjNlLTQ3
NGItYWNlNy1lZWQ2MDUzOTMxZTcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB
ALNRHEirN6vQAIBtf+Ud2yfgZ308sqt/sR4Nh8h1XQdko8PxYOtsHHb98G7Q5bay
OfO/9JVlsnQMC48T+nT55PSpvaoLXSLP2JMPuuJt3uDGGKtVvzJvd8GNezFzxXyn
g3LUty81KoKPBRsmp1GiB+gl5OKo8znzPmw3O6/mKruLAgMBAAEwDQYJKoZIhvcN
AQEFBQADgYEACzoHUF8UV2Z6541Q2wKEA0UFUzmUjf/E1XwBO+1P15ZZ64uw34B4
1RwMPtAo9RY/PmICTWtNxWGxkzwb2JtDWtnxVER/lF8k2XcXPE76fxTHJF/BKk9J
QU8OTD1dd9gHCBviQB9TqntRZ5X7axjtuWjb2umY+owBYzAHZkp1HKI=
-----END CERTIFICATE-----
83 changes: 82 additions & 1 deletion test/get-client.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,90 @@
import path from 'path';
import http from 'http';
import https from 'https';
import {promisify} from 'util';
import {readFile} from 'fs-extra';
import test from 'ava';
import {isFunction, isPlainObject, inRange} from 'lodash';
import {stub} from 'sinon';
import {stub, spy} from 'sinon';
import proxyquire from 'proxyquire';
import Proxy from 'proxy';
import serverDestroy from 'server-destroy';
import getClient from '../lib/get-client';

test.serial('Use a http proxy', async t => {
const server = http.createServer();
await promisify(server.listen).bind(server)();
const serverPort = server.address().port;
serverDestroy(server);
const proxy = new Proxy();
await promisify(proxy.listen).bind(proxy)();
const proxyPort = proxy.address().port;
serverDestroy(proxy);

const proxyHandler = spy();
const serverHandler = spy((req, res) => {
res.end();
});
proxy.on('request', proxyHandler);
server.on('request', serverHandler);

const github = getClient({
githubToken: 'github_token',
githubUrl: `http://localhost:${serverPort}`,
githubApiPathPrefix: '',
proxy: `http://localhost:${proxyPort}`,
retry: {retries: 1, factor: 1, minTimeout: 1, maxTimeout: 1},
});

await github.repos.get({repo: 'repo', owner: 'owner'});

t.is(proxyHandler.args[0][0].headers.accept, 'application/vnd.github.drax-preview+json');
t.is(serverHandler.args[0][0].headers.accept, 'application/vnd.github.drax-preview+json');
t.regex(serverHandler.args[0][0].headers.via, /proxy/);
t.truthy(serverHandler.args[0][0].headers['x-forwarded-for'], /proxy/);

await promisify(proxy.destroy).bind(proxy)();
await promisify(server.destroy).bind(server)();
});

test.serial('Use a https proxy', async t => {
const server = https.createServer({
key: await readFile(path.join(__dirname, '/fixtures/ssl/ssl-cert-snakeoil.key')),
cert: await readFile(path.join(__dirname, '/fixtures/ssl/ssl-cert-snakeoil.pem')),
});
await promisify(server.listen).bind(server)();
const serverPort = server.address().port;
serverDestroy(server);
const proxy = new Proxy();
await promisify(proxy.listen).bind(proxy)();
const proxyPort = proxy.address().port;
serverDestroy(proxy);

const proxyHandler = spy();
const serverHandler = spy((req, res) => {
res.end();
});
proxy.on('connect', proxyHandler);
server.on('request', serverHandler);

const github = getClient({
githubToken: 'github_token',
githubUrl: `https://localhost:${serverPort}`,
githubApiPathPrefix: '',
proxy: {host: 'localhost', port: proxyPort, rejectUnauthorized: false, headers: {foo: 'bar'}},
retry: {retries: 1, factor: 1, minTimeout: 1, maxTimeout: 1},
});

await github.repos.get({repo: 'repo', owner: 'owner'});

t.is(proxyHandler.args[0][0].url, `localhost:${serverPort}`);
t.is(proxyHandler.args[0][0].headers.foo, 'bar');
t.is(serverHandler.args[0][0].headers.accept, 'application/vnd.github.drax-preview+json');

await promisify(proxy.destroy).bind(proxy)();
await promisify(server.destroy).bind(server)();
});

test('Wrap Octokit in a proxy', t => {
const github = getClient({githubToken: 'github_token'});

Expand Down
Loading

0 comments on commit 5714da6

Please sign in to comment.