@@ -0,0 +1,160 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user