161 lines
6.3 KiB
TypeScript
161 lines
6.3 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { z } from "zod";
|
|
|
|
const workflow = z
|
|
.object({
|
|
jobs: z.object({
|
|
release: z.object({
|
|
steps: z.array(z.object({ name: z.string(), run: z.string().optional() })),
|
|
}),
|
|
}),
|
|
})
|
|
.parse(
|
|
Bun.YAML.parse(
|
|
await Bun.file(new URL("../.gitea/workflows/release.yml", import.meta.url)).text(),
|
|
),
|
|
);
|
|
const script = workflow.jobs.release.steps.find(
|
|
(step) => step.name === "Publish Gitea release",
|
|
)!.run!;
|
|
const base = "/api/v1/repos/test/project/releases";
|
|
const tag = "release/v1+rc#1";
|
|
const assets = ["stanp-opencode.tgz", "stanp-opencode.tgz.sha256"];
|
|
|
|
async function publish(status: number) {
|
|
const directory = await mkdtemp(join(tmpdir(), "stanp-release-"));
|
|
const requests: { method: string; path: string; body: unknown }[] = [];
|
|
const server = Bun.serve({
|
|
hostname: "127.0.0.1",
|
|
port: 0,
|
|
async fetch(request) {
|
|
const path = new URL(request.url).pathname;
|
|
if (request.headers.get("content-type")?.startsWith("multipart/form-data")) {
|
|
const attachment = (await request.formData()).get("attachment");
|
|
if (!(attachment instanceof File)) {
|
|
return new Response("Missing attachment", { status: 400 });
|
|
}
|
|
requests.push({
|
|
method: request.method,
|
|
path,
|
|
body: { name: attachment.name, content: await attachment.text() },
|
|
});
|
|
} else if (request.method === "POST" || request.method === "PATCH") {
|
|
requests.push({ method: request.method, path, body: await request.json() });
|
|
} else {
|
|
requests.push({ method: request.method, path, body: null });
|
|
}
|
|
if (request.method === "POST" && path === base) {
|
|
return Response.json(
|
|
status === 201
|
|
? { id: 42, assets: [] }
|
|
: { message: "release creation denied" },
|
|
{ status },
|
|
);
|
|
}
|
|
if (request.method === "GET" && path === `${base}/tags/${encodeURIComponent(tag)}`) {
|
|
return Response.json({
|
|
id: 42,
|
|
name: "Keep this title",
|
|
body: "Keep these notes",
|
|
assets: [
|
|
{ id: 10, name: assets[0] },
|
|
{ id: 11, name: "stanp-opencode.tgz.sig" },
|
|
{ id: 12, name: assets[1] },
|
|
],
|
|
});
|
|
}
|
|
if (request.method === "DELETE" && /\/42\/assets\/(10|12)$/.test(path)) {
|
|
return new Response(null, { status: 204 });
|
|
}
|
|
if (request.method === "POST" && path === `${base}/42/assets`) {
|
|
return Response.json({ id: 20 }, { status: 201 });
|
|
}
|
|
if (request.method === "PATCH" && path === `${base}/42`) {
|
|
return Response.json({ id: 42, draft: false });
|
|
}
|
|
return Response.json({ message: "unexpected request" }, { status: 500 });
|
|
},
|
|
});
|
|
try {
|
|
await Promise.all(
|
|
assets.map((asset) => writeFile(join(directory, asset), `contents of ${asset}`)),
|
|
);
|
|
const process = Bun.spawn(["bash", "-c", script], {
|
|
cwd: directory,
|
|
env: {
|
|
...Bun.env,
|
|
GITEA_SERVER_URL: server.url.origin,
|
|
GITEA_REPOSITORY: "test/project",
|
|
GITEA_TAG: tag,
|
|
GITEA_COMMIT: "abc123",
|
|
GITEA_TOKEN: "test-token",
|
|
},
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
timeout: 10_000,
|
|
});
|
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
process.exited,
|
|
new Response(process.stdout).text(),
|
|
new Response(process.stderr).text(),
|
|
]);
|
|
return { exitCode, output: stdout + stderr, requests };
|
|
} finally {
|
|
await server.stop(true);
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
for (const status of [201, 409]) {
|
|
test(`publishes both assets after release creation returns ${status}`, async () => {
|
|
const result = await publish(status);
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.requests.filter(({ method }) => method !== "DELETE")).toEqual([
|
|
{
|
|
method: "POST",
|
|
path: base,
|
|
body: { tag_name: tag, name: tag, target_commitish: "abc123", draft: true },
|
|
},
|
|
...(status === 409
|
|
? [{ method: "GET", path: `${base}/tags/${encodeURIComponent(tag)}`, body: null }]
|
|
: []),
|
|
...assets.map((asset) => ({
|
|
method: "POST",
|
|
path: `${base}/42/assets`,
|
|
body: { name: asset, content: `contents of ${asset}` },
|
|
})),
|
|
{ method: "PATCH", path: `${base}/42`, body: { draft: false } },
|
|
]);
|
|
const deletions = result.requests.filter(({ method }) => method === "DELETE");
|
|
expect(deletions).toEqual(
|
|
status === 409
|
|
? [10, 12].map((id) => ({
|
|
method: "DELETE",
|
|
path: `${base}/42/assets/${id}`,
|
|
body: null,
|
|
}))
|
|
: [],
|
|
);
|
|
const uploads = result.requests.filter(
|
|
({ method, path }) => method === "POST" && path === `${base}/42/assets`,
|
|
);
|
|
for (const [index, deletion] of deletions.entries()) {
|
|
expect(result.requests.indexOf(deletion)).toBeLessThan(
|
|
result.requests.indexOf(uploads[index]!),
|
|
);
|
|
}
|
|
}, 15_000);
|
|
}
|
|
|
|
test("non-conflict creation failure stops publishing and shows the response body", async () => {
|
|
const result = await publish(403);
|
|
expect(result.exitCode).not.toBe(0);
|
|
expect(result.output).toContain("release creation denied");
|
|
expect(result.requests.map(({ method, path }) => ({ method, path }))).toEqual([
|
|
{ method: "POST", path: base },
|
|
]);
|
|
}, 15_000);
|