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.tsBlame160 lines · 2 contributors
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(),
2c3ba6ecopilot-swe-agent[bot]51 `gluecron-dl-${Date.now()}-${crypto.randomUUID().replace(/-/g, '').slice(0, 8)}.tar.gz`
abfa9adClaude52 );
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");
a28cedeClaude67 await fs.unlink(tmpPath).catch((err) => {
68 console.warn(
69 `[download-artifact] tmpPath cleanup failed for ${tmpPath}:`,
70 err instanceof Error ? err.message : err
71 );
72 });
abfa9adClaude73 } catch {
74 /* noop */
75 }
76 }
77}
78
79export const downloadArtifactAction: ActionHandler = {
80 name: "gluecron/download-artifact",
81 version: "v1",
82 async run(ctx): Promise<import("../action-registry").ActionResult> {
83 try {
84 const parsed = parseInputs(ctx);
85 if ("error" in parsed) {
86 return { exitCode: 1, stderr: parsed.error };
87 }
88
89 // Dynamic import so a missing helper module degrades gracefully.
90 let listArtifacts: typeof import("../workflow-artifacts").listArtifacts;
91 let downloadArtifact: typeof import("../workflow-artifacts").downloadArtifact;
92 try {
93 const mod = await import("../workflow-artifacts");
94 listArtifacts = mod.listArtifacts;
95 downloadArtifact = mod.downloadArtifact;
96 } catch (err) {
97 return {
98 exitCode: 0,
99 stderr:
100 "download-artifact unavailable; skipping (" +
101 (err instanceof Error ? err.message : String(err)) +
102 ")",
103 };
104 }
105
106 const listed = await listArtifacts(ctx.runId);
107 if (!listed.ok) {
108 return {
109 exitCode: parsed.optional ? 0 : 1,
110 stderr: `download-artifact: list failed: ${listed.error}`,
111 };
112 }
113
114 const match = listed.artifacts.find((a) => a.name === parsed.name);
115 if (!match) {
116 const msg = `download-artifact: no artifact named "${parsed.name}" on run ${ctx.runId}`;
117 return {
118 exitCode: parsed.optional ? 0 : 1,
119 stderr: msg,
120 outputs: { found: "false" },
121 };
122 }
123
124 const fetched = await downloadArtifact(match.id);
125 if (!fetched.ok) {
126 return {
127 exitCode: parsed.optional ? 0 : 1,
128 stderr: `download-artifact: fetch failed: ${fetched.error}`,
129 };
130 }
131
132 const destDir = join(ctx.workspace, parsed.path);
133 if (isArchive(fetched.contentType)) {
134 await extractArchive(fetched.content, destDir);
135 } else {
136 const outPath = join(destDir, parsed.name);
137 await mkdir(dirname(outPath), { recursive: true });
138 await Bun.write(outPath, fetched.content);
139 }
140
141 return {
142 exitCode: 0,
143 stdout: `Downloaded artifact "${parsed.name}" (${fetched.content.byteLength} bytes) to ${parsed.path}`,
144 outputs: {
145 found: "true",
146 "artifact-id": match.id,
147 name: parsed.name,
148 size: String(fetched.content.byteLength),
149 },
150 };
151 } catch (err) {
152 return {
153 exitCode: 1,
154 stderr:
155 "download-artifact error: " +
156 (err instanceof Error ? err.message : String(err)),
157 };
158 }
159 },
160};