Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

download-artifact-action.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

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