// // Workflow artifacts: download to disk, reveal in Finder, and open. Artifacts // arrive as zip archives, so "open" unzips alongside the download when possible // and otherwise hands the archive to Finder. // const builders = require("../views/builders.js"); const config = require("../util/config.js"); const selection = require("./selection.js"); const ui = require("../ui.js"); const { exec } = require("../util/exec.js"); /** Resolves the configured download directory for a repository. */ function downloadDirectory(repo, run) { const configured = config.artifactDownloadPath(); const base = nova.path.isAbsolute(configured) ? configured : nova.path.join(repo.dir || nova.workspace.path || nova.path.expanduser("~"), configured); return nova.path.join(base, repo.owner, repo.name, String(run ? run.id : "runs")); } function artifactFrom(argument) { const node = selection.selectedNode(argument); if (!node || !node.data || !node.data.artifact) return null; return { node: node, repo: selection.repoOf(node), artifact: node.data.artifact, run: node.data.run, }; } async function download(store, repo, run, artifact) { if (artifact.expired) { ui.warn(`${artifact.name} has expired and is no longer downloadable.`); return null; } const api = store.apiFor(repo); if (!api) return null; const target = nova.path.join(downloadDirectory(repo, run), `${artifact.name}.zip`); try { const bytes = await api.downloadArtifact(repo, artifact); ui.writeBinary(target, bytes); } catch (error) { ui.error( `Could not download ${artifact.name}: ${error.message || error}` + (error.status === 403 ? " The token needs the Actions read scope for artifacts." : ""), ); return null; } const size = builders.formatBytes( artifact.size_in_bytes || (nova.fs.stat(target) || {}).size || 0, ); ui.info(`Downloaded ${artifact.name} (${size}) to ${target}.`); return target; } /** Unzips into a sibling folder; returns the folder or null when unzip fails. */ async function unzip(zipPath) { const folder = zipPath.replace(/\.zip$/i, ""); const result = await exec("/usr/bin/env", ["unzip", "-o", "-q", zipPath, "-d", folder]); return result.status === 0 ? folder : null; } function register(store) { nova.commands.register("gitea.downloadArtifact", async (argument) => { const context = artifactFrom(argument); if (!context || !context.repo) { ui.warn("Select an artifact in the Gitea sidebar first."); return; } const path = await download(store, context.repo, context.run, context.artifact); if (path) nova.fs.reveal(path); }); nova.commands.register("gitea.openArtifact", async (argument) => { const context = artifactFrom(argument); if (!context || !context.repo) { ui.warn("Select an artifact in the Gitea sidebar first."); return; } const zipPath = await download(store, context.repo, context.run, context.artifact); if (!zipPath) return; const folder = await unzip(zipPath); if (!folder) { nova.fs.reveal(zipPath); return; } // A single-file artifact is far more useful opened than revealed. let entries = []; try { entries = nova.fs.listdir(folder).filter((entry) => !entry.startsWith(".")); } catch (error) { entries = []; } if (entries.length === 1) { const only = nova.path.join(folder, entries[0]); const stats = nova.fs.stat(only); if (stats && stats.isFile()) { await nova.workspace.openFile(only); return; } } nova.fs.reveal(folder); }); nova.commands.register("gitea.revealArtifact", async (argument) => { const context = artifactFrom(argument); if (!context || !context.repo) { ui.warn("Select an artifact in the Gitea sidebar first."); return; } const expected = nova.path.join( downloadDirectory(context.repo, context.run), `${context.artifact.name}.zip`, ); if (nova.fs.access(expected, nova.fs.F_OK)) { nova.fs.reveal(expected); return; } const path = await download(store, context.repo, context.run, context.artifact); if (path) nova.fs.reveal(path); }); } exports.register = register; exports.downloadDirectory = downloadDirectory;