Skip to content

Commit 15def77

Browse files
authored
fix: add undici ProxyAgent support for GitHub Enterprise Server behind proxies (#1104)
1 parent e86c7cc commit 15def77

File tree

6 files changed

+211
-14
lines changed

6 files changed

+211
-14
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,15 @@ Can be `false`, a proxy URL or an `Object` with the following properties:
116116
| `secureProxy` | If `true`, then use TLS to connect to the proxy. | `false` |
117117
| `headers` | Additional HTTP headers to be sent on the HTTP CONNECT method. | - |
118118

119-
See [node-https-proxy-agent](https://github.com/TooTallNate/node-https-proxy-agent#new-httpsproxyagentobject-options) and [node-http-proxy-agent](https://github.com/TooTallNate/node-http-proxy-agent) for additional details.
119+
This plugin uses [undici's ProxyAgent](https://github.com/nodejs/undici#proxyagent) for modern proxy support, with backwards compatibility maintained through [node-https-proxy-agent](https://github.com/TooTallNate/node-https-proxy-agent#new-httpsproxyagentobject-options) and [node-http-proxy-agent](https://github.com/TooTallNate/node-http-proxy-agent). This ensures proxy functionality works with GitHub Enterprise Server environments behind corporate proxies.
120120

121121
##### proxy examples
122122

123123
`'http://168.63.76.32:3128'`: use the proxy running on host `168.63.76.32` and port `3128` for each GitHub API request.
124124
`{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`.
125125

126+
**Note**: This plugin now uses undici's ProxyAgent internally for enhanced proxy support, particularly beneficial for GitHub Enterprise Server environments behind corporate proxies. All existing proxy configurations remain fully compatible.
127+
126128
#### assets
127129

128130
Can be a [glob](https://github.com/isaacs/node-glob#glob-primer) or and `Array` of

lib/octokit.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { throttling } from "@octokit/plugin-throttling";
1414
import urljoin from "url-join";
1515
import { HttpProxyAgent } from "http-proxy-agent";
1616
import { HttpsProxyAgent } from "https-proxy-agent";
17+
import { ProxyAgent, fetch as undiciFetch } from "undici";
1718

1819
import { RETRY_CONF } from "./definitions/retry.js";
1920
import { THROTTLE_CONF } from "./definitions/throttle.js";
@@ -50,7 +51,7 @@ export const SemanticReleaseOctokit = Octokit.plugin(
5051

5152
/**
5253
* @param {{githubToken: string, proxy: any} | {githubUrl: string, githubApiPathPrefix: string, githubApiUrl: string,githubToken: string, proxy: any}} options
53-
* @returns {{ auth: string, baseUrl?: string, request: { agent?: any } }}
54+
* @returns {{ auth: string, baseUrl?: string, request: { agent?: any, fetch?: any } }}
5455
*/
5556
export function toOctokitOptions(options) {
5657
const baseUrl =
@@ -69,11 +70,17 @@ export function toOctokitOptions(options) {
6970
: new HttpsProxyAgent(options.proxy, options.proxy)
7071
: undefined;
7172

73+
const fetchWithDispatcher = (url, opts) =>
74+
undiciFetch(url, {
75+
...opts,
76+
dispatcher: options.proxy
77+
? new ProxyAgent({ uri: options.proxy })
78+
: undefined,
79+
});
80+
7281
return {
7382
...(baseUrl ? { baseUrl } : {}),
7483
auth: options.githubToken,
75-
request: {
76-
agent,
77-
},
84+
request: { agent, fetch: fetchWithDispatcher },
7885
};
7986
}

package-lock.json

Lines changed: 17 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"mime": "^4.0.0",
3838
"p-filter": "^4.0.0",
3939
"tinyglobby": "^0.2.14",
40+
"undici": "^7.0.0",
4041
"url-join": "^5.0.0"
4142
},
4243
"devDependencies": {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { createServer } from "node:http";
2+
import { createServer as createHttpsServer } from "node:https";
3+
import { readFileSync } from "node:fs";
4+
import { join } from "node:path";
5+
6+
import test from "ava";
7+
import { SemanticReleaseOctokit, toOctokitOptions } from "../lib/octokit.js";
8+
9+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0;
10+
11+
test("Octokit proxy setup creates proper fetch function", async (t) => {
12+
// This test verifies that the proxy setup creates the expected function structure
13+
// without actually testing network connectivity which can be flaky in CI environments
14+
15+
const options = toOctokitOptions({
16+
githubToken: "test_token",
17+
githubApiUrl: "https://api.github.com",
18+
proxy: "http://proxy.example.com:8080",
19+
});
20+
21+
const octokit = new SemanticReleaseOctokit(options);
22+
23+
// Verify that the options are set up correctly
24+
t.true(typeof options.request.fetch === "function");
25+
t.is(options.auth, "test_token");
26+
t.is(options.baseUrl, "https://api.github.com");
27+
28+
// Verify that both agent (for backwards compatibility) and fetch are present
29+
t.truthy(options.request.agent);
30+
t.truthy(options.request.fetch);
31+
32+
// Verify that the fetch function has the correct signature
33+
t.is(options.request.fetch.length, 2);
34+
});
35+
36+
test("Octokit works without proxy using custom fetch", async (t) => {
37+
let requestReceived = false;
38+
39+
// Create a mock GitHub API server
40+
const mockApiServer = createServer((req, res) => {
41+
requestReceived = true;
42+
res.writeHead(200, { "Content-Type": "application/json" });
43+
res.end(
44+
JSON.stringify({
45+
id: 1,
46+
tag_name: "v1.0.0",
47+
name: "Test Release",
48+
body: "Test release body",
49+
}),
50+
);
51+
});
52+
53+
await new Promise((resolve) => {
54+
mockApiServer.listen(0, "127.0.0.1", resolve);
55+
});
56+
57+
const apiPort = mockApiServer.address().port;
58+
59+
try {
60+
const options = toOctokitOptions({
61+
githubToken: "test_token",
62+
githubApiUrl: `http://127.0.0.1:${apiPort}`,
63+
// No proxy specified
64+
});
65+
66+
const octokit = new SemanticReleaseOctokit(options);
67+
68+
// Test that the custom fetch function is still created (even without proxy)
69+
t.true(typeof options.request.fetch === "function");
70+
71+
const response = await options.request.fetch(
72+
`http://127.0.0.1:${apiPort}/test`,
73+
{
74+
method: "GET",
75+
headers: {
76+
Authorization: "token test_token",
77+
},
78+
},
79+
);
80+
81+
t.is(response.status, 200);
82+
t.true(requestReceived);
83+
84+
const data = await response.json();
85+
t.is(data.tag_name, "v1.0.0");
86+
} finally {
87+
mockApiServer.close();
88+
}
89+
});

test/to-octokit-options.test.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createServer as _createServer } from "node:https";
33
import test from "ava";
44
import { HttpProxyAgent } from "http-proxy-agent";
55
import { HttpsProxyAgent } from "https-proxy-agent";
6+
import { ProxyAgent } from "undici";
67

78
import { toOctokitOptions } from "../lib/octokit.js";
89

@@ -67,3 +68,92 @@ test("githubApiUrl with trailing slash", async (t) => {
6768
});
6869
t.is(options.baseUrl, "http://api.localhost:10001");
6970
});
71+
72+
test("fetch function uses ProxyAgent with proxy", async (t) => {
73+
const proxyUrl = "http://localhost:1002";
74+
const options = toOctokitOptions({
75+
githubToken: "github_token",
76+
githubUrl: "https://localhost:10001",
77+
githubApiPathPrefix: "",
78+
proxy: proxyUrl,
79+
});
80+
81+
t.true(typeof options.request.fetch === "function");
82+
83+
// Test that the fetch function is created and different from the default undici fetch
84+
const { fetch: undiciFetch } = await import("undici");
85+
t.not(options.request.fetch, undiciFetch);
86+
});
87+
88+
test("fetch function does not use ProxyAgent without proxy", async (t) => {
89+
const options = toOctokitOptions({
90+
githubToken: "github_token",
91+
githubUrl: "https://localhost:10001",
92+
githubApiPathPrefix: "",
93+
});
94+
95+
t.true(typeof options.request.fetch === "function");
96+
97+
// Test that the fetch function is created and different from the default undici fetch
98+
const { fetch: undiciFetch } = await import("undici");
99+
t.not(options.request.fetch, undiciFetch);
100+
});
101+
102+
test("fetch function preserves original fetch options", async (t) => {
103+
const proxyUrl = "http://localhost:1002";
104+
const options = toOctokitOptions({
105+
githubToken: "github_token",
106+
proxy: proxyUrl,
107+
});
108+
109+
// Test that we get a custom fetch function when proxy is set
110+
t.true(typeof options.request.fetch === "function");
111+
112+
// Test that we can call the function without errors (even though we can't mock the actual fetch)
113+
t.notThrows(() => {
114+
const fetchFn = options.request.fetch;
115+
// Just verify it's a function that can be called with the expected signature
116+
t.is(typeof fetchFn, "function");
117+
t.is(fetchFn.length, 2); // fetch function should accept 2 parameters (url, options)
118+
});
119+
});
120+
121+
test("both agent and fetch are provided for backwards compatibility", async (t) => {
122+
const proxyUrl = "http://localhost:1002";
123+
const options = toOctokitOptions({
124+
githubToken: "github_token",
125+
githubUrl: "https://localhost:10001",
126+
githubApiPathPrefix: "",
127+
proxy: proxyUrl,
128+
});
129+
130+
const { request, ...rest } = options;
131+
132+
// Should have both agent and fetch for compatibility
133+
t.true(request.agent instanceof HttpsProxyAgent);
134+
t.true(typeof request.fetch === "function");
135+
136+
t.deepEqual(rest, {
137+
baseUrl: "https://localhost:10001",
138+
auth: "github_token",
139+
});
140+
});
141+
142+
test("only fetch is provided when no proxy is set", async (t) => {
143+
const options = toOctokitOptions({
144+
githubToken: "github_token",
145+
githubUrl: "https://localhost:10001",
146+
githubApiPathPrefix: "",
147+
});
148+
149+
const { request, ...rest } = options;
150+
151+
// Should have fetch function but no agent when no proxy
152+
t.is(request.agent, undefined);
153+
t.true(typeof request.fetch === "function");
154+
155+
t.deepEqual(rest, {
156+
baseUrl: "https://localhost:10001",
157+
auth: "github_token",
158+
});
159+
});

0 commit comments

Comments
 (0)