1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
|
import { mkdir } from "fs/promises";
import { dirname, join } from "path";
import { tmpdir } from "os";
import type { ActionHandler, ActionContext } from "../action-registry";
function parseInputs(
ctx: ActionContext
): { name: string; path: string; optional: boolean } | { error: string } {
const w = ctx.with || {};
const name = typeof w.name === "string" ? w.name.trim() : "";
const pathRaw = typeof w.path === "string" ? w.path.trim() : "";
const path = pathRaw || ".";
const optional = w.optional === true || w.optional === "true";
if (!name) return { error: "download-artifact: `name` is required" };
return { name, path, optional };
}
function isArchive(contentType: string): boolean {
const ct = (contentType || "").toLowerCase();
return (
ct === "application/gzip" ||
ct === "application/x-gzip" ||
ct === "application/x-tar" ||
ct === "application/tar+gzip"
);
}
async function extractArchive(content: Buffer, destDir: string): Promise<void> {
await mkdir(destDir, { recursive: true });
const tmpPath = join(
tmpdir(),
`gluecron-dl-${Date.now()}-${crypto.randomUUID().replace(/-/g, '').slice(0, 8)}.tar.gz`
);
await Bun.write(tmpPath, content);
try {
const proc = Bun.spawn(
["tar", "-xf", tmpPath, "-C", destDir],
{ stdout: "pipe", stderr: "pipe" }
);
const exit = await proc.exited;
if (exit !== 0) {
const err = await new Response(proc.stderr).text().catch(() => "");
throw new Error(`tar extract failed (exit ${exit}): ${err.slice(0, 200)}`);
}
} finally {
try {
const fs = await import("fs/promises");
await fs.unlink(tmpPath).catch(() => {});
} catch {
}
}
}
export const downloadArtifactAction: ActionHandler = {
name: "gluecron/download-artifact",
version: "v1",
async run(ctx): Promise<import("../action-registry").ActionResult> {
try {
const parsed = parseInputs(ctx);
if ("error" in parsed) {
return { exitCode: 1, stderr: parsed.error };
}
let listArtifacts: typeof import("../workflow-artifacts").listArtifacts;
let downloadArtifact: typeof import("../workflow-artifacts").downloadArtifact;
try {
const mod = await import("../workflow-artifacts");
listArtifacts = mod.listArtifacts;
downloadArtifact = mod.downloadArtifact;
} catch (err) {
return {
exitCode: 0,
stderr:
"download-artifact unavailable; skipping (" +
(err instanceof Error ? err.message : String(err)) +
")",
};
}
const listed = await listArtifacts(ctx.runId);
if (!listed.ok) {
return {
exitCode: parsed.optional ? 0 : 1,
stderr: `download-artifact: list failed: ${listed.error}`,
};
}
const match = listed.artifacts.find((a) => a.name === parsed.name);
if (!match) {
const msg = `download-artifact: no artifact named "${parsed.name}" on run ${ctx.runId}`;
return {
exitCode: parsed.optional ? 0 : 1,
stderr: msg,
outputs: { found: "false" },
};
}
const fetched = await downloadArtifact(match.id);
if (!fetched.ok) {
return {
exitCode: parsed.optional ? 0 : 1,
stderr: `download-artifact: fetch failed: ${fetched.error}`,
};
}
const destDir = join(ctx.workspace, parsed.path);
if (isArchive(fetched.contentType)) {
await extractArchive(fetched.content, destDir);
} else {
const outPath = join(destDir, parsed.name);
await mkdir(dirname(outPath), { recursive: true });
await Bun.write(outPath, fetched.content);
}
return {
exitCode: 0,
stdout: `Downloaded artifact "${parsed.name}" (${fetched.content.byteLength} bytes) to ${parsed.path}`,
outputs: {
found: "true",
"artifact-id": match.id,
name: parsed.name,
size: String(fetched.content.byteLength),
},
};
} catch (err) {
return {
exitCode: 1,
stderr:
"download-artifact error: " +
(err instanceof Error ? err.message : String(err)),
};
}
},
};
|