Skip to content
Merged
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
4 changes: 2 additions & 2 deletions lib/octokit.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +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 { Agent, ProxyAgent, fetch as undiciFetch } from "undici";

import { RETRY_CONF } from "./definitions/retry.js";
import { THROTTLE_CONF } from "./definitions/throttle.js";
Expand Down Expand Up @@ -75,7 +75,7 @@ export function toOctokitOptions(options) {
...opts,
dispatcher: options.proxy
? new ProxyAgent({ uri: options.proxy })
: undefined,
: new Agent(),
});

return {
Expand Down
1 change: 0 additions & 1 deletion lib/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ export default async function publish(pluginConfig, context, { Octokit }) {
name: fileName,
headers: {
"content-type": mime.getType(extname(fileName)) || "text/plain",
"content-length": file.size,
},
};

Expand Down
95 changes: 95 additions & 0 deletions test/publish.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,101 @@ test("Publish a release with one asset", async (t) => {
t.true(fetch.done());
});

test("Publish upload request does not set content-length manually", async (t) => {
const owner = "test_user";
const repo = "test_repo";
const env = { GITHUB_TOKEN: "github_token" };
const pluginConfig = {
assets: [".dotfile"],
};
const nextRelease = {
gitTag: "v1.0.0",
name: "v1.0.0",
notes: "Test release note body",
};
const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git` };
const untaggedReleaseUrl = `https://github.com/${owner}/${repo}/releases/untagged-123`;
const releaseUrl = `https://github.com/${owner}/${repo}/releases/${nextRelease.version}`;
const assetUrl = `https://github.com/${owner}/${repo}/releases/download/${nextRelease.version}/.dotfile`;
const releaseId = 1;
const uploadOrigin = "https://uploads.github.local";
const uploadUri = `/api/uploads/repos/${owner}/${repo}/releases/${releaseId}/assets`;
const uploadUrl = `${uploadOrigin}${uploadUri}{?name,label}`;
const branch = "test_branch";
const assetUploadUrl = `${uploadOrigin}${uploadUri}?name=${encodeURIComponent(".dotfile")}&`;

/** @type {Record<string, string>} */
let uploadHeaders;

const fetch = async (url, request = {}) => {
if (
url === `https://api.github.local/repos/${owner}/${repo}/releases` &&
request.method === "POST"
) {
return new Response(
JSON.stringify({
upload_url: uploadUrl,
html_url: untaggedReleaseUrl,
id: releaseId,
}),
{ headers: { "content-type": "application/json" } },
);
}

if (
url ===
`https://api.github.local/repos/${owner}/${repo}/releases/${releaseId}` &&
request.method === "PATCH"
) {
return new Response(JSON.stringify({ html_url: releaseUrl }), {
headers: { "content-type": "application/json" },
});
}

if (url === assetUploadUrl && request.method === "POST") {
uploadHeaders = request.headers;
return new Response(JSON.stringify({ browser_download_url: assetUrl }), {
headers: { "content-type": "application/json" },
});
}

throw new Error(`Unexpected request ${request.method} ${url}`);
};

const result = await publish(
pluginConfig,
{
cwd,
env,
options,
branch: { name: branch, type: "release", main: true },
nextRelease,
logger: t.context.logger,
},
{
Octokit: TestOctokit.defaults((options) => ({
...options,
request: { ...options.request, fetch },
})),
},
);

t.is(result.url, releaseUrl);
t.true(t.context.log.calledWith("Published GitHub release: %s", releaseUrl));
t.true(t.context.log.calledWith("Published file %s", assetUrl));
t.truthy(uploadHeaders);

const normalizedHeaders = Object.fromEntries(
Object.entries(uploadHeaders).map(([name, value]) => [
name.toLowerCase(),
String(value),
]),
);

t.is(normalizedHeaders["content-type"], "text/plain");
t.falsy(normalizedHeaders["content-length"]);
});

test("Publish a release with one asset and custom github url", async (t) => {
const owner = "test_user";
const repo = "test_repo";
Expand Down
93 changes: 91 additions & 2 deletions test/to-octokit-options.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { createServer as _createServer } from "node:https";
import { createServer } from "node:http";

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 @@ -157,3 +156,93 @@ test("only fetch is provided when no proxy is set", async (t) => {
auth: "github_token",
});
});

test("fetch without proxy stays on direct undici dispatcher", async (t) => {
const server = createServer((req, res) => {
req.resume();
req.on("end", () => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("ok");
});
});

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

const { port } = server.address();
const url = `http://127.0.0.1:${port}/upload`;

try {
// Initialize Node's built-in fetch first to mimic real runtime usage.
await fetch(url);

const options = toOctokitOptions({
githubToken: "github_token",
githubApiUrl: `http://127.0.0.1:${port}`,
});

// throws because the content-length does not match the body length
const error = await t.throwsAsync(
options.request.fetch(url, {
method: "POST",
headers: { "content-length": "10" },
body: "abc",
}),
);

const causeStack = error?.cause?.stack || "";
t.false(causeStack.includes("node:internal/deps/undici/undici"));
t.true(causeStack.includes("/node_modules/undici/"));
} finally {
await new Promise((resolve) => {
server.close(resolve);
});
}
});

test("fetch function sets content-length for Buffer body", async (t) => {
let requestContentLength;
let receivedBodyLength = 0;
const body = Buffer.from("upload-body");

const server = createServer((req, res) => {
requestContentLength = req.headers["content-length"];
req.on("data", (chunk) => {
receivedBodyLength += chunk.length;
});
req.on("end", () => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
});
});

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

const { port } = server.address();
const url = `http://127.0.0.1:${port}/upload`;

try {
const options = toOctokitOptions({
githubToken: "github_token",
githubApiUrl: `http://127.0.0.1:${port}`,
});

const response = await options.request.fetch(url, {
method: "POST",
headers: { "content-type": "application/octet-stream" },
body,
});

t.is(response.status, 200);
t.deepEqual(await response.json(), { ok: true });
t.is(requestContentLength, `${body.byteLength}`);
t.is(receivedBodyLength, body.byteLength);
} finally {
await new Promise((resolve) => {
server.close(resolve);
});
}
});