Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

96 lines · 4.3 KBRaw History
1import { randomUUID } from "node:crypto";
2import { mkdir, rm, stat, writeFile } from "node:fs/promises";
3import path from "node:path";
4import { errMsg, failure, StepFailure } from "../job.js";
5import { globPaths, inputList, truthyInput } from "../util.js";
6import { downloadTo } from "./cache.js";
7import type { Builtin } from "./index.js";
8
9function lca(dirs: string[]): string {
10 if (dirs.length === 0) return "/";
11 let parts = dirs[0]!.split("/");
12 for (const d of dirs.slice(1)) {
13 const p = d.split("/");
14 let i = 0;
15 while (i < parts.length && i < p.length && parts[i] === p[i]) i++;
16 parts = parts.slice(0, i);
17 }
18 return parts.join("/") || "/";
19}
20
21export const artifactUpload: Builtin = async (c) => {
22 const name = c.input("name", "artifact");
23 if (/[":<>|*?\\/\r\n]/.test(name)) throw failure(`The artifact name is not valid: ${name}. Contains invalid characters.`);
24 const patterns = inputList(c.input("path"));
25 if (!patterns.length) throw failure("Input required and not supplied: path");
26 const ifNone = c.input("if-no-files-found", "warn").toLowerCase();
27 const includeHidden = truthyInput(c.input("include-hidden-files"), false);
28 let files = globPaths(c.job.workspace, patterns, { filesOnly: true });
29 if (!includeHidden) files = files.filter((f) => !path.relative(c.job.workspace, f).split("/").some((s) => s.startsWith(".") && s !== ".."));
30 if (files.length === 0) {
31 const msg = `No files were found with the provided path: ${patterns.join(", ")}. No artifacts will be uploaded.`;
32 if (ifNone === "error") throw failure(msg);
33 if (ifNone === "warn") c.warn(msg);
34 else c.log(msg);
35 return;
36 }
37 if (!c.job.api.online) {
38 c.warn("Artifact service is not available in offline mode; skipping upload.");
39 return;
40 }
41 const root = lca(files.map((f) => path.dirname(f)));
42 const rel = files.map((f) => path.relative(root, f));
43 c.log(`With the provided path, there will be ${files.length} file(s) uploaded (root: ${root})`);
44 const list = path.join(c.job.tempDir, `artifact-${randomUUID()}.list`);
45 const archive = path.join(c.job.tempDir, `artifact-${randomUUID()}.tgz`);
46 try {
47 await writeFile(list, rel.join("\0") + "\0");
48 await c.run("tar", ["-czf", archive, "-C", root, "--null", "-T", list], { silent: true });
49 const size = (await stat(archive)).size;
50 const q = new URLSearchParams();
51 const retention = c.input("retention-days");
52 if (retention) q.set("retention-days", retention);
53 if (truthyInput(c.input("overwrite"), false)) q.set("overwrite", "true");
54 const qs = q.toString();
55 const res = await c.job.api.putFile(`/artifacts/${encodeURIComponent(name)}${qs ? `?${qs}` : ""}`, archive);
56 if (res.status === 409) throw failure(`An artifact with the name '${name}' already exists. Set 'overwrite: true' to replace it.`);
57 let body: { id?: string | number; url?: string } = {};
58 try {
59 body = (await res.json()) as typeof body;
60 } catch {
61 /* no body */
62 }
63 c.log(`Artifact '${name}' uploaded (${size} bytes compressed).`);
64 if (body.id !== undefined) c.setOutput("artifact-id", String(body.id));
65 if (body.url) c.setOutput("artifact-url", body.url);
66 } catch (e) {
67 if (e instanceof StepFailure) throw e;
68 throw failure(`Failed to upload artifact '${name}': ${errMsg(e)}`);
69 } finally {
70 await rm(list, { force: true });
71 await rm(archive, { force: true });
72 }
73};
74
75export const artifactDownload: Builtin = async (c) => {
76 const name = c.input("name");
77 if (!name) {
78 throw failure("download-artifact without `name` (download all / `pattern`) is not supported on Diffinery runners yet; specify `name`.");
79 }
80 if (!c.job.api.online) throw failure("Artifact service is not available in offline mode.");
81 const dest = path.resolve(c.job.workspace, c.input("path", "."));
82 await mkdir(dest, { recursive: true });
83 const archive = path.join(c.job.tempDir, `artifact-${randomUUID()}.tgz`);
84 try {
85 try {
86 await downloadTo(c, c.job.api.url(`/artifacts/${encodeURIComponent(name)}`), archive);
87 } catch (e) {
88 throw failure(`Unable to download artifact '${name}': ${errMsg(e)}`);
89 }
90 await c.run("tar", ["-xzf", archive, "-C", dest], { silent: true });
91 c.log(`Artifact '${name}' downloaded to ${dest}`);
92 c.setOutput("download-path", dest);
93 } finally {
94 await rm(archive, { force: true });
95 }
96};