@@ -51,12 +51,34 @@ jobs:
|
|||||||
api="${GITEA_SERVER_URL%/}/api/v1/repos/${GITEA_REPOSITORY}/releases"
|
api="${GITEA_SERVER_URL%/}/api/v1/repos/${GITEA_REPOSITORY}/releases"
|
||||||
payload=$(jq -n --arg tag "$GITEA_TAG" --arg commit "$GITEA_COMMIT" \
|
payload=$(jq -n --arg tag "$GITEA_TAG" --arg commit "$GITEA_COMMIT" \
|
||||||
'{tag_name: $tag, name: $tag, target_commitish: $commit, draft: true}')
|
'{tag_name: $tag, name: $tag, target_commitish: $commit, draft: true}')
|
||||||
release=$(curl --fail-with-body --silent --show-error \
|
response=$(mktemp)
|
||||||
|
trap 'rm -f "$response"' EXIT
|
||||||
|
status=$(curl --silent --show-error --output "$response" --write-out '%{http_code}' \
|
||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
--data "$payload" "$api")
|
--data "$payload" "$api")
|
||||||
|
case "$status" in
|
||||||
|
201) release=$(cat "$response") ;;
|
||||||
|
409)
|
||||||
|
tag=$(jq -rn --arg tag "$GITEA_TAG" '$tag | @uri')
|
||||||
|
if ! release=$(curl --fail-with-body --silent --show-error \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" "$api/tags/$tag"); then
|
||||||
|
printf '%s\n' "$release" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
cat "$response" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
id=$(jq -er '.id | select(type == "number" and . > 0)' <<< "$release")
|
id=$(jq -er '.id | select(type == "number" and . > 0)' <<< "$release")
|
||||||
for asset in stanp-opencode.tgz stanp-opencode.tgz.sha256; do
|
for asset in stanp-opencode.tgz stanp-opencode.tgz.sha256; do
|
||||||
|
asset_ids=$(jq -r --arg name "$asset" '.assets[]? | select(.name == $name) | .id' <<< "$release")
|
||||||
|
for asset_id in $asset_ids; do
|
||||||
|
curl --fail-with-body --silent --show-error --request DELETE \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" "$api/$id/assets/$asset_id"
|
||||||
|
done
|
||||||
curl --fail-with-body --silent --show-error \
|
curl --fail-with-body --silent --show-error \
|
||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
--form "attachment=@$asset" "$api/$id/assets"
|
--form "attachment=@$asset" "$api/$id/assets"
|
||||||
|
|||||||
@@ -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