Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions lib/octokit.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { throttling } from "@octokit/plugin-throttling";
import urljoin from "url-join";
import { HttpProxyAgent } from "http-proxy-agent";
import { HttpsProxyAgent } from "https-proxy-agent";
import { ProxyAgent, fetch as undiciFetch } from "undici";

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

/**
* @param {{githubToken: string, proxy: any} | {githubUrl: string, githubApiPathPrefix: string, githubApiUrl: string,githubToken: string, proxy: any}} options
* @returns {{ auth: string, baseUrl?: string, request: { agent?: any } }}
* @returns {{ auth: string, baseUrl?: string, request: { agent?: any, fetch?: any } }}
*/
export function toOctokitOptions(options) {
const baseUrl =
Expand All @@ -69,11 +70,17 @@ export function toOctokitOptions(options) {
: new HttpsProxyAgent(options.proxy, options.proxy)
: undefined;

const fetchWithDispatcher = (url, opts) =>
undiciFetch(url, {
...opts,
dispatcher: options.proxy
? new ProxyAgent({ uri: options.proxy })
: undefined,
});

return {
...(baseUrl ? { baseUrl } : {}),
auth: options.githubToken,
request: {
agent,
},
request: { agent, fetch: fetchWithDispatcher },
};
}
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"mime": "^4.0.0",
"p-filter": "^4.0.0",
"tinyglobby": "^0.2.14",
"undici": "^7.0.0",
"url-join": "^5.0.0"
},
"devDependencies": {
Expand Down
89 changes: 89 additions & 0 deletions test/octokit-proxy-integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { createServer } from "node:http";
import { createServer as createHttpsServer } from "node:https";
import { readFileSync } from "node:fs";
import { join } from "node:path";

import test from "ava";
import { SemanticReleaseOctokit, toOctokitOptions } from "../lib/octokit.js";

process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0;

test("Octokit proxy setup creates proper fetch function", async (t) => {
// This test verifies that the proxy setup creates the expected function structure
// without actually testing network connectivity which can be flaky in CI environments

const options = toOctokitOptions({
githubToken: "test_token",
githubApiUrl: "https://api.github.com",
proxy: "http://proxy.example.com:8080",
});

const octokit = new SemanticReleaseOctokit(options);

// Verify that the options are set up correctly
t.true(typeof options.request.fetch === "function");
t.is(options.auth, "test_token");
t.is(options.baseUrl, "https://api.github.com");

// Verify that both agent (for backwards compatibility) and fetch are present
t.truthy(options.request.agent);
t.truthy(options.request.fetch);

// Verify that the fetch function has the correct signature
t.is(options.request.fetch.length, 2);
});

test("Octokit works without proxy using custom fetch", async (t) => {
let requestReceived = false;

// Create a mock GitHub API server
const mockApiServer = createServer((req, res) => {
requestReceived = true;
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
id: 1,
tag_name: "v1.0.0",
name: "Test Release",
body: "Test release body",
}),
);
});

await new Promise((resolve) => {
mockApiServer.listen(0, "127.0.0.1", resolve);
});

const apiPort = mockApiServer.address().port;

try {
const options = toOctokitOptions({
githubToken: "test_token",
githubApiUrl: `http://127.0.0.1:${apiPort}`,
// No proxy specified
});

const octokit = new SemanticReleaseOctokit(options);

// Test that the custom fetch function is still created (even without proxy)
t.true(typeof options.request.fetch === "function");

const response = await options.request.fetch(
`http://127.0.0.1:${apiPort}/test`,
{
method: "GET",
headers: {
Authorization: "token test_token",
},
},
);

t.is(response.status, 200);
t.true(requestReceived);

const data = await response.json();
t.is(data.tag_name, "v1.0.0");
} finally {
mockApiServer.close();
}
});
90 changes: 90 additions & 0 deletions test/to-octokit-options.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createServer as _createServer } from "node:https";
import test from "ava";
import { HttpProxyAgent } from "http-proxy-agent";
import { HttpsProxyAgent } from "https-proxy-agent";
import { ProxyAgent } from "undici";

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

Expand Down Expand Up @@ -67,3 +68,92 @@ test("githubApiUrl with trailing slash", async (t) => {
});
t.is(options.baseUrl, "http://api.localhost:10001");
});

test("fetch function uses ProxyAgent with proxy", async (t) => {
const proxyUrl = "http://localhost:1002";
const options = toOctokitOptions({
githubToken: "github_token",
githubUrl: "https://localhost:10001",
githubApiPathPrefix: "",
proxy: proxyUrl,
});

t.true(typeof options.request.fetch === "function");

// Test that the fetch function is created and different from the default undici fetch
const { fetch: undiciFetch } = await import("undici");
t.not(options.request.fetch, undiciFetch);
});

test("fetch function does not use ProxyAgent without proxy", async (t) => {
const options = toOctokitOptions({
githubToken: "github_token",
githubUrl: "https://localhost:10001",
githubApiPathPrefix: "",
});

t.true(typeof options.request.fetch === "function");

// Test that the fetch function is created and different from the default undici fetch
const { fetch: undiciFetch } = await import("undici");
t.not(options.request.fetch, undiciFetch);
});

test("fetch function preserves original fetch options", async (t) => {
const proxyUrl = "http://localhost:1002";
const options = toOctokitOptions({
githubToken: "github_token",
proxy: proxyUrl,
});

// Test that we get a custom fetch function when proxy is set
t.true(typeof options.request.fetch === "function");

// Test that we can call the function without errors (even though we can't mock the actual fetch)
t.notThrows(() => {
const fetchFn = options.request.fetch;
// Just verify it's a function that can be called with the expected signature
t.is(typeof fetchFn, "function");
t.is(fetchFn.length, 2); // fetch function should accept 2 parameters (url, options)
});
});

test("both agent and fetch are provided for backwards compatibility", async (t) => {
const proxyUrl = "http://localhost:1002";
const options = toOctokitOptions({
githubToken: "github_token",
githubUrl: "https://localhost:10001",
githubApiPathPrefix: "",
proxy: proxyUrl,
});

const { request, ...rest } = options;

// Should have both agent and fetch for compatibility
t.true(request.agent instanceof HttpsProxyAgent);
t.true(typeof request.fetch === "function");

t.deepEqual(rest, {
baseUrl: "https://localhost:10001",
auth: "github_token",
});
});

test("only fetch is provided when no proxy is set", async (t) => {
const options = toOctokitOptions({
githubToken: "github_token",
githubUrl: "https://localhost:10001",
githubApiPathPrefix: "",
});

const { request, ...rest } = options;

// Should have fetch function but no agent when no proxy
t.is(request.agent, undefined);
t.true(typeof request.fetch === "function");

t.deepEqual(rest, {
baseUrl: "https://localhost:10001",
auth: "github_token",
});
});