fix: big file uploads (#562)

* fix: use readableWebStream() to stream asset contents

This allows the uploads to finish without mismatched Content-Length,
likely because the original method implied a wrong body encoding or
something similar. Unfortunately a GitHub server API mock was not
readily available so I had to test manually with a barebones repository.

Fixes: #555
Fixes: #556
Signed-off-by: WANG Xuerui <git@xen0n.name>

* feat: log when each asset is successfully uploaded

Signed-off-by: WANG Xuerui <git@xen0n.name>

* build: refresh dist

Signed-off-by: WANG Xuerui <git@xen0n.name>

* style: format with prettier

Signed-off-by: WANG Xuerui <git@xen0n.name>

---------

Signed-off-by: WANG Xuerui <git@xen0n.name>
This commit is contained in:
WÁNG Xuěruì 2025-01-08 02:42:55 +08:00 committed by GitHub
parent 33fcd69d45
commit deddb09c64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 29 additions and 25 deletions

View File

@ -14,11 +14,10 @@ describe("github", () => {
describe("asset", () => { describe("asset", () => {
it("derives asset info from a path", async () => { it("derives asset info from a path", async () => {
const { name, mime, size, data } = asset("tests/data/foo/bar.txt"); const { name, mime, size } = asset("tests/data/foo/bar.txt");
assert.equal(name, "bar.txt"); assert.equal(name, "bar.txt");
assert.equal(mime, "text/plain"); assert.equal(mime, "text/plain");
assert.equal(size, 10); assert.equal(size, 10);
assert.equal(await text(data), "release me");
}); });
}); });
}); });

2
dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
import { GitHub } from "@actions/github/lib/utils"; import { GitHub } from "@actions/github/lib/utils";
import { Config, isTag, releaseBody, alignAssetName } from "./util"; import { Config, isTag, releaseBody, alignAssetName } from "./util";
import { createReadStream, statSync, type ReadStream } from "fs"; import { statSync } from "fs";
import { open } from "fs/promises";
import { getType } from "mime"; import { getType } from "mime";
import { basename } from "path"; import { basename } from "path";
@ -10,7 +11,6 @@ export interface ReleaseAsset {
name: string; name: string;
mime: string; mime: string;
size: number; size: number;
data: ReadStream;
} }
export interface Release { export interface Release {
@ -145,7 +145,6 @@ export const asset = (path: string): ReleaseAsset => {
name: basename(path), name: basename(path),
mime: mimeOrDefault(path), mime: mimeOrDefault(path),
size: statSync(path).size, size: statSync(path).size,
data: createReadStream(path, "binary"),
}; };
}; };
@ -161,7 +160,7 @@ export const upload = async (
currentAssets: Array<{ id: number; name: string }>, currentAssets: Array<{ id: number; name: string }>,
): Promise<any> => { ): Promise<any> => {
const [owner, repo] = config.github_repository.split("/"); const [owner, repo] = config.github_repository.split("/");
const { name, size, mime, data: body } = asset(path); const { name, mime, size } = asset(path);
const currentAsset = currentAssets.find( const currentAsset = currentAssets.find(
// note: GitHub renames asset filenames that have special characters, non-alphanumeric characters, and leading or trailing periods. The "List release assets" endpoint lists the renamed filenames. // note: GitHub renames asset filenames that have special characters, non-alphanumeric characters, and leading or trailing periods. The "List release assets" endpoint lists the renamed filenames.
// due to this renaming we need to be mindful when we compare the file name we're uploading with a name github may already have rewritten for logical comparison // due to this renaming we need to be mindful when we compare the file name we're uploading with a name github may already have rewritten for logical comparison
@ -179,25 +178,31 @@ export const upload = async (
console.log(`⬆️ Uploading ${name}...`); console.log(`⬆️ Uploading ${name}...`);
const endpoint = new URL(url); const endpoint = new URL(url);
endpoint.searchParams.append("name", name); endpoint.searchParams.append("name", name);
const resp = await github.request({ const fh = await open(path);
method: "POST", try {
url: endpoint.toString(), const resp = await github.request({
headers: { method: "POST",
"content-length": `${size}`, url: endpoint.toString(),
"content-type": mime, headers: {
authorization: `token ${config.github_token}`, "content-length": `${size}`,
}, "content-type": mime,
data: body, authorization: `token ${config.github_token}`,
}); },
const json = resp.data; data: fh.readableWebStream({ type: "bytes" }),
if (resp.status !== 201) { });
throw new Error( const json = resp.data;
`Failed to upload release asset ${name}. received status code ${ if (resp.status !== 201) {
resp.status throw new Error(
}\n${json.message}\n${JSON.stringify(json.errors)}`, `Failed to upload release asset ${name}. received status code ${
); resp.status
}\n${json.message}\n${JSON.stringify(json.errors)}`,
);
}
console.log(`✅ Uploaded ${name}`);
return json;
} finally {
await fh.close();
} }
return json;
}; };
export const release = async ( export const release = async (