From 465b20e0d9d15b825d3ab9048036b660f26a489b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:47:37 +0000 Subject: [PATCH 01/23] Initial plan: add upload_artifact safe output type Agent-Logs-Url: https://github.com/github/gh-aw/sessions/06153ed3-a241-400b-9414-3dc304516475 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-call-workflow.lock.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/smoke-call-workflow.lock.yml b/.github/workflows/smoke-call-workflow.lock.yml index ecd0e36a4e0..3d5fa1f6f9a 100644 --- a/.github/workflows/smoke-call-workflow.lock.yml +++ b/.github/workflows/smoke-call-workflow.lock.yml @@ -863,6 +863,7 @@ jobs: needs: safe_outputs if: needs.safe_outputs.outputs.call_workflow_name == 'smoke-workflow-call' permissions: + actions: read contents: read discussions: write issues: write From ff14be6987cc64a92a31f7f0be9b957733bc1760 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:18:04 +0000 Subject: [PATCH 02/23] feat: add upload_artifact safe output type with native GitHub Actions artifact support Agent-Logs-Url: https://github.com/github/gh-aw/sessions/06153ed3-a241-400b-9414-3dc304516475 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/safe_output_handler_manager.cjs | 4 +- actions/setup/js/upload_artifact.cjs | 427 ++++++++++++++++++ actions/setup/js/upload_artifact.test.cjs | 374 +++++++++++++++ pkg/workflow/compiler_safe_output_jobs.go | 18 + pkg/workflow/compiler_safe_outputs_config.go | 31 ++ pkg/workflow/compiler_types.go | 1 + pkg/workflow/compiler_yaml_main_job.go | 5 + pkg/workflow/js/safe_outputs_tools.json | 52 +++ pkg/workflow/publish_artifacts.go | 361 +++++++++++++++ pkg/workflow/publish_artifacts_test.go | 314 +++++++++++++ pkg/workflow/safe_outputs_config.go | 6 + pkg/workflow/safe_outputs_state.go | 1 + .../safe_outputs_tools_computation.go | 3 + .../basic-copilot.golden | 1 + .../with-imports.golden | 1 + 15 files changed, 1597 insertions(+), 2 deletions(-) create mode 100644 actions/setup/js/upload_artifact.cjs create mode 100644 actions/setup/js/upload_artifact.test.cjs create mode 100644 pkg/workflow/publish_artifacts.go create mode 100644 pkg/workflow/publish_artifacts_test.go diff --git a/actions/setup/js/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs index 05ea9f2cef7..bc11c8758aa 100644 --- a/actions/setup/js/safe_output_handler_manager.cjs +++ b/actions/setup/js/safe_output_handler_manager.cjs @@ -80,10 +80,10 @@ const HANDLER_MAP = { * Message types handled by standalone steps (not through the handler manager) * These types should not trigger warnings when skipped by the handler manager * - * Standalone types: upload_asset, noop + * Standalone types: upload_asset, upload_artifact, noop * - Have dedicated processing steps with specialized logic */ -const STANDALONE_STEP_TYPES = new Set(["upload_asset", "noop"]); +const STANDALONE_STEP_TYPES = new Set(["upload_asset", "upload_artifact", "noop"]); /** * Code-push safe output types that must succeed before remaining outputs are processed. diff --git a/actions/setup/js/upload_artifact.cjs b/actions/setup/js/upload_artifact.cjs new file mode 100644 index 00000000000..9e6ace0a243 --- /dev/null +++ b/actions/setup/js/upload_artifact.cjs @@ -0,0 +1,427 @@ +// @ts-check +/// + +/** + * upload_artifact handler + * + * Validates and stages artifact upload requests emitted by the model via the upload_artifact + * safe output tool. The model must have already copied the files it wants to upload to + * /tmp/gh-aw/safeoutputs/upload-artifacts/ before calling the tool. + * + * This handler: + * 1. Reads upload_artifact records from agent output. + * 2. Validates each request against the workflow's policy configuration. + * 3. Resolves the requested files (path or filter-based) from the staging directory. + * 4. Copies approved files into per-slot directories under /tmp/gh-aw/upload-artifacts/slot_N/. + * 5. Sets step outputs so the wrapping job's actions/upload-artifact steps can run conditionally. + * 6. Generates a temporary artifact ID for each slot. + * + * Environment variables consumed (set by the Go job builder): + * GH_AW_ARTIFACT_MAX_UPLOADS - Max number of upload_artifact calls allowed + * GH_AW_ARTIFACT_DEFAULT_RETENTION_DAYS - Default retention period + * GH_AW_ARTIFACT_MAX_RETENTION_DAYS - Maximum retention cap + * GH_AW_ARTIFACT_MAX_SIZE_BYTES - Maximum total bytes per upload + * GH_AW_ARTIFACT_ALLOWED_PATHS - JSON array of allowed path patterns + * GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE - "true" if skip_archive is permitted + * GH_AW_ARTIFACT_DEFAULT_SKIP_ARCHIVE - "true" if skip_archive defaults to true + * GH_AW_ARTIFACT_DEFAULT_IF_NO_FILES - "error" or "ignore" + * GH_AW_ARTIFACT_FILTERS_INCLUDE - JSON array of default include patterns + * GH_AW_ARTIFACT_FILTERS_EXCLUDE - JSON array of default exclude patterns + * GH_AW_AGENT_OUTPUT - Path to agent output file + * GH_AW_SAFE_OUTPUTS_STAGED - "true" for staged/dry-run mode + */ + +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); +const { loadAgentOutput } = require("./load_agent_output.cjs"); +const { getErrorMessage } = require("./error_helpers.cjs"); +const { globPatternToRegex } = require("./glob_pattern_helpers.cjs"); +const { ERR_CONFIG, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); + +/** Staging directory where the model places files to be uploaded. */ +const STAGING_DIR = "/tmp/gh-aw/safeoutputs/upload-artifacts/"; + +/** Base directory for per-slot artifact staging used by actions/upload-artifact. */ +const SLOT_BASE_DIR = "/tmp/gh-aw/upload-artifacts/"; + +/** Prefix for temporary artifact IDs returned to the caller. */ +const TEMP_ID_PREFIX = "tmp_artifact_"; + +/** Path where the resolver mapping (tmpId → artifact name) is written. */ +const RESOLVER_FILE = "/tmp/gh-aw/artifact-resolver.json"; + +/** + * Generate a temporary artifact ID. + * Format: tmp_artifact_<26 uppercase alphanumeric characters> + * @returns {string} + */ +function generateTemporaryArtifactId() { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let id = TEMP_ID_PREFIX; + for (let i = 0; i < 26; i++) { + id += chars[Math.floor(Math.random() * chars.length)]; + } + return id; +} + +/** + * Parse a JSON array from an environment variable, returning an empty array on failure. + * @param {string|undefined} envVar + * @returns {string[]} + */ +function parseJsonArrayEnv(envVar) { + if (!envVar) return []; + try { + const parsed = JSON.parse(envVar); + return Array.isArray(parsed) ? parsed.filter(v => typeof v === "string") : []; + } catch { + return []; + } +} + +/** + * Check whether a relative path matches any of the provided glob patterns. + * @param {string} relPath - Path relative to the staging root + * @param {string[]} patterns + * @returns {boolean} + */ +function matchesAnyPattern(relPath, patterns) { + if (patterns.length === 0) return false; + return patterns.some(pattern => { + const regex = globPatternToRegex(pattern); + return regex.test(relPath); + }); +} + +/** + * Validate that a path does not escape the staging root using traversal sequences. + * @param {string} filePath - Absolute path + * @param {string} root - Absolute root directory (must end with /) + * @returns {boolean} + */ +function isWithinRoot(filePath, root) { + const resolved = path.resolve(filePath); + const normalRoot = path.resolve(root); + return resolved.startsWith(normalRoot + path.sep) || resolved === normalRoot; +} + +/** + * Recursively list all regular files under a directory. + * @param {string} dir - Absolute directory path + * @param {string} baseDir - Root used to compute relative paths + * @returns {string[]} Relative paths from baseDir + */ +function listFilesRecursive(dir, baseDir) { + /** @type {string[]} */ + const files = []; + if (!fs.existsSync(dir)) return files; + + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...listFilesRecursive(fullPath, baseDir)); + } else if (entry.isFile()) { + // Reject symlinks – entry.isFile() returns false for symlinks unless dereferenced. + // We check explicitly to avoid following symlinks. + const stat = fs.lstatSync(fullPath); + if (!stat.isSymbolicLink()) { + files.push(path.relative(baseDir, fullPath)); + } else { + core.warning(`Skipping symlink: ${fullPath}`); + } + } + } + return files; +} + +/** + * Resolve the list of files to upload for a single request. + * Applies: staging root → allowed-paths → request include/exclude → dedup + sort. + * + * @param {Record} request - Parsed upload_artifact record + * @param {string[]} allowedPaths - Policy allowed-paths patterns + * @param {string[]} defaultInclude - Policy default include patterns + * @param {string[]} defaultExclude - Policy default exclude patterns + * @returns {{ files: string[], error: string|null }} + */ +function resolveFiles(request, allowedPaths, defaultInclude, defaultExclude) { + const hasMutuallyExclusive = ("path" in request ? 1 : 0) + ("filters" in request ? 1 : 0); + if (hasMutuallyExclusive !== 1) { + return { files: [], error: "exactly one of 'path' or 'filters' must be present" }; + } + + /** @type {string[]} candidateRelPaths */ + let candidateRelPaths; + + if ("path" in request) { + const reqPath = String(request.path); + // Reject absolute paths + if (path.isAbsolute(reqPath)) { + return { files: [], error: `path must be relative (staging-dir-relative), got absolute path: ${reqPath}` }; + } + // Reject traversal + const resolved = path.resolve(STAGING_DIR, reqPath); + if (!isWithinRoot(resolved, STAGING_DIR)) { + return { files: [], error: `path must not traverse outside staging directory: ${reqPath}` }; + } + if (!fs.existsSync(resolved)) { + return { files: [], error: `path does not exist in staging directory: ${reqPath}` }; + } + const stat = fs.lstatSync(resolved); + if (stat.isSymbolicLink()) { + return { files: [], error: `symlinks are not allowed: ${reqPath}` }; + } + if (stat.isDirectory()) { + candidateRelPaths = listFilesRecursive(resolved, STAGING_DIR); + } else { + candidateRelPaths = [reqPath]; + } + } else { + // Filter-based selection: start from all files in the staging directory. + const allFiles = listFilesRecursive(STAGING_DIR, STAGING_DIR); + const requestFilters = request.filters || {}; + const include = /** @type {string[]} */ requestFilters.include || defaultInclude; + const exclude = /** @type {string[]} */ requestFilters.exclude || defaultExclude; + + candidateRelPaths = allFiles.filter(f => { + if (include.length > 0 && !matchesAnyPattern(f, include)) return false; + if (exclude.length > 0 && matchesAnyPattern(f, exclude)) return false; + return true; + }); + } + + // Apply allowed-paths policy filter. + if (allowedPaths.length > 0) { + candidateRelPaths = candidateRelPaths.filter(f => matchesAnyPattern(f, allowedPaths)); + } + + // Deduplicate and sort deterministically. + const unique = Array.from(new Set(candidateRelPaths)).sort(); + return { files: unique, error: null }; +} + +/** + * Validate skip_archive constraints: + * - skip_archive may only be used for a single file. + * - directories are rejected (already expanded to file list). + * + * @param {boolean} skipArchive + * @param {string[]} files + * @returns {string|null} Error message or null + */ +function validateSkipArchive(skipArchive, files) { + if (!skipArchive) return null; + if (files.length !== 1) { + return `skip_archive=true requires exactly one selected file, but ${files.length} files matched`; + } + return null; +} + +/** + * Compute total size of the given file list (relative paths from STAGING_DIR). + * @param {string[]} files + * @returns {number} Total size in bytes + */ +function computeTotalSize(files) { + let total = 0; + for (const f of files) { + const abs = path.join(STAGING_DIR, f); + try { + total += fs.statSync(abs).size; + } catch { + // Ignore missing files (already validated upstream). + } + } + return total; +} + +/** + * Derive a sanitised artifact name from a path or a slot index. + * @param {Record} request + * @param {number} slotIndex + * @returns {string} + */ +function deriveArtifactName(request, slotIndex) { + if (typeof request.name === "string" && request.name.trim()) { + return request.name.trim().replace(/[^a-zA-Z0-9._\-]/g, "-"); + } + if ("path" in request && typeof request.path === "string") { + const base = path.basename(String(request.path)).replace(/[^a-zA-Z0-9._\-]/g, "-"); + if (base) return base; + } + return `artifact-slot-${slotIndex}`; +} + +/** + * Clamp a retention value between 1 and the policy maximum. + * @param {number|undefined} requested + * @param {number} defaultDays + * @param {number} maxDays + * @returns {number} + */ +function clampRetention(requested, defaultDays, maxDays) { + if (typeof requested !== "number" || requested < 1) return defaultDays; + return Math.min(requested, maxDays); +} + +/** + * Copy resolved files from STAGING_DIR into the per-slot directory. + * @param {string[]} files - Relative paths from STAGING_DIR + * @param {string} slotDir - Absolute target slot directory + */ +function stageFilesToSlot(files, slotDir) { + fs.mkdirSync(slotDir, { recursive: true }); + for (const relPath of files) { + const src = path.join(STAGING_DIR, relPath); + const dest = path.join(slotDir, relPath); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.copyFileSync(src, dest); + } +} + +async function main() { + const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + + // Load policy configuration from environment variables. + const maxUploads = parseInt(process.env.GH_AW_ARTIFACT_MAX_UPLOADS || "1", 10) || 1; + const defaultRetentionDays = parseInt(process.env.GH_AW_ARTIFACT_DEFAULT_RETENTION_DAYS || "7", 10) || 7; + const maxRetentionDays = parseInt(process.env.GH_AW_ARTIFACT_MAX_RETENTION_DAYS || "30", 10) || 30; + const maxSizeBytes = parseInt(process.env.GH_AW_ARTIFACT_MAX_SIZE_BYTES || "104857600", 10) || 104857600; + const allowSkipArchive = process.env.GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE === "true"; + const defaultSkipArchive = process.env.GH_AW_ARTIFACT_DEFAULT_SKIP_ARCHIVE === "true"; + const defaultIfNoFiles = process.env.GH_AW_ARTIFACT_DEFAULT_IF_NO_FILES || "error"; + const allowedPaths = parseJsonArrayEnv(process.env.GH_AW_ARTIFACT_ALLOWED_PATHS); + const filtersInclude = parseJsonArrayEnv(process.env.GH_AW_ARTIFACT_FILTERS_INCLUDE); + const filtersExclude = parseJsonArrayEnv(process.env.GH_AW_ARTIFACT_FILTERS_EXCLUDE); + + core.info(`upload_artifact handler: max_uploads=${maxUploads}, default_retention=${defaultRetentionDays}, max_retention=${maxRetentionDays}`); + core.info(`Allowed paths: ${allowedPaths.length > 0 ? allowedPaths.join(", ") : "(none – all staging files allowed)"}`); + + // Load agent output to find upload_artifact records. + const result = loadAgentOutput(); + if (!result.success) { + core.info("No agent output found, skipping upload_artifact processing"); + core.setOutput("artifact_count", "0"); + return; + } + + const uploadRequests = result.items.filter(/** @param {any} item */ item => item.type === "upload_artifact"); + + if (uploadRequests.length === 0) { + core.info("No upload_artifact records in agent output"); + core.setOutput("artifact_count", "0"); + return; + } + + core.info(`Found ${uploadRequests.length} upload_artifact request(s)`); + + // Enforce max-uploads policy. + if (uploadRequests.length > maxUploads) { + core.setFailed(`${ERR_VALIDATION}: upload_artifact: ${uploadRequests.length} requests exceed max-uploads policy (${maxUploads}). ` + `Reduce the number of upload_artifact calls or raise max-uploads in workflow configuration.`); + return; + } + + if (!fs.existsSync(STAGING_DIR)) { + core.warning(`Staging directory ${STAGING_DIR} does not exist. Did the model copy files there before calling upload_artifact?`); + fs.mkdirSync(STAGING_DIR, { recursive: true }); + } + + /** @type {Record} resolver: tmpId → artifact name */ + const resolver = {}; + + let successfulUploads = 0; + + for (let i = 0; i < uploadRequests.length; i++) { + const request = uploadRequests[i]; + core.info(`Processing upload_artifact request ${i + 1}/${uploadRequests.length}`); + + // Resolve skip_archive. + const skipArchive = typeof request.skip_archive === "boolean" ? request.skip_archive : defaultSkipArchive; + if (skipArchive && !allowSkipArchive) { + core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: skip_archive=true is not permitted. ` + `Enable it with allow.skip-archive: true in workflow configuration.`); + return; + } + + // Resolve files. + const { files, error: resolveError } = resolveFiles(request, allowedPaths, filtersInclude, filtersExclude); + if (resolveError) { + core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: ${resolveError}`); + return; + } + + if (files.length === 0) { + if (defaultIfNoFiles === "ignore") { + core.warning(`upload_artifact request ${i + 1}: no files matched, skipping (if-no-files=ignore)`); + continue; + } else { + core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: no files matched the selection criteria. ` + `Check allowed-paths, filters, or use defaults.if-no-files: ignore to skip empty uploads.`); + return; + } + } + + // Validate skip_archive file-count constraint. + const skipArchiveError = validateSkipArchive(skipArchive, files); + if (skipArchiveError) { + core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: ${skipArchiveError}`); + return; + } + + // Validate total size. + const totalSize = computeTotalSize(files); + if (totalSize > maxSizeBytes) { + core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: total file size ${totalSize} bytes exceeds ` + `max-size-bytes limit of ${maxSizeBytes} bytes.`); + return; + } + + // Compute retention days. + const retentionDays = clampRetention(typeof request.retention_days === "number" ? request.retention_days : undefined, defaultRetentionDays, maxRetentionDays); + + // Derive artifact name and generate temporary ID. + const artifactName = deriveArtifactName(request, i); + const tmpId = generateTemporaryArtifactId(); + resolver[tmpId] = artifactName; + + core.info(`Slot ${i}: artifact="${artifactName}", files=${files.length}, size=${totalSize}B, ` + `retention=${retentionDays}d, skip_archive=${skipArchive}, tmp_id=${tmpId}`); + + if (!isStaged) { + // Stage files into the per-slot directory for the actions/upload-artifact step. + const slotDir = path.join(SLOT_BASE_DIR, `slot_${i}`); + stageFilesToSlot(files, slotDir); + core.info(`Staged ${files.length} file(s) to ${slotDir}`); + } else { + core.info(`Staged mode: skipping file staging for slot ${i}`); + } + + // Set step outputs for the conditional actions/upload-artifact steps in the job YAML. + core.setOutput(`slot_${i}_enabled`, "true"); + core.setOutput(`slot_${i}_name`, artifactName); + core.setOutput(`slot_${i}_retention_days`, String(retentionDays)); + core.setOutput(`slot_${i}_tmp_id`, tmpId); + core.setOutput(`slot_${i}_file_count`, String(files.length)); + core.setOutput(`slot_${i}_size_bytes`, String(totalSize)); + + successfulUploads++; + } + + // Write resolver mapping so downstream steps can resolve tmp IDs to artifact names. + try { + fs.mkdirSync(path.dirname(RESOLVER_FILE), { recursive: true }); + fs.writeFileSync(RESOLVER_FILE, JSON.stringify(resolver, null, 2)); + core.info(`Wrote artifact resolver mapping to ${RESOLVER_FILE}`); + } catch (err) { + core.warning(`Failed to write artifact resolver file: ${getErrorMessage(err)}`); + } + + core.setOutput("artifact_count", String(successfulUploads)); + core.info(`upload_artifact handler complete: ${successfulUploads} artifact(s) staged`); + + if (isStaged) { + core.summary.addHeading("🎭 Staged Mode: Artifact Upload Preview", 2); + core.summary.addRaw(`Would upload **${successfulUploads}** artifact(s). Files staged at ${STAGING_DIR}.`); + await core.summary.write(); + } +} + +module.exports = { main }; diff --git a/actions/setup/js/upload_artifact.test.cjs b/actions/setup/js/upload_artifact.test.cjs new file mode 100644 index 00000000000..ef59a262a9a --- /dev/null +++ b/actions/setup/js/upload_artifact.test.cjs @@ -0,0 +1,374 @@ +// @ts-check +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import fs from "fs"; +import path from "path"; +import os from "os"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const STAGING_DIR = "/tmp/gh-aw/safeoutputs/upload-artifacts/"; +const SLOT_BASE_DIR = "/tmp/gh-aw/upload-artifacts/"; +const RESOLVER_FILE = "/tmp/gh-aw/artifact-resolver.json"; + +describe("upload_artifact.cjs", () => { + let mockCore; + let agentOutputPath; + let originalEnv; + + /** + * @param {object} data + */ + function writeAgentOutput(data) { + agentOutputPath = path.join(os.tmpdir(), `test_upload_artifact_${Date.now()}_${Math.random().toString(36).slice(2)}.json`); + fs.writeFileSync(agentOutputPath, JSON.stringify(data)); + process.env.GH_AW_AGENT_OUTPUT = agentOutputPath; + } + + /** + * @param {string} relPath + * @param {string} content + */ + function writeStaging(relPath, content = "test content") { + const fullPath = path.join(STAGING_DIR, relPath); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, content); + } + + /** + * @returns {Promise} + */ + async function runMain() { + const scriptText = fs.readFileSync(path.join(__dirname, "upload_artifact.cjs"), "utf8"); + global.core = mockCore; + await eval(`(async () => { ${scriptText}; await main(); })()`); + } + + beforeEach(() => { + vi.clearAllMocks(); + + mockCore = { + info: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + setOutput: vi.fn(), + setFailed: vi.fn(), + summary: { + addHeading: vi.fn().mockReturnThis(), + addRaw: vi.fn().mockReturnThis(), + write: vi.fn().mockResolvedValue(undefined), + }, + }; + + originalEnv = { ...process.env }; + + // Set reasonable defaults + process.env.GH_AW_ARTIFACT_MAX_UPLOADS = "3"; + process.env.GH_AW_ARTIFACT_DEFAULT_RETENTION_DAYS = "7"; + process.env.GH_AW_ARTIFACT_MAX_RETENTION_DAYS = "30"; + process.env.GH_AW_ARTIFACT_MAX_SIZE_BYTES = "104857600"; + delete process.env.GH_AW_ARTIFACT_ALLOWED_PATHS; + delete process.env.GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE; + delete process.env.GH_AW_ARTIFACT_DEFAULT_SKIP_ARCHIVE; + delete process.env.GH_AW_ARTIFACT_DEFAULT_IF_NO_FILES; + delete process.env.GH_AW_ARTIFACT_FILTERS_INCLUDE; + delete process.env.GH_AW_ARTIFACT_FILTERS_EXCLUDE; + delete process.env.GH_AW_SAFE_OUTPUTS_STAGED; + + // Ensure staging dir exists and is clean + if (fs.existsSync(STAGING_DIR)) { + fs.rmSync(STAGING_DIR, { recursive: true }); + } + fs.mkdirSync(STAGING_DIR, { recursive: true }); + + // Clean slot dir + if (fs.existsSync(SLOT_BASE_DIR)) { + fs.rmSync(SLOT_BASE_DIR, { recursive: true }); + } + + // Clean resolver file + if (fs.existsSync(RESOLVER_FILE)) { + fs.unlinkSync(RESOLVER_FILE); + } + }); + + afterEach(() => { + // Restore env + process.env = originalEnv; + + if (agentOutputPath && fs.existsSync(agentOutputPath)) { + fs.unlinkSync(agentOutputPath); + } + }); + + describe("no agent output", () => { + it("sets artifact_count to 0 when no agent output is present", async () => { + delete process.env.GH_AW_AGENT_OUTPUT; + await runMain(); + expect(mockCore.setOutput).toHaveBeenCalledWith("artifact_count", "0"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + }); + + describe("no upload_artifact records", () => { + it("sets artifact_count to 0 when output has no upload_artifact items", async () => { + writeAgentOutput({ items: [{ type: "create_issue", title: "test" }] }); + await runMain(); + expect(mockCore.setOutput).toHaveBeenCalledWith("artifact_count", "0"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + }); + + describe("path-based upload", () => { + it("stages a single file and sets slot outputs", async () => { + writeStaging("report.json", '{"result": "ok"}'); + writeAgentOutput({ + items: [{ type: "upload_artifact", path: "report.json", retention_days: 14 }], + }); + + await runMain(); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); + expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_retention_days", "14"); + expect(mockCore.setOutput).toHaveBeenCalledWith("artifact_count", "1"); + + // Verify the file was staged into slot_0. + const slotFile = path.join(SLOT_BASE_DIR, "slot_0", "report.json"); + expect(fs.existsSync(slotFile)).toBe(true); + }); + + it("clamps retention days to max-retention-days", async () => { + writeStaging("report.json"); + writeAgentOutput({ + items: [{ type: "upload_artifact", path: "report.json", retention_days: 999 }], + }); + + await runMain(); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_retention_days", "30"); + }); + + it("uses default retention when retention_days is absent", async () => { + writeStaging("report.json"); + writeAgentOutput({ items: [{ type: "upload_artifact", path: "report.json" }] }); + + await runMain(); + + expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_retention_days", "7"); + }); + }); + + describe("validation errors", () => { + it("fails when both path and filters are present", async () => { + writeStaging("report.json"); + writeAgentOutput({ + items: [ + { + type: "upload_artifact", + path: "report.json", + filters: { include: ["**/*.json"] }, + }, + ], + }); + + await runMain(); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("exactly one of 'path' or 'filters'")); + }); + + it("fails when neither path nor filters are present", async () => { + writeAgentOutput({ items: [{ type: "upload_artifact", retention_days: 7 }] }); + + await runMain(); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("exactly one of 'path' or 'filters'")); + }); + + it("fails when path traverses outside staging dir", async () => { + writeAgentOutput({ items: [{ type: "upload_artifact", path: "../etc/passwd" }] }); + + await runMain(); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("must not traverse outside staging directory")); + }); + + it("fails when absolute path is provided", async () => { + writeAgentOutput({ items: [{ type: "upload_artifact", path: "/etc/passwd" }] }); + + await runMain(); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("must be relative")); + }); + + it("fails when path does not exist in staging dir", async () => { + writeAgentOutput({ items: [{ type: "upload_artifact", path: "nonexistent.json" }] }); + + await runMain(); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("does not exist in staging directory")); + }); + + it("fails when max-uploads is exceeded", async () => { + process.env.GH_AW_ARTIFACT_MAX_UPLOADS = "1"; + writeStaging("a.json"); + writeStaging("b.json"); + writeAgentOutput({ + items: [ + { type: "upload_artifact", path: "a.json" }, + { type: "upload_artifact", path: "b.json" }, + ], + }); + + await runMain(); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("exceed max-uploads policy")); + }); + + it("fails when skip_archive is requested but not allowed", async () => { + writeStaging("app.bin"); + writeAgentOutput({ items: [{ type: "upload_artifact", path: "app.bin", skip_archive: true }] }); + + await runMain(); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("skip_archive=true is not permitted")); + }); + + it("fails when skip_archive=true with multiple files", async () => { + process.env.GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE = "true"; + writeStaging("output/a.json"); + writeStaging("output/b.json"); + writeAgentOutput({ + items: [ + { + type: "upload_artifact", + // Use "output/**" which matches output/a.json and output/b.json + filters: { include: ["output/**"] }, + skip_archive: true, + }, + ], + }); + + await runMain(); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("skip_archive=true requires exactly one selected file")); + }); + }); + + describe("skip_archive allowed", () => { + it("succeeds with skip_archive=true and a single file", async () => { + process.env.GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE = "true"; + writeStaging("app.bin", "binary data"); + writeAgentOutput({ items: [{ type: "upload_artifact", path: "app.bin", skip_archive: true }] }); + + await runMain(); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); + }); + }); + + describe("filter-based upload", () => { + it("selects files matching include pattern", async () => { + writeStaging("reports/daily/summary.json", "{}"); + writeStaging("reports/weekly/summary.json", "{}"); + writeStaging("reports/private/secret.json", "{}"); + writeAgentOutput({ + items: [ + { + type: "upload_artifact", + filters: { + include: ["reports/**/*.json"], + exclude: ["reports/private/**"], + }, + }, + ], + }); + + await runMain(); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); + expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_file_count", "2"); + }); + + it("handles no-files with if-no-files=ignore", async () => { + process.env.GH_AW_ARTIFACT_DEFAULT_IF_NO_FILES = "ignore"; + writeAgentOutput({ + items: [ + { + type: "upload_artifact", + filters: { include: ["nonexistent/**"] }, + }, + ], + }); + + await runMain(); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith("artifact_count", "0"); + }); + + it("fails when no files match and if-no-files=error (default)", async () => { + writeAgentOutput({ + items: [ + { + type: "upload_artifact", + filters: { include: ["nonexistent/**"] }, + }, + ], + }); + + await runMain(); + + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("no files matched")); + }); + }); + + describe("allowed-paths policy", () => { + it("filters out files not in allowed-paths", async () => { + process.env.GH_AW_ARTIFACT_ALLOWED_PATHS = JSON.stringify(["dist/**"]); + writeStaging("dist/app.js"); + writeStaging("secret.env"); + writeAgentOutput({ + items: [ + { + type: "upload_artifact", + filters: { include: ["**"] }, + }, + ], + }); + + await runMain(); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_file_count", "1"); + }); + }); + + describe("staged mode", () => { + it("skips file staging but sets outputs in staged mode", async () => { + process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"; + writeStaging("report.json"); + writeAgentOutput({ items: [{ type: "upload_artifact", path: "report.json" }] }); + + await runMain(); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); + + // In staged mode, files are NOT copied to the slot directory. + const slotFile = path.join(SLOT_BASE_DIR, "slot_0", "report.json"); + expect(fs.existsSync(slotFile)).toBe(false); + }); + }); + + describe("resolver file", () => { + it("writes a resolver mapping with temporary IDs", async () => { + writeStaging("report.json"); + writeAgentOutput({ items: [{ type: "upload_artifact", path: "report.json" }] }); + + await runMain(); + + expect(fs.existsSync(RESOLVER_FILE)).toBe(true); + const resolver = JSON.parse(fs.readFileSync(RESOLVER_FILE, "utf8")); + const keys = Object.keys(resolver); + expect(keys.length).toBe(1); + expect(keys[0]).toMatch(/^tmp_artifact_[A-Z0-9]{26}$/); + }); + }); +}); diff --git a/pkg/workflow/compiler_safe_output_jobs.go b/pkg/workflow/compiler_safe_output_jobs.go index f1d6d83c436..dabfa512dfa 100644 --- a/pkg/workflow/compiler_safe_output_jobs.go +++ b/pkg/workflow/compiler_safe_output_jobs.go @@ -89,6 +89,24 @@ func (c *Compiler) buildSafeOutputsJobs(data *WorkflowData, jobName, markdownPat compilerSafeOutputJobsLog.Printf("Added separate upload_assets job") } + // Build upload_artifact job as a separate job if configured. + // This is separate from the consolidated safe_outputs job because it needs to: + // 1. Download the staging artifact produced by the main job + // 2. Validate and filter the requested files + // 3. Upload each approved set of files as a proper GitHub Actions artifact + if data.SafeOutputs != nil && data.SafeOutputs.UploadArtifact != nil { + compilerSafeOutputJobsLog.Print("Building separate upload_artifact job") + uploadArtifactJob, err := c.buildUploadArtifactJob(data, jobName, threatDetectionEnabled) + if err != nil { + return fmt.Errorf("failed to build upload_artifact job: %w", err) + } + if err := c.jobManager.AddJob(uploadArtifactJob); err != nil { + return fmt.Errorf("failed to add upload_artifact job: %w", err) + } + safeOutputJobNames = append(safeOutputJobNames, uploadArtifactJob.Name) + compilerSafeOutputJobsLog.Printf("Added separate upload_artifact job") + } + // Build upload_code_scanning_sarif job as a separate job if create-code-scanning-alert is configured. // This job runs after safe_outputs and only when the safe_outputs job exported a SARIF file. // It is separate to avoid the checkout step (needed to restore HEAD to github.sha) from diff --git a/pkg/workflow/compiler_safe_outputs_config.go b/pkg/workflow/compiler_safe_outputs_config.go index 4eb757e46ec..4bc1638fb2e 100644 --- a/pkg/workflow/compiler_safe_outputs_config.go +++ b/pkg/workflow/compiler_safe_outputs_config.go @@ -768,6 +768,37 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfTrue("staged", c.Staged). Build() }, + "upload_artifact": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.UploadArtifact == nil { + return nil + } + c := cfg.UploadArtifact + b := newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfPositive("max-uploads", c.MaxUploads). + AddIfPositive("default-retention-days", c.DefaultRetentionDays). + AddIfPositive("max-retention-days", c.MaxRetentionDays). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged) + if c.MaxSizeBytes > 0 { + b = b.AddDefault("max-size-bytes", c.MaxSizeBytes) + } + if len(c.AllowedPaths) > 0 { + b = b.AddStringSlice("allowed-paths", c.AllowedPaths) + } + if c.Allow != nil && c.Allow.SkipArchive { + b = b.AddIfTrue("allow-skip-archive", true) + } + if c.Defaults != nil { + if c.Defaults.SkipArchive { + b = b.AddIfTrue("default-skip-archive", true) + } + if c.Defaults.IfNoFiles != "" { + b = b.AddIfNotEmpty("default-if-no-files", c.Defaults.IfNoFiles) + } + } + return b.Build() + }, "autofix_code_scanning_alert": func(cfg *SafeOutputsConfig) map[string]any { if cfg.AutofixCodeScanningAlert == nil { return nil diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index 91fa861f4ab..5c430b4f948 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -472,6 +472,7 @@ type SafeOutputsConfig struct { UpdatePullRequests *UpdatePullRequestsConfig `yaml:"update-pull-request,omitempty"` // Update GitHub pull request title/body PushToPullRequestBranch *PushToPullRequestBranchConfig `yaml:"push-to-pull-request-branch,omitempty"` UploadAssets *UploadAssetsConfig `yaml:"upload-asset,omitempty"` + UploadArtifact *UploadArtifactConfig `yaml:"upload-artifact,omitempty"` // Upload files as run-scoped GitHub Actions artifacts UpdateRelease *UpdateReleaseConfig `yaml:"update-release,omitempty"` // Update GitHub release descriptions CreateAgentSessions *CreateAgentSessionConfig `yaml:"create-agent-session,omitempty"` // Create GitHub Copilot coding agent sessions UpdateProjects *UpdateProjectConfig `yaml:"update-project,omitempty"` // Smart project board management (create/add/update) diff --git a/pkg/workflow/compiler_yaml_main_job.go b/pkg/workflow/compiler_yaml_main_job.go index 37b7bb5d974..e9ca525be53 100644 --- a/pkg/workflow/compiler_yaml_main_job.go +++ b/pkg/workflow/compiler_yaml_main_job.go @@ -513,6 +513,11 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat // This creates a separate artifact for assets that will be downloaded by upload_assets job generateSafeOutputsAssetsArtifactUpload(yaml, data) + // Add safe-outputs upload-artifact staging upload (after agent execution) + // This creates a separate artifact for files the model staged for artifact upload, + // to be downloaded and processed by the upload_artifact job + generateSafeOutputsArtifactStagingUpload(yaml, data) + // Collect git patch path if safe-outputs with PR operations is configured // NOTE: Git patch generation has been moved to the safe-outputs MCP server // The patch is now generated when create_pull_request or push_to_pull_request_branch diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index 9614a6f42dd..ac0196a526d 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -1583,5 +1583,57 @@ }, "additionalProperties": false } + }, + { + "name": "upload_artifact", + "description": "Upload files as a run-scoped GitHub Actions artifact. The model must first copy files to /tmp/gh-aw/safeoutputs/upload-artifacts/ then request upload using this tool. Returns a temporary artifact ID that can be resolved to a download URL by an authorised step. Exactly one of path or filters must be present.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Path to the file or directory to upload, relative to /tmp/gh-aw/safeoutputs/upload-artifacts/ (e.g., \"report.json\" or \"dist/\"). Required unless filters is provided." + }, + "filters": { + "type": "object", + "description": "Glob-based file selection filters. Required unless path is provided.", + "properties": { + "include": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Glob patterns for files to include (e.g., [\"reports/**/*.json\"])" + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Glob patterns for files to exclude (e.g., [\"**/*.env\", \"**/*.pem\"])" + } + }, + "additionalProperties": false + }, + "retention_days": { + "type": "integer", + "minimum": 1, + "description": "Number of days to retain the artifact. Capped by workflow configuration." + }, + "skip_archive": { + "type": "boolean", + "description": "Upload the file directly without archiving. Only allowed for single-file uploads when enabled in workflow configuration." + }, + "secrecy": { + "type": "string", + "description": "Confidentiality level of the artifact content (e.g., \"public\", \"internal\", \"private\")." + }, + "integrity": { + "type": "string", + "description": "Trustworthiness level of the artifact source (e.g., \"low\", \"medium\", \"high\")." + } + }, + "additionalProperties": false + } } ] diff --git a/pkg/workflow/publish_artifacts.go b/pkg/workflow/publish_artifacts.go new file mode 100644 index 00000000000..aaacea24412 --- /dev/null +++ b/pkg/workflow/publish_artifacts.go @@ -0,0 +1,361 @@ +package workflow + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/logger" +) + +var publishArtifactsLog = logger.New("workflow:publish_artifacts") + +// defaultArtifactMaxUploads is the default maximum number of upload_artifact tool calls allowed per run. +const defaultArtifactMaxUploads = 1 + +// defaultArtifactRetentionDays is the default artifact retention period in days. +const defaultArtifactRetentionDays = 7 + +// defaultArtifactMaxRetentionDays is the default maximum retention cap in days. +const defaultArtifactMaxRetentionDays = 30 + +// defaultArtifactMaxSizeBytes is the default maximum total upload size (100 MB). +const defaultArtifactMaxSizeBytes int64 = 104857600 + +// artifactStagingDir is the path where the model stages files to be uploaded as artifacts. +const artifactStagingDir = "/tmp/gh-aw/safeoutputs/upload-artifacts/" + +// artifactSlotDir is the per-slot directory used by the handler to organise staged files. +const artifactSlotDir = "/tmp/gh-aw/upload-artifacts/" + +// SafeOutputsUploadArtifactStagingArtifactName is the artifact that carries the staging directory +// from the main agent job to the upload_artifact job. +const SafeOutputsUploadArtifactStagingArtifactName = "safe-outputs-upload-artifacts" + +// ArtifactFiltersConfig holds include/exclude glob patterns for artifact file selection. +type ArtifactFiltersConfig struct { + Include []string `yaml:"include,omitempty"` // Glob patterns for files to include + Exclude []string `yaml:"exclude,omitempty"` // Glob patterns for files to exclude +} + +// ArtifactDefaultsConfig holds default request settings applied when the model does not +// specify a value explicitly. +type ArtifactDefaultsConfig struct { + SkipArchive bool `yaml:"skip-archive,omitempty"` // Default value for skip_archive + IfNoFiles string `yaml:"if-no-files,omitempty"` // Behaviour when no files match: "error" or "ignore" +} + +// ArtifactAllowConfig holds policy settings for optional behaviours that must be explicitly +// opted-in to by the workflow author. +type ArtifactAllowConfig struct { + SkipArchive bool `yaml:"skip-archive,omitempty"` // Allow skip_archive: true in model requests +} + +// UploadArtifactConfig holds configuration for the upload-artifact safe output type. +type UploadArtifactConfig struct { + BaseSafeOutputConfig `yaml:",inline"` + MaxUploads int `yaml:"max-uploads,omitempty"` // Max upload_artifact tool calls allowed (default: 1) + DefaultRetentionDays int `yaml:"default-retention-days,omitempty"` // Default retention period (default: 7 days) + MaxRetentionDays int `yaml:"max-retention-days,omitempty"` // Maximum retention cap (default: 30 days) + MaxSizeBytes int64 `yaml:"max-size-bytes,omitempty"` // Max total bytes per upload (default: 100 MB) + AllowedPaths []string `yaml:"allowed-paths,omitempty"` // Glob patterns restricting which paths the model may upload + Filters *ArtifactFiltersConfig `yaml:"filters,omitempty"` // Default include/exclude filters applied on top of allowed-paths + Defaults *ArtifactDefaultsConfig `yaml:"defaults,omitempty"` // Default values injected when the model omits a field + Allow *ArtifactAllowConfig `yaml:"allow,omitempty"` // Opt-in behaviours +} + +// parseUploadArtifactConfig parses the upload-artifact key from the safe-outputs map. +func (c *Compiler) parseUploadArtifactConfig(outputMap map[string]any) *UploadArtifactConfig { + configData, exists := outputMap["upload-artifact"] + if !exists { + return nil + } + + // Explicit false disables upload-artifact (e.g. when passed via import-inputs). + if b, ok := configData.(bool); ok && !b { + publishArtifactsLog.Print("upload-artifact explicitly set to false, skipping") + return nil + } + + publishArtifactsLog.Print("Parsing upload-artifact configuration") + config := &UploadArtifactConfig{ + MaxUploads: defaultArtifactMaxUploads, + DefaultRetentionDays: defaultArtifactRetentionDays, + MaxRetentionDays: defaultArtifactMaxRetentionDays, + MaxSizeBytes: defaultArtifactMaxSizeBytes, + } + + configMap, ok := configData.(map[string]any) + if !ok { + // No config map (e.g. upload-artifact: true) – use defaults. + publishArtifactsLog.Print("Using default upload-artifact configuration") + return config + } + + // Parse max-uploads. + if maxUploads, exists := configMap["max-uploads"]; exists { + if v, ok := parseIntValue(maxUploads); ok && v > 0 { + config.MaxUploads = v + } + } + + // Parse default-retention-days. + if retDays, exists := configMap["default-retention-days"]; exists { + if v, ok := parseIntValue(retDays); ok && v > 0 { + config.DefaultRetentionDays = v + } + } + + // Parse max-retention-days. + if maxRetDays, exists := configMap["max-retention-days"]; exists { + if v, ok := parseIntValue(maxRetDays); ok && v > 0 { + config.MaxRetentionDays = v + } + } + + // Parse max-size-bytes. + if maxBytes, exists := configMap["max-size-bytes"]; exists { + if v, ok := parseIntValue(maxBytes); ok && v > 0 { + config.MaxSizeBytes = int64(v) + } + } + + // Parse allowed-paths. + if allowedPaths, exists := configMap["allowed-paths"]; exists { + if arr, ok := allowedPaths.([]any); ok { + for _, p := range arr { + if s, ok := p.(string); ok && s != "" { + config.AllowedPaths = append(config.AllowedPaths, s) + } + } + } + } + + // Parse filters. + if filtersData, exists := configMap["filters"]; exists { + if filtersMap, ok := filtersData.(map[string]any); ok { + filters := &ArtifactFiltersConfig{} + if inc, ok := filtersMap["include"].([]any); ok { + for _, v := range inc { + if s, ok := v.(string); ok { + filters.Include = append(filters.Include, s) + } + } + } + if exc, ok := filtersMap["exclude"].([]any); ok { + for _, v := range exc { + if s, ok := v.(string); ok { + filters.Exclude = append(filters.Exclude, s) + } + } + } + if len(filters.Include) > 0 || len(filters.Exclude) > 0 { + config.Filters = filters + } + } + } + + // Parse defaults. + if defaultsData, exists := configMap["defaults"]; exists { + if defaultsMap, ok := defaultsData.(map[string]any); ok { + defaults := &ArtifactDefaultsConfig{} + if skipArchive, ok := defaultsMap["skip-archive"].(bool); ok { + defaults.SkipArchive = skipArchive + } + if ifNoFiles, ok := defaultsMap["if-no-files"].(string); ok && ifNoFiles != "" { + defaults.IfNoFiles = ifNoFiles + } + config.Defaults = defaults + } + } + + // Parse allow. + if allowData, exists := configMap["allow"]; exists { + if allowMap, ok := allowData.(map[string]any); ok { + allow := &ArtifactAllowConfig{} + if skipArchive, ok := allowMap["skip-archive"].(bool); ok { + allow.SkipArchive = skipArchive + } + config.Allow = allow + } + } + + // Parse common base fields (max, github-token, staged). + c.parseBaseSafeOutputConfig(configMap, &config.BaseSafeOutputConfig, 0) + + publishArtifactsLog.Printf("Parsed upload-artifact config: max_uploads=%d, default_retention=%d, max_retention=%d, max_size_bytes=%d", + config.MaxUploads, config.DefaultRetentionDays, config.MaxRetentionDays, config.MaxSizeBytes) + return config +} + +// buildUploadArtifactJob creates the upload_artifact standalone job. +// +// Architecture: +// 1. The model stages files to artifactStagingDir during its run. +// 2. The main agent job uploads that directory as a GitHub Actions staging artifact. +// 3. This job downloads the staging artifact, validates each upload_artifact request, +// copies approved files into per-slot directories, and then uploads each slot using +// actions/upload-artifact with a conditional step per MaxUploads slot. +// 4. A temporary artifact ID is returned for each slot via job outputs. +func (c *Compiler) buildUploadArtifactJob(data *WorkflowData, mainJobName string, threatDetectionEnabled bool) (*Job, error) { + publishArtifactsLog.Printf("Building upload_artifact job: workflow=%s, main_job=%s, threat_detection=%v", + data.Name, mainJobName, threatDetectionEnabled) + + if data.SafeOutputs == nil || data.SafeOutputs.UploadArtifact == nil { + return nil, errors.New("safe-outputs.upload-artifact configuration is required") + } + + cfg := data.SafeOutputs.UploadArtifact + + var preSteps []string + + // Add setup step so scripts are available at SetupActionDestination. + setupActionRef := c.resolveActionReference("./actions/setup", data) + if setupActionRef != "" || c.actionMode.IsScript() { + preSteps = append(preSteps, c.generateCheckoutActionsFolder(data)...) + publishTraceID := fmt.Sprintf("${{ needs.%s.outputs.setup-trace-id }}", constants.ActivationJobName) + preSteps = append(preSteps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, publishTraceID)...) + } + + // Download agent output artifact (to read upload_artifact requests). + artifactPrefix := artifactPrefixExprForAgentDownstreamJob(data) + preSteps = append(preSteps, + buildAgentOutputDownloadSteps(artifactPrefix)..., + ) + + // Download the staging artifact that holds the files the model wants to upload. + stagingArtifactName := artifactPrefix + SafeOutputsUploadArtifactStagingArtifactName + preSteps = append(preSteps, + " - name: Download upload-artifact staging\n", + " continue-on-error: true\n", + fmt.Sprintf(" uses: %s\n", GetActionPin("actions/download-artifact")), + " with:\n", + fmt.Sprintf(" name: %s\n", stagingArtifactName), + fmt.Sprintf(" path: %s\n", artifactStagingDir), + ) + + // Build custom environment variables consumed by upload_artifact.cjs. + var customEnvVars []string + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_MAX_UPLOADS: %d\n", cfg.MaxUploads)) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_DEFAULT_RETENTION_DAYS: %d\n", cfg.DefaultRetentionDays)) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_MAX_RETENTION_DAYS: %d\n", cfg.MaxRetentionDays)) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_MAX_SIZE_BYTES: %d\n", cfg.MaxSizeBytes)) + + if len(cfg.AllowedPaths) > 0 { + allowedPathsJSON := marshalStringSliceJSON(cfg.AllowedPaths) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_ALLOWED_PATHS: %q\n", allowedPathsJSON)) + } + + if cfg.Allow != nil && cfg.Allow.SkipArchive { + customEnvVars = append(customEnvVars, " GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE: \"true\"\n") + } + if cfg.Defaults != nil { + if cfg.Defaults.SkipArchive { + customEnvVars = append(customEnvVars, " GH_AW_ARTIFACT_DEFAULT_SKIP_ARCHIVE: \"true\"\n") + } + if cfg.Defaults.IfNoFiles != "" { + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_DEFAULT_IF_NO_FILES: %q\n", cfg.Defaults.IfNoFiles)) + } + } + if cfg.Filters != nil { + if len(cfg.Filters.Include) > 0 { + filtersIncJSON := marshalStringSliceJSON(cfg.Filters.Include) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_FILTERS_INCLUDE: %q\n", filtersIncJSON)) + } + if len(cfg.Filters.Exclude) > 0 { + filtersExcJSON := marshalStringSliceJSON(cfg.Filters.Exclude) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_FILTERS_EXCLUDE: %q\n", filtersExcJSON)) + } + } + + // Add standard env vars (run ID, repo, etc.). + customEnvVars = append(customEnvVars, c.buildStandardSafeOutputEnvVars(data, "")...) + + // Build conditional actions/upload-artifact steps – one per MaxUploads slot. + // The handler sets slot_N_enabled=true and outputs the slot name / retention when + // the Nth upload_artifact request was successfully validated and staged. + var postSteps []string + for i := range cfg.MaxUploads { + slotDir := fmt.Sprintf("%sslot_%d/", artifactSlotDir, i) + postSteps = append(postSteps, + fmt.Sprintf(" - name: Upload artifact slot %d\n", i), + fmt.Sprintf(" if: steps.upload_artifacts.outputs.slot_%d_enabled == 'true'\n", i), + fmt.Sprintf(" uses: %s\n", GetActionPin("actions/upload-artifact")), + " with:\n", + fmt.Sprintf(" name: ${{ steps.upload_artifacts.outputs.slot_%d_name }}\n", i), + fmt.Sprintf(" path: %s\n", slotDir), + fmt.Sprintf(" retention-days: ${{ steps.upload_artifacts.outputs.slot_%d_retention_days }}\n", i), + " if-no-files-found: ignore\n", + ) + } + + // In dev mode, restore the actions/setup folder so the post-step cleanup succeeds. + if c.actionMode.IsDev() { + postSteps = append(postSteps, c.generateRestoreActionsSetupStep()) + publishArtifactsLog.Print("Added restore actions folder step to upload_artifact job (dev mode)") + } + + jobCondition := BuildSafeOutputType("upload_artifact") + needs := []string{mainJobName, string(constants.ActivationJobName)} + + // Collect job outputs for all slots so downstream jobs can reference them. + outputs := map[string]string{ + "artifact_count": "${{ steps.upload_artifacts.outputs.artifact_count }}", + } + for i := range cfg.MaxUploads { + outputs[fmt.Sprintf("slot_%d_tmp_id", i)] = fmt.Sprintf("${{ steps.upload_artifacts.outputs.slot_%d_tmp_id }}", i) + } + + return c.buildSafeOutputJob(data, SafeOutputJobConfig{ + JobName: "upload_artifact", + StepName: "Upload artifacts", + StepID: "upload_artifacts", + ScriptName: "upload_artifact", + MainJobName: mainJobName, + CustomEnvVars: customEnvVars, + Script: "", + Permissions: NewPermissions(), + Outputs: outputs, + Condition: jobCondition, + PreSteps: preSteps, + PostSteps: postSteps, + Token: cfg.GitHubToken, + Needs: needs, + }) +} + +// generateSafeOutputsArtifactStagingUpload generates a step in the main agent job that uploads +// the artifact staging directory so the upload_artifact job can download it. +// This step only appears when upload-artifact is configured in safe-outputs. +func generateSafeOutputsArtifactStagingUpload(builder *strings.Builder, data *WorkflowData) { + if data.SafeOutputs == nil || data.SafeOutputs.UploadArtifact == nil { + return + } + + publishArtifactsLog.Print("Generating safe-outputs artifact staging upload step") + + prefix := artifactPrefixExprForDownstreamJob(data) + + builder.WriteString(" # Upload safe-outputs upload-artifact staging for the upload_artifact job\n") + builder.WriteString(" - name: Upload Upload-Artifact Staging\n") + builder.WriteString(" if: always()\n") + fmt.Fprintf(builder, " uses: %s\n", GetActionPin("actions/upload-artifact")) + builder.WriteString(" with:\n") + fmt.Fprintf(builder, " name: %s%s\n", prefix, SafeOutputsUploadArtifactStagingArtifactName) + fmt.Fprintf(builder, " path: %s\n", artifactStagingDir) + builder.WriteString(" retention-days: 1\n") + builder.WriteString(" if-no-files-found: ignore\n") +} + +// marshalStringSliceJSON serialises a []string to a compact JSON array string. +// This is used to pass multi-value config fields as environment variables. +func marshalStringSliceJSON(values []string) string { + data, err := json.Marshal(values) + if err != nil { + // Should never happen for plain string slices. + return "[]" + } + return string(data) +} diff --git a/pkg/workflow/publish_artifacts_test.go b/pkg/workflow/publish_artifacts_test.go new file mode 100644 index 00000000000..459fc3a8649 --- /dev/null +++ b/pkg/workflow/publish_artifacts_test.go @@ -0,0 +1,314 @@ +//go:build !integration + +package workflow + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseUploadArtifactConfig(t *testing.T) { + c := &Compiler{} + + tests := []struct { + name string + input map[string]any + expected *UploadArtifactConfig + isNil bool + }{ + { + name: "no upload-artifact key", + input: map[string]any{}, + isNil: true, + }, + { + name: "upload-artifact explicitly false", + input: map[string]any{"upload-artifact": false}, + isNil: true, + }, + { + name: "upload-artifact true uses defaults", + input: map[string]any{"upload-artifact": true}, + expected: &UploadArtifactConfig{ + MaxUploads: defaultArtifactMaxUploads, + DefaultRetentionDays: defaultArtifactRetentionDays, + MaxRetentionDays: defaultArtifactMaxRetentionDays, + MaxSizeBytes: defaultArtifactMaxSizeBytes, + }, + }, + { + name: "upload-artifact with custom values", + input: map[string]any{ + "upload-artifact": map[string]any{ + "max-uploads": 3, + "default-retention-days": 14, + "max-retention-days": 60, + "max-size-bytes": 52428800, + "allowed-paths": []any{"dist/**", "reports/**"}, + "github-token": "${{ secrets.MY_TOKEN }}", + }, + }, + expected: &UploadArtifactConfig{ + MaxUploads: 3, + DefaultRetentionDays: 14, + MaxRetentionDays: 60, + MaxSizeBytes: 52428800, + AllowedPaths: []string{"dist/**", "reports/**"}, + BaseSafeOutputConfig: BaseSafeOutputConfig{GitHubToken: "${{ secrets.MY_TOKEN }}"}, + }, + }, + { + name: "upload-artifact with filters", + input: map[string]any{ + "upload-artifact": map[string]any{ + "filters": map[string]any{ + "include": []any{"reports/**/*.json"}, + "exclude": []any{"**/*.env", "**/*.pem"}, + }, + }, + }, + expected: &UploadArtifactConfig{ + MaxUploads: defaultArtifactMaxUploads, + DefaultRetentionDays: defaultArtifactRetentionDays, + MaxRetentionDays: defaultArtifactMaxRetentionDays, + MaxSizeBytes: defaultArtifactMaxSizeBytes, + Filters: &ArtifactFiltersConfig{ + Include: []string{"reports/**/*.json"}, + Exclude: []string{"**/*.env", "**/*.pem"}, + }, + }, + }, + { + name: "upload-artifact with defaults and allow", + input: map[string]any{ + "upload-artifact": map[string]any{ + "defaults": map[string]any{ + "skip-archive": false, + "if-no-files": "ignore", + }, + "allow": map[string]any{ + "skip-archive": true, + }, + }, + }, + expected: &UploadArtifactConfig{ + MaxUploads: defaultArtifactMaxUploads, + DefaultRetentionDays: defaultArtifactRetentionDays, + MaxRetentionDays: defaultArtifactMaxRetentionDays, + MaxSizeBytes: defaultArtifactMaxSizeBytes, + Defaults: &ArtifactDefaultsConfig{ + SkipArchive: false, + IfNoFiles: "ignore", + }, + Allow: &ArtifactAllowConfig{ + SkipArchive: true, + }, + }, + }, + { + name: "upload-artifact with max field", + input: map[string]any{ + "upload-artifact": map[string]any{ + "max": 5, + }, + }, + expected: &UploadArtifactConfig{ + MaxUploads: defaultArtifactMaxUploads, + DefaultRetentionDays: defaultArtifactRetentionDays, + MaxRetentionDays: defaultArtifactMaxRetentionDays, + MaxSizeBytes: defaultArtifactMaxSizeBytes, + BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("5")}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := c.parseUploadArtifactConfig(tt.input) + + if tt.isNil { + assert.Nil(t, result, "expected nil result") + return + } + + require.NotNil(t, result, "expected non-nil result") + assert.Equal(t, tt.expected.MaxUploads, result.MaxUploads, "MaxUploads mismatch") + assert.Equal(t, tt.expected.DefaultRetentionDays, result.DefaultRetentionDays, "DefaultRetentionDays mismatch") + assert.Equal(t, tt.expected.MaxRetentionDays, result.MaxRetentionDays, "MaxRetentionDays mismatch") + assert.Equal(t, tt.expected.MaxSizeBytes, result.MaxSizeBytes, "MaxSizeBytes mismatch") + assert.Equal(t, tt.expected.AllowedPaths, result.AllowedPaths, "AllowedPaths mismatch") + assert.Equal(t, tt.expected.GitHubToken, result.GitHubToken, "GitHubToken mismatch") + + if tt.expected.Max == nil { + assert.Nil(t, result.Max, "Max should be nil") + } else { + require.NotNil(t, result.Max, "Max should not be nil") + assert.Equal(t, *tt.expected.Max, *result.Max, "Max value mismatch") + } + + if tt.expected.Filters == nil { + assert.Nil(t, result.Filters, "Filters should be nil") + } else { + require.NotNil(t, result.Filters, "Filters should not be nil") + assert.Equal(t, tt.expected.Filters.Include, result.Filters.Include, "Filters.Include mismatch") + assert.Equal(t, tt.expected.Filters.Exclude, result.Filters.Exclude, "Filters.Exclude mismatch") + } + + if tt.expected.Defaults == nil { + assert.Nil(t, result.Defaults, "Defaults should be nil") + } else { + require.NotNil(t, result.Defaults, "Defaults should not be nil") + assert.Equal(t, tt.expected.Defaults.SkipArchive, result.Defaults.SkipArchive, "Defaults.SkipArchive mismatch") + assert.Equal(t, tt.expected.Defaults.IfNoFiles, result.Defaults.IfNoFiles, "Defaults.IfNoFiles mismatch") + } + + if tt.expected.Allow == nil { + assert.Nil(t, result.Allow, "Allow should be nil") + } else { + require.NotNil(t, result.Allow, "Allow should not be nil") + assert.Equal(t, tt.expected.Allow.SkipArchive, result.Allow.SkipArchive, "Allow.SkipArchive mismatch") + } + }) + } +} + +func TestHasSafeOutputsEnabledWithUploadArtifact(t *testing.T) { + t.Run("UploadArtifact is detected as enabled", func(t *testing.T) { + config := &SafeOutputsConfig{ + UploadArtifact: &UploadArtifactConfig{}, + } + assert.True(t, HasSafeOutputsEnabled(config), "UploadArtifact should be detected as enabled") + }) + + t.Run("nil SafeOutputsConfig returns false", func(t *testing.T) { + assert.False(t, HasSafeOutputsEnabled(nil), "nil config should return false") + }) + + t.Run("empty SafeOutputsConfig returns false", func(t *testing.T) { + assert.False(t, HasSafeOutputsEnabled(&SafeOutputsConfig{}), "empty config should return false") + }) +} + +func TestComputeEnabledToolNamesIncludesUploadArtifact(t *testing.T) { + data := &WorkflowData{ + SafeOutputs: &SafeOutputsConfig{ + UploadArtifact: &UploadArtifactConfig{}, + }, + } + tools := computeEnabledToolNames(data) + assert.True(t, tools["upload_artifact"], "upload_artifact should be in enabled tools") +} + +func TestBuildUploadArtifactJobBasicStructure(t *testing.T) { + c := NewCompiler() + data := &WorkflowData{ + Name: "Test Workflow", + SafeOutputs: &SafeOutputsConfig{ + UploadArtifact: &UploadArtifactConfig{ + MaxUploads: 2, + DefaultRetentionDays: 7, + MaxRetentionDays: 30, + MaxSizeBytes: defaultArtifactMaxSizeBytes, + AllowedPaths: []string{"dist/**", "reports/**"}, + }, + }, + } + + job, err := c.buildUploadArtifactJob(data, "agent", false) + require.NoError(t, err, "buildUploadArtifactJob should not return error") + require.NotNil(t, job, "job should not be nil") + + assert.Equal(t, "upload_artifact", job.Name, "job name should be upload_artifact") + + // Convert steps to string for inspection. + var stepsStr strings.Builder + for _, step := range job.Steps { + stepsStr.WriteString(step) + } + s := stepsStr.String() + + assert.Contains(t, s, "Download agent output artifact", "should have agent output download step") + assert.Contains(t, s, "Download upload-artifact staging", "should have staging artifact download step") + assert.Contains(t, s, "GH_AW_ARTIFACT_MAX_UPLOADS", "should have max uploads env var") + assert.Contains(t, s, "GH_AW_ARTIFACT_DEFAULT_RETENTION_DAYS", "should have default retention env var") + assert.Contains(t, s, "GH_AW_ARTIFACT_MAX_RETENTION_DAYS", "should have max retention env var") + assert.Contains(t, s, "GH_AW_ARTIFACT_MAX_SIZE_BYTES", "should have max size bytes env var") + assert.Contains(t, s, "GH_AW_ARTIFACT_ALLOWED_PATHS", "should have allowed paths env var") + + // Should have upload steps for each slot (MaxUploads = 2). + assert.Contains(t, s, "Upload artifact slot 0", "should have upload step for slot 0") + assert.Contains(t, s, "Upload artifact slot 1", "should have upload step for slot 1") + assert.NotContains(t, s, "Upload artifact slot 2", "should NOT have upload step for slot 2") +} + +func TestBuildUploadArtifactJobRequiresConfig(t *testing.T) { + c := NewCompiler() + + t.Run("nil SafeOutputs returns error", func(t *testing.T) { + data := &WorkflowData{Name: "Test", SafeOutputs: nil} + _, err := c.buildUploadArtifactJob(data, "agent", false) + assert.Error(t, err, "should return error when SafeOutputs is nil") + }) + + t.Run("nil UploadArtifact returns error", func(t *testing.T) { + data := &WorkflowData{ + Name: "Test", + SafeOutputs: &SafeOutputsConfig{UploadArtifact: nil}, + } + _, err := c.buildUploadArtifactJob(data, "agent", false) + assert.Error(t, err, "should return error when UploadArtifact is nil") + }) +} + +func TestGenerateSafeOutputsArtifactStagingUpload(t *testing.T) { + t.Run("generates step when UploadArtifact is configured", func(t *testing.T) { + var b strings.Builder + data := &WorkflowData{ + SafeOutputs: &SafeOutputsConfig{ + UploadArtifact: &UploadArtifactConfig{}, + }, + } + generateSafeOutputsArtifactStagingUpload(&b, data) + result := b.String() + assert.Contains(t, result, "safe-outputs-upload-artifacts", "should reference staging artifact name") + assert.Contains(t, result, artifactStagingDir, "should reference staging directory") + assert.Contains(t, result, "if: always()", "should have always() condition") + }) + + t.Run("generates nothing when UploadArtifact is nil", func(t *testing.T) { + var b strings.Builder + data := &WorkflowData{ + SafeOutputs: &SafeOutputsConfig{UploadArtifact: nil}, + } + generateSafeOutputsArtifactStagingUpload(&b, data) + assert.Empty(t, b.String(), "should generate nothing when UploadArtifact is nil") + }) + + t.Run("generates nothing when SafeOutputs is nil", func(t *testing.T) { + var b strings.Builder + data := &WorkflowData{SafeOutputs: nil} + generateSafeOutputsArtifactStagingUpload(&b, data) + assert.Empty(t, b.String(), "should generate nothing when SafeOutputs is nil") + }) +} + +func TestMarshalStringSliceJSON(t *testing.T) { + tests := []struct { + name string + input []string + expected string + }{ + {"empty slice", []string{}, "[]"}, + {"single value", []string{"dist/**"}, `["dist/**"]`}, + {"multiple values", []string{"dist/**", "reports/**/*.json"}, `["dist/**","reports/**/*.json"]`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := marshalStringSliceJSON(tt.input) + assert.Equal(t, tt.expected, result, "JSON output mismatch") + }) + } +} diff --git a/pkg/workflow/safe_outputs_config.go b/pkg/workflow/safe_outputs_config.go index a26d7ac4f2e..047c3d3eed9 100644 --- a/pkg/workflow/safe_outputs_config.go +++ b/pkg/workflow/safe_outputs_config.go @@ -269,6 +269,12 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut config.UploadAssets = uploadAssetsConfig } + // Handle upload-artifact + uploadArtifactConfig := c.parseUploadArtifactConfig(outputMap) + if uploadArtifactConfig != nil { + config.UploadArtifact = uploadArtifactConfig + } + // Handle update-release updateReleaseConfig := c.parseUpdateReleaseConfig(outputMap) if updateReleaseConfig != nil { diff --git a/pkg/workflow/safe_outputs_state.go b/pkg/workflow/safe_outputs_state.go index bc727c5e6da..cb3def0c99a 100644 --- a/pkg/workflow/safe_outputs_state.go +++ b/pkg/workflow/safe_outputs_state.go @@ -47,6 +47,7 @@ var safeOutputFieldMapping = map[string]string{ "UpdatePullRequests": "update_pull_request", "PushToPullRequestBranch": "push_to_pull_request_branch", "UploadAssets": "upload_asset", + "UploadArtifact": "upload_artifact", "UpdateRelease": "update_release", "UpdateProjects": "update_project", "CreateProjects": "create_project", diff --git a/pkg/workflow/safe_outputs_tools_computation.go b/pkg/workflow/safe_outputs_tools_computation.go index 4502e1e2b73..2b477f07923 100644 --- a/pkg/workflow/safe_outputs_tools_computation.go +++ b/pkg/workflow/safe_outputs_tools_computation.go @@ -95,6 +95,9 @@ func computeEnabledToolNames(data *WorkflowData) map[string]bool { if data.SafeOutputs.UploadAssets != nil { enabledTools["upload_asset"] = true } + if data.SafeOutputs.UploadArtifact != nil { + enabledTools["upload_artifact"] = true + } if data.SafeOutputs.MissingTool != nil { enabledTools["missing_tool"] = true } diff --git a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/basic-copilot.golden b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/basic-copilot.golden index bd5d6472956..479d7255169 100644 --- a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/basic-copilot.golden +++ b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/basic-copilot.golden @@ -21,6 +21,7 @@ jobs: if: needs.pre_activation.outputs.activated == 'true' runs-on: ubuntu-slim permissions: + actions: read contents: read outputs: comment_id: "" diff --git a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/with-imports.golden b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/with-imports.golden index c29fa80098e..4bf8f9a4c20 100644 --- a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/with-imports.golden +++ b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/with-imports.golden @@ -21,6 +21,7 @@ jobs: if: needs.pre_activation.outputs.activated == 'true' runs-on: ubuntu-slim permissions: + actions: read contents: read outputs: comment_id: "" From 60b037602cc827384c731e26f6d65e37c5eca7fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:19:18 +0000 Subject: [PATCH 03/23] feat: add upload_artifact safe output type with native GitHub Actions artifact support Agent-Logs-Url: https://github.com/github/gh-aw/sessions/06153ed3-a241-400b-9414-3dc304516475 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/publish_artifacts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/workflow/publish_artifacts.go b/pkg/workflow/publish_artifacts.go index aaacea24412..434ca5fa2fc 100644 --- a/pkg/workflow/publish_artifacts.go +++ b/pkg/workflow/publish_artifacts.go @@ -90,7 +90,7 @@ func (c *Compiler) parseUploadArtifactConfig(outputMap map[string]any) *UploadAr configMap, ok := configData.(map[string]any) if !ok { // No config map (e.g. upload-artifact: true) – use defaults. - publishArtifactsLog.Print("Using default upload-artifact configuration") + publishArtifactsLog.Print("upload-artifact enabled with default configuration") return config } From 1d4b6ad8e69cc3cd2346478802f7ca2923d1e0bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:49:29 +0000 Subject: [PATCH 04/23] fix: consolidate template literal concatenations in upload_artifact.cjs Agent-Logs-Url: https://github.com/github/gh-aw/sessions/e2c3d991-4ccd-4c47-8a7b-53d9905e1444 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/upload_artifact.cjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/actions/setup/js/upload_artifact.cjs b/actions/setup/js/upload_artifact.cjs index 9e6ace0a243..4d624951f42 100644 --- a/actions/setup/js/upload_artifact.cjs +++ b/actions/setup/js/upload_artifact.cjs @@ -319,7 +319,7 @@ async function main() { // Enforce max-uploads policy. if (uploadRequests.length > maxUploads) { - core.setFailed(`${ERR_VALIDATION}: upload_artifact: ${uploadRequests.length} requests exceed max-uploads policy (${maxUploads}). ` + `Reduce the number of upload_artifact calls or raise max-uploads in workflow configuration.`); + core.setFailed(`${ERR_VALIDATION}: upload_artifact: ${uploadRequests.length} requests exceed max-uploads policy (${maxUploads}). Reduce the number of upload_artifact calls or raise max-uploads in workflow configuration.`); return; } @@ -340,7 +340,7 @@ async function main() { // Resolve skip_archive. const skipArchive = typeof request.skip_archive === "boolean" ? request.skip_archive : defaultSkipArchive; if (skipArchive && !allowSkipArchive) { - core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: skip_archive=true is not permitted. ` + `Enable it with allow.skip-archive: true in workflow configuration.`); + core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: skip_archive=true is not permitted. Enable it with allow.skip-archive: true in workflow configuration.`); return; } @@ -356,7 +356,7 @@ async function main() { core.warning(`upload_artifact request ${i + 1}: no files matched, skipping (if-no-files=ignore)`); continue; } else { - core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: no files matched the selection criteria. ` + `Check allowed-paths, filters, or use defaults.if-no-files: ignore to skip empty uploads.`); + core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: no files matched the selection criteria. Check allowed-paths, filters, or use defaults.if-no-files: ignore to skip empty uploads.`); return; } } @@ -371,7 +371,7 @@ async function main() { // Validate total size. const totalSize = computeTotalSize(files); if (totalSize > maxSizeBytes) { - core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: total file size ${totalSize} bytes exceeds ` + `max-size-bytes limit of ${maxSizeBytes} bytes.`); + core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: total file size ${totalSize} bytes exceeds max-size-bytes limit of ${maxSizeBytes} bytes.`); return; } @@ -383,7 +383,7 @@ async function main() { const tmpId = generateTemporaryArtifactId(); resolver[tmpId] = artifactName; - core.info(`Slot ${i}: artifact="${artifactName}", files=${files.length}, size=${totalSize}B, ` + `retention=${retentionDays}d, skip_archive=${skipArchive}, tmp_id=${tmpId}`); + core.info(`Slot ${i}: artifact="${artifactName}", files=${files.length}, size=${totalSize}B, retention=${retentionDays}d, skip_archive=${skipArchive}, tmp_id=${tmpId}`); if (!isStaged) { // Stage files into the per-slot directory for the actions/upload-artifact step. From a9e8f8f5b760c7982b163c4f9bfcc9e34c75d723 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:03:27 +0000 Subject: [PATCH 05/23] feat: add upload-artifact to JSON schema (no enabled field required) Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8dce883a-59fd-437b-a8ba-9dc9587acc98 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 108 ++++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 8c34b92db88..415cb386934 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -4344,7 +4344,7 @@ }, "safe-outputs": { "type": "object", - "$comment": "Required if workflow creates or modifies GitHub resources. Operations requiring safe-outputs: autofix-code-scanning-alert, add-comment, add-labels, add-reviewer, assign-milestone, assign-to-agent, assign-to-user, close-discussion, close-issue, close-pull-request, create-agent-session, create-agent-task (deprecated, use create-agent-session), create-code-scanning-alert, create-discussion, create-issue, create-project, create-project-status-update, create-pull-request, create-pull-request-review-comment, dispatch-workflow, hide-comment, link-sub-issue, mark-pull-request-as-ready-for-review, missing-data, missing-tool, noop, push-to-pull-request-branch, remove-labels, reply-to-pull-request-review-comment, resolve-pull-request-review-thread, set-issue-type, submit-pull-request-review, threat-detection, unassign-from-user, update-discussion, update-issue, update-project, update-pull-request, update-release, upload-asset. See documentation for complete details.", + "$comment": "Required if workflow creates or modifies GitHub resources. Operations requiring safe-outputs: autofix-code-scanning-alert, add-comment, add-labels, add-reviewer, assign-milestone, assign-to-agent, assign-to-user, close-discussion, close-issue, close-pull-request, create-agent-session, create-agent-task (deprecated, use create-agent-session), create-code-scanning-alert, create-discussion, create-issue, create-project, create-project-status-update, create-pull-request, create-pull-request-review-comment, dispatch-workflow, hide-comment, link-sub-issue, mark-pull-request-as-ready-for-review, missing-data, missing-tool, noop, push-to-pull-request-branch, remove-labels, reply-to-pull-request-review-comment, resolve-pull-request-review-thread, set-issue-type, submit-pull-request-review, threat-detection, unassign-from-user, update-discussion, update-issue, update-project, update-pull-request, update-release, upload-artifact, upload-asset. See documentation for complete details.", "description": "Safe output processing configuration that automatically creates GitHub issues, comments, and pull requests from AI workflow output without requiring write permissions in the main job", "examples": [ { @@ -7445,6 +7445,112 @@ ], "description": "Enable AI agents to publish files (images, charts, reports) to an orphaned git branch for persistent storage and web access." }, + "upload-artifact": { + "oneOf": [ + { + "type": "object", + "description": "Configuration for uploading files as run-scoped GitHub Actions artifacts", + "properties": { + "max-uploads": { + "type": "integer", + "description": "Maximum number of upload_artifact tool calls allowed per run (default: 1)", + "minimum": 1, + "maximum": 20, + "default": 1 + }, + "default-retention-days": { + "type": "integer", + "description": "Default artifact retention period in days (default: 7)", + "minimum": 1, + "maximum": 90, + "default": 7 + }, + "max-retention-days": { + "type": "integer", + "description": "Maximum retention cap in days; model requests are clamped to this value (default: 30)", + "minimum": 1, + "maximum": 90, + "default": 30 + }, + "max-size-bytes": { + "type": "integer", + "description": "Maximum total upload size in bytes per slot (default: 104857600 = 100 MB)", + "minimum": 1, + "default": 104857600 + }, + "allowed-paths": { + "type": "array", + "description": "Glob patterns restricting which paths relative to the staging directory the model may upload", + "items": { + "type": "string" + } + }, + "filters": { + "type": "object", + "description": "Default include/exclude glob filters applied on top of allowed-paths", + "properties": { + "include": { + "type": "array", + "items": { "type": "string" }, + "description": "Glob patterns for files to include" + }, + "exclude": { + "type": "array", + "items": { "type": "string" }, + "description": "Glob patterns for files to exclude" + } + }, + "additionalProperties": false + }, + "defaults": { + "type": "object", + "description": "Default values injected when the model omits a field", + "properties": { + "skip-archive": { + "type": "boolean", + "description": "Default value for skip_archive (default: false)", + "default": false + }, + "if-no-files": { + "type": "string", + "description": "Behaviour when no files match: 'error' (default) or 'ignore'", + "enum": ["error", "ignore"], + "default": "error" + } + }, + "additionalProperties": false + }, + "allow": { + "type": "object", + "description": "Opt-in behaviours that must be explicitly enabled by the workflow author", + "properties": { + "skip-archive": { + "type": "boolean", + "description": "Allow the model to set skip_archive: true (uploads the file directly without archiving)", + "default": false + } + }, + "additionalProperties": false + }, + "github-token": { + "$ref": "#/$defs/github_token", + "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." + }, + "staged": { + "type": "boolean", + "description": "If true, emit step summary messages instead of making GitHub Actions artifact uploads (preview mode)", + "examples": [true, false] + } + }, + "additionalProperties": false + }, + { + "type": "null", + "description": "Enable artifact uploads with default configuration" + } + ], + "description": "Enable AI agents to upload files as run-scoped GitHub Actions artifacts. Returns a temporary artifact ID rather than a raw download URL, keeping authorization centralized." + }, "update-release": { "oneOf": [ { From ae61a262b9a730fcba4451c1596d9acd84bf86c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:30:55 +0000 Subject: [PATCH 06/23] fix: use RUNNER_TEMP env var for artifact staging and slot paths Agent-Logs-Url: https://github.com/github/gh-aw/sessions/05ddc6f7-ae5e-4faa-9423-6f551a67af06 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/upload_artifact.cjs | 10 +++++----- actions/setup/js/upload_artifact.test.cjs | 11 ++++++++--- pkg/workflow/js/safe_outputs_tools.json | 4 ++-- pkg/workflow/publish_artifacts.go | 4 ++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/actions/setup/js/upload_artifact.cjs b/actions/setup/js/upload_artifact.cjs index 4d624951f42..2ad264b17e3 100644 --- a/actions/setup/js/upload_artifact.cjs +++ b/actions/setup/js/upload_artifact.cjs @@ -6,13 +6,13 @@ * * Validates and stages artifact upload requests emitted by the model via the upload_artifact * safe output tool. The model must have already copied the files it wants to upload to - * /tmp/gh-aw/safeoutputs/upload-artifacts/ before calling the tool. + * ${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/ before calling the tool. * * This handler: * 1. Reads upload_artifact records from agent output. * 2. Validates each request against the workflow's policy configuration. * 3. Resolves the requested files (path or filter-based) from the staging directory. - * 4. Copies approved files into per-slot directories under /tmp/gh-aw/upload-artifacts/slot_N/. + * 4. Copies approved files into per-slot directories under ${RUNNER_TEMP}/gh-aw/upload-artifacts/slot_N/. * 5. Sets step outputs so the wrapping job's actions/upload-artifact steps can run conditionally. * 6. Generates a temporary artifact ID for each slot. * @@ -40,16 +40,16 @@ const { globPatternToRegex } = require("./glob_pattern_helpers.cjs"); const { ERR_CONFIG, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); /** Staging directory where the model places files to be uploaded. */ -const STAGING_DIR = "/tmp/gh-aw/safeoutputs/upload-artifacts/"; +const STAGING_DIR = `${process.env.RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/`; /** Base directory for per-slot artifact staging used by actions/upload-artifact. */ -const SLOT_BASE_DIR = "/tmp/gh-aw/upload-artifacts/"; +const SLOT_BASE_DIR = `${process.env.RUNNER_TEMP}/gh-aw/upload-artifacts/`; /** Prefix for temporary artifact IDs returned to the caller. */ const TEMP_ID_PREFIX = "tmp_artifact_"; /** Path where the resolver mapping (tmpId → artifact name) is written. */ -const RESOLVER_FILE = "/tmp/gh-aw/artifact-resolver.json"; +const RESOLVER_FILE = `${process.env.RUNNER_TEMP}/gh-aw/artifact-resolver.json`; /** * Generate a temporary artifact ID. diff --git a/actions/setup/js/upload_artifact.test.cjs b/actions/setup/js/upload_artifact.test.cjs index ef59a262a9a..cf1055530ec 100644 --- a/actions/setup/js/upload_artifact.test.cjs +++ b/actions/setup/js/upload_artifact.test.cjs @@ -8,9 +8,11 @@ import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const STAGING_DIR = "/tmp/gh-aw/safeoutputs/upload-artifacts/"; -const SLOT_BASE_DIR = "/tmp/gh-aw/upload-artifacts/"; -const RESOLVER_FILE = "/tmp/gh-aw/artifact-resolver.json"; +// Use RUNNER_TEMP as the base so paths match what upload_artifact.cjs computes at runtime. +const RUNNER_TEMP = "/tmp"; +const STAGING_DIR = `${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/`; +const SLOT_BASE_DIR = `${RUNNER_TEMP}/gh-aw/upload-artifacts/`; +const RESOLVER_FILE = `${RUNNER_TEMP}/gh-aw/artifact-resolver.json`; describe("upload_artifact.cjs", () => { let mockCore; @@ -63,6 +65,9 @@ describe("upload_artifact.cjs", () => { originalEnv = { ...process.env }; + // Set RUNNER_TEMP so the script resolves paths to the same directories as the test helpers. + process.env.RUNNER_TEMP = RUNNER_TEMP; + // Set reasonable defaults process.env.GH_AW_ARTIFACT_MAX_UPLOADS = "3"; process.env.GH_AW_ARTIFACT_DEFAULT_RETENTION_DAYS = "7"; diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index ac0196a526d..a0579d792a1 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -1586,13 +1586,13 @@ }, { "name": "upload_artifact", - "description": "Upload files as a run-scoped GitHub Actions artifact. The model must first copy files to /tmp/gh-aw/safeoutputs/upload-artifacts/ then request upload using this tool. Returns a temporary artifact ID that can be resolved to a download URL by an authorised step. Exactly one of path or filters must be present.", + "description": "Upload files as a run-scoped GitHub Actions artifact. The model must first copy files to $RUNNER_TEMP/gh-aw/safeoutputs/upload-artifacts/ then request upload using this tool. Returns a temporary artifact ID that can be resolved to a download URL by an authorised step. Exactly one of path or filters must be present.", "inputSchema": { "type": "object", "properties": { "path": { "type": "string", - "description": "Path to the file or directory to upload, relative to /tmp/gh-aw/safeoutputs/upload-artifacts/ (e.g., \"report.json\" or \"dist/\"). Required unless filters is provided." + "description": "Path to the file or directory to upload, relative to $RUNNER_TEMP/gh-aw/safeoutputs/upload-artifacts/ (e.g., \"report.json\" or \"dist/\"). Required unless filters is provided." }, "filters": { "type": "object", diff --git a/pkg/workflow/publish_artifacts.go b/pkg/workflow/publish_artifacts.go index 434ca5fa2fc..a565b3c2c3a 100644 --- a/pkg/workflow/publish_artifacts.go +++ b/pkg/workflow/publish_artifacts.go @@ -25,10 +25,10 @@ const defaultArtifactMaxRetentionDays = 30 const defaultArtifactMaxSizeBytes int64 = 104857600 // artifactStagingDir is the path where the model stages files to be uploaded as artifacts. -const artifactStagingDir = "/tmp/gh-aw/safeoutputs/upload-artifacts/" +const artifactStagingDir = "${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/" // artifactSlotDir is the per-slot directory used by the handler to organise staged files. -const artifactSlotDir = "/tmp/gh-aw/upload-artifacts/" +const artifactSlotDir = "${RUNNER_TEMP}/gh-aw/upload-artifacts/" // SafeOutputsUploadArtifactStagingArtifactName is the artifact that carries the staging directory // from the main agent job to the upload_artifact job. From f962e66767a2a73d2f49f73408643e0c0c9f0822 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 04:09:45 +0000 Subject: [PATCH 07/23] feat: update smoke-copilot to upload gh-aw binary as artifact; fix upload_artifact job permissions and duplicate step bug Agent-Logs-Url: https://github.com/github/gh-aw/sessions/c901fdd4-ee9b-47d2-a4e6-ebe13975e3a4 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot.lock.yml | 148 +++++++++++++++++++---- .github/workflows/smoke-copilot.md | 13 +- pkg/workflow/publish_artifacts.go | 12 +- 3 files changed, 137 insertions(+), 36 deletions(-) diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 4408af85f4b..5d75913a2e5 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"6d0a385e47ce5ed241f4358e1578525037722f288b64d3dc18289d01bd352fbd","agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"2a931073663f42902da7a9ca2f3f56370ad310f3e6bbcf1308329503eeabccd9","agent_id":"copilot"} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -230,9 +230,9 @@ jobs: run: | bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh { - cat << 'GH_AW_PROMPT_2d91fec7281e9c47_EOF' + cat << 'GH_AW_PROMPT_9896dd1a279d5d86_EOF' - GH_AW_PROMPT_2d91fec7281e9c47_EOF + GH_AW_PROMPT_9896dd1a279d5d86_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" @@ -240,7 +240,7 @@ jobs: cat "${RUNNER_TEMP}/gh-aw/prompts/agentic_workflows_guide.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_2d91fec7281e9c47_EOF' + cat << 'GH_AW_PROMPT_9896dd1a279d5d86_EOF' Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), add_labels, remove_labels, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message @@ -272,9 +272,9 @@ jobs: {{/if}} - GH_AW_PROMPT_2d91fec7281e9c47_EOF + GH_AW_PROMPT_9896dd1a279d5d86_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_2d91fec7281e9c47_EOF' + cat << 'GH_AW_PROMPT_9896dd1a279d5d86_EOF' ## Serena Code Analysis @@ -314,7 +314,7 @@ jobs: {{#runtime-import .github/workflows/shared/mcp/serena-go.md}} {{#runtime-import .github/workflows/shared/observability-otlp.md}} {{#runtime-import .github/workflows/smoke-copilot.md}} - GH_AW_PROMPT_2d91fec7281e9c47_EOF + GH_AW_PROMPT_9896dd1a279d5d86_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -579,9 +579,9 @@ jobs: mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_8c3103569671ea37_EOF' - {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-copilot"],"allowed_repos":["github/gh-aw"]},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_8c3103569671ea37_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_37135a487e85aeac_EOF' + {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-copilot"],"allowed_repos":["github/gh-aw"]},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"allow-skip-archive":true,"default-retention-days":1,"max-retention-days":1,"max-size-bytes":104857600,"max-uploads":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_37135a487e85aeac_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -978,7 +978,7 @@ jobs: - name: Write MCP Scripts Config run: | mkdir -p ${RUNNER_TEMP}/gh-aw/mcp-scripts/logs - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/tools.json << 'GH_AW_MCP_SCRIPTS_TOOLS_7babc89e6d790778_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/tools.json << 'GH_AW_MCP_SCRIPTS_TOOLS_d58c0e40e52491a9_EOF' { "serverName": "mcpscripts", "version": "1.0.0", @@ -1094,8 +1094,8 @@ jobs: } ] } - GH_AW_MCP_SCRIPTS_TOOLS_7babc89e6d790778_EOF - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs << 'GH_AW_MCP_SCRIPTS_SERVER_ef1fbc7ce3eca295_EOF' + GH_AW_MCP_SCRIPTS_TOOLS_d58c0e40e52491a9_EOF + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs << 'GH_AW_MCP_SCRIPTS_SERVER_dd0c3af6b77b1bf9_EOF' const path = require("path"); const { startHttpServer } = require("./mcp_scripts_mcp_server_http.cjs"); const configPath = path.join(__dirname, "tools.json"); @@ -1109,12 +1109,12 @@ jobs: console.error("Failed to start mcp-scripts HTTP server:", error); process.exit(1); }); - GH_AW_MCP_SCRIPTS_SERVER_ef1fbc7ce3eca295_EOF + GH_AW_MCP_SCRIPTS_SERVER_dd0c3af6b77b1bf9_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs - name: Write MCP Scripts Tool Files run: | - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/gh.sh << 'GH_AW_MCP_SCRIPTS_SH_GH_5a6688685d632c08_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/gh.sh << 'GH_AW_MCP_SCRIPTS_SH_GH_413a2d9b16bce3b7_EOF' #!/bin/bash # Auto-generated mcp-script tool: gh # Execute any gh CLI command. This tool is accessible as 'mcpscripts-gh'. Provide the full command after 'gh' (e.g., args: 'pr list --limit 5'). The tool will run: gh . Use single quotes ' for complex args to avoid shell interpretation issues. @@ -1125,9 +1125,9 @@ jobs: echo " token: ${GH_AW_GH_TOKEN:0:6}..." GH_TOKEN="$GH_AW_GH_TOKEN" gh $INPUT_ARGS - GH_AW_MCP_SCRIPTS_SH_GH_5a6688685d632c08_EOF + GH_AW_MCP_SCRIPTS_SH_GH_413a2d9b16bce3b7_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/gh.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-discussion-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_acccc7340415fad4_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-discussion-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_ecb08d56af922c60_EOF' #!/bin/bash # Auto-generated mcp-script tool: github-discussion-query # Query GitHub discussions with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. @@ -1262,9 +1262,9 @@ jobs: EOF fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_acccc7340415fad4_EOF + GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_ecb08d56af922c60_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-discussion-query.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-issue-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_a6eacbb65c40c0ed_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-issue-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_b2c3240691c382a4_EOF' #!/bin/bash # Auto-generated mcp-script tool: github-issue-query # Query GitHub issues with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. @@ -1343,9 +1343,9 @@ jobs: fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_a6eacbb65c40c0ed_EOF + GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_b2c3240691c382a4_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-issue-query.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-pr-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_cba8eb127506e4a8_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-pr-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_5cd7ef183044e7f8_EOF' #!/bin/bash # Auto-generated mcp-script tool: github-pr-query # Query GitHub pull requests with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. @@ -1430,7 +1430,7 @@ jobs: fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_cba8eb127506e4a8_EOF + GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_5cd7ef183044e7f8_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-pr-query.sh - name: Generate MCP Scripts Server Config @@ -1506,7 +1506,7 @@ jobs: if [ -n "${OTEL_EXPORTER_OTLP_HEADERS:-}" ]; then _GH_AW_OTLP_HEADERS_JSON=$(node -e 'const h=process.env["OTEL_EXPORTER_OTLP_HEADERS"]||"";const o={};h.split(",").forEach(function(p){const i=p.indexOf("=");if(i>0)o[p.slice(0,i).trim()]=p.slice(i+1).trim();});console.log(JSON.stringify(o));' 2>/dev/null || echo "{}") fi - cat << GH_AW_MCP_CONFIG_8d31e9e79e8b0709_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh + cat << GH_AW_MCP_CONFIG_b2fa325b88dbf094_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh { "mcpServers": { "agenticworkflows": { @@ -1632,7 +1632,7 @@ jobs: } } } - GH_AW_MCP_CONFIG_8d31e9e79e8b0709_EOF + GH_AW_MCP_CONFIG_b2fa325b88dbf094_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -1829,6 +1829,15 @@ jobs: with: name: cache-memory path: /tmp/gh-aw/cache-memory + # Upload safe-outputs upload-artifact staging for the upload_artifact job + - name: Upload Upload-Artifact Staging + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-outputs-upload-artifacts + path: ${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/ + retention-days: 1 + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1871,6 +1880,7 @@ jobs: - safe_outputs - send_slack_message - update_cache_memory + - upload_artifact if: always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true') runs-on: ubuntu-slim permissions: @@ -2001,7 +2011,7 @@ jobs: GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 📰 *BREAKING: Report filed by [{workflow_name}]({run_url})*{effective_tokens_suffix}{history_link}\",\"appendOnlyComments\":true,\"runStarted\":\"📰 BREAKING: [{workflow_name}]({run_url}) is now investigating this {event_type}. Sources say the story is developing...\",\"runSuccess\":\"📰 VERDICT: [{workflow_name}]({run_url}) has concluded. All systems operational. This is a developing story. 🎤\",\"runFailure\":\"📰 DEVELOPING STORY: [{workflow_name}]({run_url}) reports {status}. Our correspondents are investigating the incident...\"}" - GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" + GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\",\"upload_artifact\":\"\"}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -2285,7 +2295,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot\"],\"allowed_repos\":[\"github/gh-aw\"]},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot\"],\"allowed_repos\":[\"github/gh-aw\"]},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"allow-skip-archive\":true,\"default-retention-days\":1,\"max-retention-days\":1,\"max-size-bytes\":104857600,\"max-uploads\":1}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -2390,3 +2400,89 @@ jobs: key: memory-approved-a3cea483-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory + upload_artifact: + needs: + - activation + - agent + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'upload_artifact') + runs-on: ubuntu-slim + permissions: + actions: write + timeout-minutes: 10 + outputs: + artifact_count: ${{ steps.upload_artifacts.outputs.artifact_count }} + slot_0_tmp_id: ${{ steps.upload_artifacts.outputs.slot_0_tmp_id }} + steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions + persist-credentials: false + - name: Setup Scripts + id: setup + uses: ./actions/setup + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download upload-artifact staging + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: safe-outputs-upload-artifacts + path: ${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/ + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Upload artifacts + id: upload_artifacts + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ARTIFACT_MAX_UPLOADS: 1 + GH_AW_ARTIFACT_DEFAULT_RETENTION_DAYS: 1 + GH_AW_ARTIFACT_MAX_RETENTION_DAYS: 1 + GH_AW_ARTIFACT_MAX_SIZE_BYTES: 104857600 + GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE: "true" + GH_AW_WORKFLOW_NAME: "Smoke Copilot" + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 📰 *BREAKING: Report filed by [{workflow_name}]({run_url})*{effective_tokens_suffix}{history_link}\",\"appendOnlyComments\":true,\"runStarted\":\"📰 BREAKING: [{workflow_name}]({run_url}) is now investigating this {event_type}. Sources say the story is developing...\",\"runSuccess\":\"📰 VERDICT: [{workflow_name}]({run_url}) has concluded. All systems operational. This is a developing story. 🎤\",\"runFailure\":\"📰 DEVELOPING STORY: [{workflow_name}]({run_url}) reports {status}. Our correspondents are investigating the incident...\"}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/upload_artifact.cjs'); + await main(); + - name: Upload artifact slot 0 + if: steps.upload_artifacts.outputs.slot_0_enabled == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: ${{ steps.upload_artifacts.outputs.slot_0_name }} + path: ${RUNNER_TEMP}/gh-aw/upload-artifacts/slot_0/ + retention-days: ${{ steps.upload_artifacts.outputs.slot_0_retention_days }} + if-no-files-found: ignore + - name: Restore actions folder + if: always() + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions/setup + sparse-checkout-cone-mode: true + persist-credentials: false + diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index 8c6087573ed..554c219011b 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -49,6 +49,12 @@ runtimes: version: "1.25" safe-outputs: allowed-domains: [default-safe-outputs] + upload-artifact: + max-uploads: 1 + default-retention-days: 1 + max-retention-days: 1 + allow: + skip-archive: true add-comment: allowed-repos: ["github/gh-aw"] hide-older-comments: true @@ -140,9 +146,10 @@ strict: false - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) - Use the `add_comment` tool with `discussion_number: ` to add a fun, playful comment stating that the smoke test agent was here 9. **Build gh-aw**: Run `GOCACHE=/tmp/go-cache GOMODCACHE=/tmp/go-mod make build` to verify the agent can successfully build the gh-aw project (both caches must be set to /tmp because the default cache locations are not writable). If the command fails, mark this test as ❌ and report the failure. -10. **Discussion Creation Testing**: Use the `create_discussion` safe-output tool to create a discussion in the announcements category titled "copilot was here" with the label "ai-generated" -11. **Workflow Dispatch Testing**: Use the `dispatch_workflow` safe output tool to trigger the `haiku-printer` workflow with a haiku as the message input. Create an original, creative haiku about software testing or automation. -12. **PR Review Testing**: Review the diff of the current pull request. Leave 1-2 inline `create_pull_request_review_comment` comments on specific lines, then call `submit_pull_request_review` with a brief body summarizing your review and event `COMMENT`. To test `reply_to_pull_request_review_comment`: use the `pull_request_read` tool (with `method: "get_review_comments"` and `pullNumber: ${{ github.event.pull_request.number }}`) to fetch the PR's existing review comments, then reply to the most recent one using `reply_to_pull_request_review_comment` with its actual numeric `id` as the `comment_id`. Note: `create_pull_request_review_comment` does not return a `comment_id` — you must fetch existing comment IDs from the GitHub API. If the PR has no existing review comments, skip the reply sub-test. +10. **Upload gh-aw binary as artifact**: After a successful build, use bash to copy the `./gh-aw` binary into the staging directory (`mkdir -p $RUNNER_TEMP/gh-aw/safeoutputs/upload-artifacts && cp ./gh-aw $RUNNER_TEMP/gh-aw/safeoutputs/upload-artifacts/gh-aw`), then call the `upload_artifact` safe-output tool with `path: "gh-aw"`, `retention_days: 1`, and `skip_archive: true`. Mark this test as ❌ if the build in step 9 failed. +11. **Discussion Creation Testing**: Use the `create_discussion` safe-output tool to create a discussion in the announcements category titled "copilot was here" with the label "ai-generated" +12. **Workflow Dispatch Testing**: Use the `dispatch_workflow` safe output tool to trigger the `haiku-printer` workflow with a haiku as the message input. Create an original, creative haiku about software testing or automation. +13. **PR Review Testing**: Review the diff of the current pull request. Leave 1-2 inline `create_pull_request_review_comment` comments on specific lines, then call `submit_pull_request_review` with a brief body summarizing your review and event `COMMENT`. To test `reply_to_pull_request_review_comment`: use the `pull_request_read` tool (with `method: "get_review_comments"` and `pullNumber: ${{ github.event.pull_request.number }}`) to fetch the PR's existing review comments, then reply to the most recent one using `reply_to_pull_request_review_comment` with its actual numeric `id` as the `comment_id`. Note: `create_pull_request_review_comment` does not return a `comment_id` — you must fetch existing comment IDs from the GitHub API. If the PR has no existing review comments, skip the reply sub-test. ## Output diff --git a/pkg/workflow/publish_artifacts.go b/pkg/workflow/publish_artifacts.go index a565b3c2c3a..62e42b73238 100644 --- a/pkg/workflow/publish_artifacts.go +++ b/pkg/workflow/publish_artifacts.go @@ -219,13 +219,11 @@ func (c *Compiler) buildUploadArtifactJob(data *WorkflowData, mainJobName string preSteps = append(preSteps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, publishTraceID)...) } - // Download agent output artifact (to read upload_artifact requests). + // Download the staging artifact that contains the files staged by the model. + // The agent output artifact (carrying upload_artifact NDJSON records) is NOT added here + // because buildCustomActionStep / buildGitHubScriptStep already prepends that step + // automatically to every safe-output job. artifactPrefix := artifactPrefixExprForAgentDownstreamJob(data) - preSteps = append(preSteps, - buildAgentOutputDownloadSteps(artifactPrefix)..., - ) - - // Download the staging artifact that holds the files the model wants to upload. stagingArtifactName := artifactPrefix + SafeOutputsUploadArtifactStagingArtifactName preSteps = append(preSteps, " - name: Download upload-artifact staging\n", @@ -316,7 +314,7 @@ func (c *Compiler) buildUploadArtifactJob(data *WorkflowData, mainJobName string MainJobName: mainJobName, CustomEnvVars: customEnvVars, Script: "", - Permissions: NewPermissions(), + Permissions: NewPermissionsActionsWrite(), Outputs: outputs, Condition: jobCondition, PreSteps: preSteps, From bedab80ef099e60d748066c996ad90162f81f97d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 04:21:55 +0000 Subject: [PATCH 08/23] fix: remove actions:write from upload_artifact job - uses ACTIONS_RUNTIME_TOKEN instead Agent-Logs-Url: https://github.com/github/gh-aw/sessions/4404709a-1bdd-4947-b526-9edf180e24cc Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot.lock.yml | 2 -- pkg/workflow/publish_artifacts.go | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 5d75913a2e5..753f03400b1 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -2406,8 +2406,6 @@ jobs: - agent if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'upload_artifact') runs-on: ubuntu-slim - permissions: - actions: write timeout-minutes: 10 outputs: artifact_count: ${{ steps.upload_artifacts.outputs.artifact_count }} diff --git a/pkg/workflow/publish_artifacts.go b/pkg/workflow/publish_artifacts.go index 62e42b73238..2a4dd21e5a1 100644 --- a/pkg/workflow/publish_artifacts.go +++ b/pkg/workflow/publish_artifacts.go @@ -314,7 +314,7 @@ func (c *Compiler) buildUploadArtifactJob(data *WorkflowData, mainJobName string MainJobName: mainJobName, CustomEnvVars: customEnvVars, Script: "", - Permissions: NewPermissionsActionsWrite(), + Permissions: NewPermissions(), Outputs: outputs, Condition: jobCondition, PreSteps: preSteps, From fdeeba3ed4ebd87ea70498db1fe60912194956a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 04:35:15 +0000 Subject: [PATCH 09/23] Add changeset --- .changeset/patch-add-upload-artifact-safe-output.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/patch-add-upload-artifact-safe-output.md diff --git a/.changeset/patch-add-upload-artifact-safe-output.md b/.changeset/patch-add-upload-artifact-safe-output.md new file mode 100644 index 00000000000..6159e700eeb --- /dev/null +++ b/.changeset/patch-add-upload-artifact-safe-output.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Add a new `upload-artifact` safe output type for run-scoped GitHub Actions artifact uploads, including frontmatter configuration, validation, and runtime handling that returns temporary artifact IDs for downstream resolution. From 4efbf5baa8432002546e4cb975b0d46996fb3069 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 04:53:25 +0000 Subject: [PATCH 10/23] fix: use runner.temp expression syntax in upload-artifact path inputs Agent-Logs-Url: https://github.com/github/gh-aw/sessions/7e04c8c2-0529-439d-b4cd-b385b1a309c8 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot.lock.yml | 6 +++--- pkg/workflow/publish_artifacts.go | 18 +++++++++++++++--- pkg/workflow/publish_artifacts_test.go | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 753f03400b1..ab82c951b89 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -1835,7 +1835,7 @@ jobs: uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: name: safe-outputs-upload-artifacts - path: ${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/ + path: ${{ runner.temp }}/gh-aw/safeoutputs/upload-artifacts/ retention-days: 1 if-no-files-found: ignore - name: Upload agent artifacts @@ -2430,7 +2430,7 @@ jobs: uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: safe-outputs-upload-artifacts - path: ${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/ + path: ${{ runner.temp }}/gh-aw/safeoutputs/upload-artifacts/ - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -2471,7 +2471,7 @@ jobs: uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: name: ${{ steps.upload_artifacts.outputs.slot_0_name }} - path: ${RUNNER_TEMP}/gh-aw/upload-artifacts/slot_0/ + path: ${{ runner.temp }}/gh-aw/upload-artifacts/slot_0/ retention-days: ${{ steps.upload_artifacts.outputs.slot_0_retention_days }} if-no-files-found: ignore - name: Restore actions folder diff --git a/pkg/workflow/publish_artifacts.go b/pkg/workflow/publish_artifacts.go index 2a4dd21e5a1..d9427882f44 100644 --- a/pkg/workflow/publish_artifacts.go +++ b/pkg/workflow/publish_artifacts.go @@ -25,11 +25,23 @@ const defaultArtifactMaxRetentionDays = 30 const defaultArtifactMaxSizeBytes int64 = 104857600 // artifactStagingDir is the path where the model stages files to be uploaded as artifacts. +// Use the shell-variable form only inside `run:` blocks; for `with: path:` inputs use +// artifactStagingDirExpr which uses the GitHub Actions expression syntax. const artifactStagingDir = "${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/" +// artifactStagingDirExpr is the GitHub Actions expression form of artifactStagingDir. +// `actions/upload-artifact` and `actions/download-artifact` do not expand shell variables +// in their `path:` inputs, so we must use ${{ runner.temp }} here. +const artifactStagingDirExpr = "${{ runner.temp }}/gh-aw/safeoutputs/upload-artifacts/" + // artifactSlotDir is the per-slot directory used by the handler to organise staged files. +// Use the shell-variable form only inside `run:` blocks; for `with: path:` inputs use +// artifactSlotDirExpr which uses the GitHub Actions expression syntax. const artifactSlotDir = "${RUNNER_TEMP}/gh-aw/upload-artifacts/" +// artifactSlotDirExpr is the GitHub Actions expression form of artifactSlotDir. +const artifactSlotDirExpr = "${{ runner.temp }}/gh-aw/upload-artifacts/" + // SafeOutputsUploadArtifactStagingArtifactName is the artifact that carries the staging directory // from the main agent job to the upload_artifact job. const SafeOutputsUploadArtifactStagingArtifactName = "safe-outputs-upload-artifacts" @@ -231,7 +243,7 @@ func (c *Compiler) buildUploadArtifactJob(data *WorkflowData, mainJobName string fmt.Sprintf(" uses: %s\n", GetActionPin("actions/download-artifact")), " with:\n", fmt.Sprintf(" name: %s\n", stagingArtifactName), - fmt.Sprintf(" path: %s\n", artifactStagingDir), + fmt.Sprintf(" path: %s\n", artifactStagingDirExpr), ) // Build custom environment variables consumed by upload_artifact.cjs. @@ -276,7 +288,7 @@ func (c *Compiler) buildUploadArtifactJob(data *WorkflowData, mainJobName string // the Nth upload_artifact request was successfully validated and staged. var postSteps []string for i := range cfg.MaxUploads { - slotDir := fmt.Sprintf("%sslot_%d/", artifactSlotDir, i) + slotDir := fmt.Sprintf("%sslot_%d/", artifactSlotDirExpr, i) postSteps = append(postSteps, fmt.Sprintf(" - name: Upload artifact slot %d\n", i), fmt.Sprintf(" if: steps.upload_artifacts.outputs.slot_%d_enabled == 'true'\n", i), @@ -342,7 +354,7 @@ func generateSafeOutputsArtifactStagingUpload(builder *strings.Builder, data *Wo fmt.Fprintf(builder, " uses: %s\n", GetActionPin("actions/upload-artifact")) builder.WriteString(" with:\n") fmt.Fprintf(builder, " name: %s%s\n", prefix, SafeOutputsUploadArtifactStagingArtifactName) - fmt.Fprintf(builder, " path: %s\n", artifactStagingDir) + fmt.Fprintf(builder, " path: %s\n", artifactStagingDirExpr) builder.WriteString(" retention-days: 1\n") builder.WriteString(" if-no-files-found: ignore\n") } diff --git a/pkg/workflow/publish_artifacts_test.go b/pkg/workflow/publish_artifacts_test.go index 459fc3a8649..7642075d9a8 100644 --- a/pkg/workflow/publish_artifacts_test.go +++ b/pkg/workflow/publish_artifacts_test.go @@ -274,7 +274,7 @@ func TestGenerateSafeOutputsArtifactStagingUpload(t *testing.T) { generateSafeOutputsArtifactStagingUpload(&b, data) result := b.String() assert.Contains(t, result, "safe-outputs-upload-artifacts", "should reference staging artifact name") - assert.Contains(t, result, artifactStagingDir, "should reference staging directory") + assert.Contains(t, result, artifactStagingDirExpr, "should reference staging directory") assert.Contains(t, result, "if: always()", "should have always() condition") }) From d577e59111aac801a8739fd1e082be0d8a283b82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:39:02 +0000 Subject: [PATCH 11/23] feat: handle upload_artifact inline in safe_outputs handler loop instead of standalone job Agent-Logs-Url: https://github.com/github/gh-aw/sessions/ce9744d6-4d2c-4fce-b5c8-19ddd4452fe7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot.lock.yml | 103 ++----- .../setup/js/safe_output_handler_manager.cjs | 5 +- actions/setup/js/upload_artifact.cjs | 219 +++++++-------- actions/setup/js/upload_artifact.test.cjs | 253 +++++++----------- pkg/workflow/compiler_safe_output_jobs.go | 18 -- pkg/workflow/compiler_safe_outputs_config.go | 8 + pkg/workflow/compiler_safe_outputs_job.go | 51 +++- pkg/workflow/publish_artifacts.go | 152 +---------- pkg/workflow/publish_artifacts_test.go | 61 ----- 9 files changed, 276 insertions(+), 594 deletions(-) diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index ab82c951b89..f8dc552f423 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -1880,7 +1880,6 @@ jobs: - safe_outputs - send_slack_message - update_cache_memory - - upload_artifact if: always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true') runs-on: ubuntu-slim permissions: @@ -2011,7 +2010,7 @@ jobs: GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 📰 *BREAKING: Report filed by [{workflow_name}]({run_url})*{effective_tokens_suffix}{history_link}\",\"appendOnlyComments\":true,\"runStarted\":\"📰 BREAKING: [{workflow_name}]({run_url}) is now investigating this {event_type}. Sources say the story is developing...\",\"runSuccess\":\"📰 VERDICT: [{workflow_name}]({run_url}) has concluded. All systems operational. This is a developing story. 🎤\",\"runFailure\":\"📰 DEVELOPING STORY: [{workflow_name}]({run_url}) reports {status}. Our correspondents are investigating the incident...\"}" - GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\",\"upload_artifact\":\"\"}" + GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -2246,6 +2245,8 @@ jobs: created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + upload_artifact_count: ${{ steps.process_safe_outputs.outputs.upload_artifact_count }} + upload_artifact_slot_0_tmp_id: ${{ steps.process_safe_outputs.outputs.slot_0_tmp_id }} steps: - name: Checkout actions folder uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -2286,6 +2287,12 @@ jobs: GH_HOST="${GITHUB_SERVER_URL#https://}" GH_HOST="${GH_HOST#http://}" echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Download upload-artifact staging + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: safe-outputs-upload-artifacts + path: ${{ runner.temp }}/gh-aw/safeoutputs/upload-artifacts/ - name: Process Safe Outputs id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -2303,6 +2310,14 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); + - name: Upload artifact slot 0 + if: steps.process_safe_outputs.outputs.slot_0_enabled == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: ${{ steps.process_safe_outputs.outputs.slot_0_name }} + path: ${{ runner.temp }}/gh-aw/upload-artifacts/slot_0/ + retention-days: ${{ steps.process_safe_outputs.outputs.slot_0_retention_days }} + if-no-files-found: ignore - name: Upload Safe Output Items if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 @@ -2400,87 +2415,3 @@ jobs: key: memory-approved-a3cea483-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory - upload_artifact: - needs: - - activation - - agent - if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'upload_artifact') - runs-on: ubuntu-slim - timeout-minutes: 10 - outputs: - artifact_count: ${{ steps.upload_artifacts.outputs.artifact_count }} - slot_0_tmp_id: ${{ steps.upload_artifacts.outputs.slot_0_tmp_id }} - steps: - - name: Checkout actions folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: github/gh-aw - sparse-checkout: | - actions - persist-credentials: false - - name: Setup Scripts - id: setup - uses: ./actions/setup - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - trace-id: ${{ needs.activation.outputs.setup-trace-id }} - - name: Download upload-artifact staging - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: safe-outputs-upload-artifacts - path: ${{ runner.temp }}/gh-aw/safeoutputs/upload-artifacts/ - - name: Download agent output artifact - id: download-agent-output - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: agent - path: /tmp/gh-aw/ - - name: Setup agent output environment variable - id: setup-agent-output-env - if: steps.download-agent-output.outcome == 'success' - run: | - mkdir -p /tmp/gh-aw/ - find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Upload artifacts - id: upload_artifacts - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_ARTIFACT_MAX_UPLOADS: 1 - GH_AW_ARTIFACT_DEFAULT_RETENTION_DAYS: 1 - GH_AW_ARTIFACT_MAX_RETENTION_DAYS: 1 - GH_AW_ARTIFACT_MAX_SIZE_BYTES: 104857600 - GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE: "true" - GH_AW_WORKFLOW_NAME: "Smoke Copilot" - GH_AW_ENGINE_ID: "copilot" - GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 📰 *BREAKING: Report filed by [{workflow_name}]({run_url})*{effective_tokens_suffix}{history_link}\",\"appendOnlyComments\":true,\"runStarted\":\"📰 BREAKING: [{workflow_name}]({run_url}) is now investigating this {event_type}. Sources say the story is developing...\",\"runSuccess\":\"📰 VERDICT: [{workflow_name}]({run_url}) has concluded. All systems operational. This is a developing story. 🎤\",\"runFailure\":\"📰 DEVELOPING STORY: [{workflow_name}]({run_url}) reports {status}. Our correspondents are investigating the incident...\"}" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/upload_artifact.cjs'); - await main(); - - name: Upload artifact slot 0 - if: steps.upload_artifacts.outputs.slot_0_enabled == 'true' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: ${{ steps.upload_artifacts.outputs.slot_0_name }} - path: ${{ runner.temp }}/gh-aw/upload-artifacts/slot_0/ - retention-days: ${{ steps.upload_artifacts.outputs.slot_0_retention_days }} - if-no-files-found: ignore - - name: Restore actions folder - if: always() - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: github/gh-aw - sparse-checkout: | - actions/setup - sparse-checkout-cone-mode: true - persist-credentials: false - diff --git a/actions/setup/js/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs index bc11c8758aa..2600089c7b0 100644 --- a/actions/setup/js/safe_output_handler_manager.cjs +++ b/actions/setup/js/safe_output_handler_manager.cjs @@ -74,16 +74,17 @@ const HANDLER_MAP = { create_project: "./create_project.cjs", create_project_status_update: "./create_project_status_update.cjs", update_project: "./update_project.cjs", + upload_artifact: "./upload_artifact.cjs", }; /** * Message types handled by standalone steps (not through the handler manager) * These types should not trigger warnings when skipped by the handler manager * - * Standalone types: upload_asset, upload_artifact, noop + * Standalone types: upload_asset, noop * - Have dedicated processing steps with specialized logic */ -const STANDALONE_STEP_TYPES = new Set(["upload_asset", "upload_artifact", "noop"]); +const STANDALONE_STEP_TYPES = new Set(["upload_asset", "noop"]); /** * Code-push safe output types that must succeed before remaining outputs are processed. diff --git a/actions/setup/js/upload_artifact.cjs b/actions/setup/js/upload_artifact.cjs index 2ad264b17e3..440769b9ede 100644 --- a/actions/setup/js/upload_artifact.cjs +++ b/actions/setup/js/upload_artifact.cjs @@ -8,36 +8,35 @@ * safe output tool. The model must have already copied the files it wants to upload to * ${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/ before calling the tool. * - * This handler: - * 1. Reads upload_artifact records from agent output. - * 2. Validates each request against the workflow's policy configuration. - * 3. Resolves the requested files (path or filter-based) from the staging directory. - * 4. Copies approved files into per-slot directories under ${RUNNER_TEMP}/gh-aw/upload-artifacts/slot_N/. - * 5. Sets step outputs so the wrapping job's actions/upload-artifact steps can run conditionally. - * 6. Generates a temporary artifact ID for each slot. + * This handler follows the per-message handler pattern used by the safe_outputs handler loop. + * main(config) returns a per-message handler function that: + * 1. Validates the request against the workflow's policy configuration. + * 2. Resolves the requested files (path or filter-based) from the staging directory. + * 3. Copies approved files into per-slot directories under ${RUNNER_TEMP}/gh-aw/upload-artifacts/slot_N/. + * 4. Sets step outputs (slot_N_enabled, slot_N_name, etc.) so the wrapping job's + * actions/upload-artifact steps can run conditionally. + * 5. Generates a temporary artifact ID for each slot. * - * Environment variables consumed (set by the Go job builder): - * GH_AW_ARTIFACT_MAX_UPLOADS - Max number of upload_artifact calls allowed - * GH_AW_ARTIFACT_DEFAULT_RETENTION_DAYS - Default retention period - * GH_AW_ARTIFACT_MAX_RETENTION_DAYS - Maximum retention cap - * GH_AW_ARTIFACT_MAX_SIZE_BYTES - Maximum total bytes per upload - * GH_AW_ARTIFACT_ALLOWED_PATHS - JSON array of allowed path patterns - * GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE - "true" if skip_archive is permitted - * GH_AW_ARTIFACT_DEFAULT_SKIP_ARCHIVE - "true" if skip_archive defaults to true - * GH_AW_ARTIFACT_DEFAULT_IF_NO_FILES - "error" or "ignore" - * GH_AW_ARTIFACT_FILTERS_INCLUDE - JSON array of default include patterns - * GH_AW_ARTIFACT_FILTERS_EXCLUDE - JSON array of default exclude patterns - * GH_AW_AGENT_OUTPUT - Path to agent output file - * GH_AW_SAFE_OUTPUTS_STAGED - "true" for staged/dry-run mode + * Configuration keys (passed via config parameter from handler manager): + * max-uploads - Max number of upload_artifact calls allowed (default: 1) + * default-retention-days - Default retention period (default: 7) + * max-retention-days - Maximum retention cap (default: 30) + * max-size-bytes - Maximum total bytes per upload (default: 100 MB) + * allowed-paths - Array of allowed path glob patterns + * allow-skip-archive - true if skip_archive is permitted + * default-skip-archive - true if skip_archive defaults to true + * default-if-no-files - "error" or "ignore" (default: "error") + * filters-include - Array of default include glob patterns + * filters-exclude - Array of default exclude glob patterns + * staged - true for staged/dry-run mode */ const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); -const { loadAgentOutput } = require("./load_agent_output.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { globPatternToRegex } = require("./glob_pattern_helpers.cjs"); -const { ERR_CONFIG, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); +const { ERR_VALIDATION } = require("./error_codes.cjs"); /** Staging directory where the model places files to be uploaded. */ const STAGING_DIR = `${process.env.RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/`; @@ -65,21 +64,6 @@ function generateTemporaryArtifactId() { return id; } -/** - * Parse a JSON array from an environment variable, returning an empty array on failure. - * @param {string|undefined} envVar - * @returns {string[]} - */ -function parseJsonArrayEnv(envVar) { - if (!envVar) return []; - try { - const parsed = JSON.parse(envVar); - return Array.isArray(parsed) ? parsed.filter(v => typeof v === "string") : []; - } catch { - return []; - } -} - /** * Check whether a relative path matches any of the provided glob patterns. * @param {string} relPath - Path relative to the staging root @@ -281,105 +265,103 @@ function stageFilesToSlot(files, slotDir) { } } -async function main() { - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; - - // Load policy configuration from environment variables. - const maxUploads = parseInt(process.env.GH_AW_ARTIFACT_MAX_UPLOADS || "1", 10) || 1; - const defaultRetentionDays = parseInt(process.env.GH_AW_ARTIFACT_DEFAULT_RETENTION_DAYS || "7", 10) || 7; - const maxRetentionDays = parseInt(process.env.GH_AW_ARTIFACT_MAX_RETENTION_DAYS || "30", 10) || 30; - const maxSizeBytes = parseInt(process.env.GH_AW_ARTIFACT_MAX_SIZE_BYTES || "104857600", 10) || 104857600; - const allowSkipArchive = process.env.GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE === "true"; - const defaultSkipArchive = process.env.GH_AW_ARTIFACT_DEFAULT_SKIP_ARCHIVE === "true"; - const defaultIfNoFiles = process.env.GH_AW_ARTIFACT_DEFAULT_IF_NO_FILES || "error"; - const allowedPaths = parseJsonArrayEnv(process.env.GH_AW_ARTIFACT_ALLOWED_PATHS); - const filtersInclude = parseJsonArrayEnv(process.env.GH_AW_ARTIFACT_FILTERS_INCLUDE); - const filtersExclude = parseJsonArrayEnv(process.env.GH_AW_ARTIFACT_FILTERS_EXCLUDE); +/** + * Main handler factory for upload_artifact. + * Returns a per-message handler function that processes a single upload_artifact request. + * + * @param {Object} config - Handler configuration from the safe outputs config + * @returns {Promise} Per-message handler function + */ +async function main(config = {}) { + const maxUploads = typeof config["max-uploads"] === "number" ? config["max-uploads"] : 1; + const defaultRetentionDays = typeof config["default-retention-days"] === "number" ? config["default-retention-days"] : 7; + const maxRetentionDays = typeof config["max-retention-days"] === "number" ? config["max-retention-days"] : 30; + const maxSizeBytes = typeof config["max-size-bytes"] === "number" ? config["max-size-bytes"] : 104857600; + const allowSkipArchive = config["allow-skip-archive"] === true; + const defaultSkipArchive = config["default-skip-archive"] === true; + const defaultIfNoFiles = typeof config["default-if-no-files"] === "string" ? config["default-if-no-files"] : "error"; + const allowedPaths = Array.isArray(config["allowed-paths"]) ? config["allowed-paths"] : []; + const filtersInclude = Array.isArray(config["filters-include"]) ? config["filters-include"] : []; + const filtersExclude = Array.isArray(config["filters-exclude"]) ? config["filters-exclude"] : []; + const isStaged = config["staged"] === true || process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; core.info(`upload_artifact handler: max_uploads=${maxUploads}, default_retention=${defaultRetentionDays}, max_retention=${maxRetentionDays}`); core.info(`Allowed paths: ${allowedPaths.length > 0 ? allowedPaths.join(", ") : "(none – all staging files allowed)"}`); - // Load agent output to find upload_artifact records. - const result = loadAgentOutput(); - if (!result.success) { - core.info("No agent output found, skipping upload_artifact processing"); - core.setOutput("artifact_count", "0"); - return; - } - - const uploadRequests = result.items.filter(/** @param {any} item */ item => item.type === "upload_artifact"); - - if (uploadRequests.length === 0) { - core.info("No upload_artifact records in agent output"); - core.setOutput("artifact_count", "0"); - return; - } - - core.info(`Found ${uploadRequests.length} upload_artifact request(s)`); - - // Enforce max-uploads policy. - if (uploadRequests.length > maxUploads) { - core.setFailed(`${ERR_VALIDATION}: upload_artifact: ${uploadRequests.length} requests exceed max-uploads policy (${maxUploads}). Reduce the number of upload_artifact calls or raise max-uploads in workflow configuration.`); - return; - } - - if (!fs.existsSync(STAGING_DIR)) { - core.warning(`Staging directory ${STAGING_DIR} does not exist. Did the model copy files there before calling upload_artifact?`); - fs.mkdirSync(STAGING_DIR, { recursive: true }); - } + // Slot index tracks which slot each successful request maps to. + let slotIndex = 0; /** @type {Record} resolver: tmpId → artifact name */ const resolver = {}; - let successfulUploads = 0; + /** + * Per-message handler: processes one upload_artifact request. + * + * Called by the safe_outputs handler manager for each `upload_artifact` message emitted + * by the model. State (slotIndex, resolver) is shared across calls via closure so that + * successive requests are assigned to sequential slot directories. + * + * @param {Object} message - The upload_artifact message from the model + * @param {Object} resolvedTemporaryIds - Map of already-resolved temporary IDs (unused here) + * @param {Map} temporaryIdMap - Shared temp-ID map; the handler does not modify it + * @returns {Promise<{success: boolean, error?: string, skipped?: boolean, tmpId?: string, artifactName?: string, slotIndex?: number}>} + */ + return async function handleUploadArtifact(message, resolvedTemporaryIds, temporaryIdMap) { + if (slotIndex >= maxUploads) { + return { + success: false, + error: `${ERR_VALIDATION}: upload_artifact: exceeded max-uploads policy (${maxUploads}). Reduce the number of upload_artifact calls or raise max-uploads in workflow configuration.`, + }; + } - for (let i = 0; i < uploadRequests.length; i++) { - const request = uploadRequests[i]; - core.info(`Processing upload_artifact request ${i + 1}/${uploadRequests.length}`); + const i = slotIndex; // Resolve skip_archive. - const skipArchive = typeof request.skip_archive === "boolean" ? request.skip_archive : defaultSkipArchive; + const skipArchive = typeof message.skip_archive === "boolean" ? message.skip_archive : defaultSkipArchive; if (skipArchive && !allowSkipArchive) { - core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: skip_archive=true is not permitted. Enable it with allow.skip-archive: true in workflow configuration.`); - return; + return { + success: false, + error: `${ERR_VALIDATION}: upload_artifact: skip_archive=true is not permitted. Enable it with allow.skip-archive: true in workflow configuration.`, + }; } // Resolve files. - const { files, error: resolveError } = resolveFiles(request, allowedPaths, filtersInclude, filtersExclude); + const { files, error: resolveError } = resolveFiles(message, allowedPaths, filtersInclude, filtersExclude); if (resolveError) { - core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: ${resolveError}`); - return; + return { success: false, error: `${ERR_VALIDATION}: upload_artifact: ${resolveError}` }; } if (files.length === 0) { if (defaultIfNoFiles === "ignore") { - core.warning(`upload_artifact request ${i + 1}: no files matched, skipping (if-no-files=ignore)`); - continue; - } else { - core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: no files matched the selection criteria. Check allowed-paths, filters, or use defaults.if-no-files: ignore to skip empty uploads.`); - return; + core.warning(`upload_artifact: no files matched, skipping (if-no-files=ignore)`); + return { success: false, skipped: true, error: "No files matched the selection criteria" }; } + return { + success: false, + error: `${ERR_VALIDATION}: upload_artifact: no files matched the selection criteria. Check allowed-paths, filters, or use defaults.if-no-files: ignore to skip empty uploads.`, + }; } // Validate skip_archive file-count constraint. const skipArchiveError = validateSkipArchive(skipArchive, files); if (skipArchiveError) { - core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: ${skipArchiveError}`); - return; + return { success: false, error: `${ERR_VALIDATION}: upload_artifact: ${skipArchiveError}` }; } // Validate total size. const totalSize = computeTotalSize(files); if (totalSize > maxSizeBytes) { - core.setFailed(`${ERR_VALIDATION}: upload_artifact request ${i + 1}: total file size ${totalSize} bytes exceeds max-size-bytes limit of ${maxSizeBytes} bytes.`); - return; + return { + success: false, + error: `${ERR_VALIDATION}: upload_artifact: total file size ${totalSize} bytes exceeds max-size-bytes limit of ${maxSizeBytes} bytes.`, + }; } // Compute retention days. - const retentionDays = clampRetention(typeof request.retention_days === "number" ? request.retention_days : undefined, defaultRetentionDays, maxRetentionDays); + const retentionDays = clampRetention(typeof message.retention_days === "number" ? message.retention_days : undefined, defaultRetentionDays, maxRetentionDays); // Derive artifact name and generate temporary ID. - const artifactName = deriveArtifactName(request, i); + const artifactName = deriveArtifactName(message, i); const tmpId = generateTemporaryArtifactId(); resolver[tmpId] = artifactName; @@ -394,7 +376,7 @@ async function main() { core.info(`Staged mode: skipping file staging for slot ${i}`); } - // Set step outputs for the conditional actions/upload-artifact steps in the job YAML. + // Set step outputs for the conditional actions/upload-artifact steps in the safe_outputs job. core.setOutput(`slot_${i}_enabled`, "true"); core.setOutput(`slot_${i}_name`, artifactName); core.setOutput(`slot_${i}_retention_days`, String(retentionDays)); @@ -402,26 +384,27 @@ async function main() { core.setOutput(`slot_${i}_file_count`, String(files.length)); core.setOutput(`slot_${i}_size_bytes`, String(totalSize)); - successfulUploads++; - } + slotIndex++; - // Write resolver mapping so downstream steps can resolve tmp IDs to artifact names. - try { - fs.mkdirSync(path.dirname(RESOLVER_FILE), { recursive: true }); - fs.writeFileSync(RESOLVER_FILE, JSON.stringify(resolver, null, 2)); - core.info(`Wrote artifact resolver mapping to ${RESOLVER_FILE}`); - } catch (err) { - core.warning(`Failed to write artifact resolver file: ${getErrorMessage(err)}`); - } + // Update the count output. + core.setOutput("upload_artifact_count", String(slotIndex)); - core.setOutput("artifact_count", String(successfulUploads)); - core.info(`upload_artifact handler complete: ${successfulUploads} artifact(s) staged`); + // Write/update resolver mapping so downstream steps can resolve tmp IDs to artifact names. + try { + fs.mkdirSync(path.dirname(RESOLVER_FILE), { recursive: true }); + fs.writeFileSync(RESOLVER_FILE, JSON.stringify(resolver, null, 2)); + core.info(`Wrote artifact resolver mapping to ${RESOLVER_FILE}`); + } catch (err) { + core.warning(`Failed to write artifact resolver file: ${getErrorMessage(err)}`); + } - if (isStaged) { - core.summary.addHeading("🎭 Staged Mode: Artifact Upload Preview", 2); - core.summary.addRaw(`Would upload **${successfulUploads}** artifact(s). Files staged at ${STAGING_DIR}.`); - await core.summary.write(); - } + return { + success: true, + tmpId, + artifactName, + slotIndex: i, + }; + }; } module.exports = { main }; diff --git a/actions/setup/js/upload_artifact.test.cjs b/actions/setup/js/upload_artifact.test.cjs index cf1055530ec..4b6b3d39c4d 100644 --- a/actions/setup/js/upload_artifact.test.cjs +++ b/actions/setup/js/upload_artifact.test.cjs @@ -16,18 +16,8 @@ const RESOLVER_FILE = `${RUNNER_TEMP}/gh-aw/artifact-resolver.json`; describe("upload_artifact.cjs", () => { let mockCore; - let agentOutputPath; let originalEnv; - /** - * @param {object} data - */ - function writeAgentOutput(data) { - agentOutputPath = path.join(os.tmpdir(), `test_upload_artifact_${Date.now()}_${Math.random().toString(36).slice(2)}.json`); - fs.writeFileSync(agentOutputPath, JSON.stringify(data)); - process.env.GH_AW_AGENT_OUTPUT = agentOutputPath; - } - /** * @param {string} relPath * @param {string} content @@ -39,12 +29,41 @@ describe("upload_artifact.cjs", () => { } /** - * @returns {Promise} + * Build a config object (replaces ENV vars in the old standalone approach). + * @param {object} overrides + */ + function buildConfig(overrides = {}) { + return { + "max-uploads": 3, + "default-retention-days": 7, + "max-retention-days": 30, + "max-size-bytes": 104857600, + ...overrides, + }; + } + + /** + * Run the handler against a list of messages using the new per-message pattern. + * Simulates what the handler manager does. + * @param {object} config + * @param {object[]} messages + * @returns {Promise} results from each message handler call */ - async function runMain() { + async function runHandler(config, messages) { const scriptText = fs.readFileSync(path.join(__dirname, "upload_artifact.cjs"), "utf8"); global.core = mockCore; - await eval(`(async () => { ${scriptText}; await main(); })()`); + let handlerFn; + await eval(`(async () => { ${scriptText}; handlerFn = await main(config); })()`); + const results = []; + for (const msg of messages) { + const result = await handlerFn(msg, {}, new Map()); + results.push(result); + // Simulate handler manager calling setFailed on failure + if (result && result.success === false && !result.skipped) { + mockCore.setFailed(result.error); + } + } + return results; } beforeEach(() => { @@ -67,18 +86,6 @@ describe("upload_artifact.cjs", () => { // Set RUNNER_TEMP so the script resolves paths to the same directories as the test helpers. process.env.RUNNER_TEMP = RUNNER_TEMP; - - // Set reasonable defaults - process.env.GH_AW_ARTIFACT_MAX_UPLOADS = "3"; - process.env.GH_AW_ARTIFACT_DEFAULT_RETENTION_DAYS = "7"; - process.env.GH_AW_ARTIFACT_MAX_RETENTION_DAYS = "30"; - process.env.GH_AW_ARTIFACT_MAX_SIZE_BYTES = "104857600"; - delete process.env.GH_AW_ARTIFACT_ALLOWED_PATHS; - delete process.env.GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE; - delete process.env.GH_AW_ARTIFACT_DEFAULT_SKIP_ARCHIVE; - delete process.env.GH_AW_ARTIFACT_DEFAULT_IF_NO_FILES; - delete process.env.GH_AW_ARTIFACT_FILTERS_INCLUDE; - delete process.env.GH_AW_ARTIFACT_FILTERS_EXCLUDE; delete process.env.GH_AW_SAFE_OUTPUTS_STAGED; // Ensure staging dir exists and is clean @@ -99,45 +106,19 @@ describe("upload_artifact.cjs", () => { }); afterEach(() => { - // Restore env process.env = originalEnv; - - if (agentOutputPath && fs.existsSync(agentOutputPath)) { - fs.unlinkSync(agentOutputPath); - } - }); - - describe("no agent output", () => { - it("sets artifact_count to 0 when no agent output is present", async () => { - delete process.env.GH_AW_AGENT_OUTPUT; - await runMain(); - expect(mockCore.setOutput).toHaveBeenCalledWith("artifact_count", "0"); - expect(mockCore.setFailed).not.toHaveBeenCalled(); - }); - }); - - describe("no upload_artifact records", () => { - it("sets artifact_count to 0 when output has no upload_artifact items", async () => { - writeAgentOutput({ items: [{ type: "create_issue", title: "test" }] }); - await runMain(); - expect(mockCore.setOutput).toHaveBeenCalledWith("artifact_count", "0"); - expect(mockCore.setFailed).not.toHaveBeenCalled(); - }); }); describe("path-based upload", () => { it("stages a single file and sets slot outputs", async () => { writeStaging("report.json", '{"result": "ok"}'); - writeAgentOutput({ - items: [{ type: "upload_artifact", path: "report.json", retention_days: 14 }], - }); - await runMain(); + await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json", retention_days: 14 }]); expect(mockCore.setFailed).not.toHaveBeenCalled(); expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_retention_days", "14"); - expect(mockCore.setOutput).toHaveBeenCalledWith("artifact_count", "1"); + expect(mockCore.setOutput).toHaveBeenCalledWith("upload_artifact_count", "1"); // Verify the file was staged into slot_0. const slotFile = path.join(SLOT_BASE_DIR, "slot_0", "report.json"); @@ -146,11 +127,8 @@ describe("upload_artifact.cjs", () => { it("clamps retention days to max-retention-days", async () => { writeStaging("report.json"); - writeAgentOutput({ - items: [{ type: "upload_artifact", path: "report.json", retention_days: 999 }], - }); - await runMain(); + await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json", retention_days: 999 }]); expect(mockCore.setFailed).not.toHaveBeenCalled(); expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_retention_days", "30"); @@ -158,9 +136,8 @@ describe("upload_artifact.cjs", () => { it("uses default retention when retention_days is absent", async () => { writeStaging("report.json"); - writeAgentOutput({ items: [{ type: "upload_artifact", path: "report.json" }] }); - await runMain(); + await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json" }]); expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_retention_days", "7"); }); @@ -169,98 +146,67 @@ describe("upload_artifact.cjs", () => { describe("validation errors", () => { it("fails when both path and filters are present", async () => { writeStaging("report.json"); - writeAgentOutput({ - items: [ - { - type: "upload_artifact", - path: "report.json", - filters: { include: ["**/*.json"] }, - }, - ], - }); - - await runMain(); + + await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json", filters: { include: ["**/*.json"] } }]); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("exactly one of 'path' or 'filters'")); }); it("fails when neither path nor filters are present", async () => { - writeAgentOutput({ items: [{ type: "upload_artifact", retention_days: 7 }] }); - - await runMain(); + await runHandler(buildConfig(), [{ type: "upload_artifact", retention_days: 7 }]); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("exactly one of 'path' or 'filters'")); }); it("fails when path traverses outside staging dir", async () => { - writeAgentOutput({ items: [{ type: "upload_artifact", path: "../etc/passwd" }] }); - - await runMain(); + await runHandler(buildConfig(), [{ type: "upload_artifact", path: "../etc/passwd" }]); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("must not traverse outside staging directory")); }); it("fails when absolute path is provided", async () => { - writeAgentOutput({ items: [{ type: "upload_artifact", path: "/etc/passwd" }] }); - - await runMain(); + await runHandler(buildConfig(), [{ type: "upload_artifact", path: "/etc/passwd" }]); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("must be relative")); }); it("fails when path does not exist in staging dir", async () => { - writeAgentOutput({ items: [{ type: "upload_artifact", path: "nonexistent.json" }] }); - - await runMain(); + await runHandler(buildConfig(), [{ type: "upload_artifact", path: "nonexistent.json" }]); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("does not exist in staging directory")); }); it("fails when max-uploads is exceeded", async () => { - process.env.GH_AW_ARTIFACT_MAX_UPLOADS = "1"; writeStaging("a.json"); writeStaging("b.json"); - writeAgentOutput({ - items: [ - { type: "upload_artifact", path: "a.json" }, - { type: "upload_artifact", path: "b.json" }, - ], - }); - - await runMain(); - expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("exceed max-uploads policy")); + + await runHandler(buildConfig({ "max-uploads": 1 }), [ + { type: "upload_artifact", path: "a.json" }, + { type: "upload_artifact", path: "b.json" }, + ]); + + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("exceeded max-uploads policy")); }); it("fails when skip_archive is requested but not allowed", async () => { writeStaging("app.bin"); - writeAgentOutput({ items: [{ type: "upload_artifact", path: "app.bin", skip_archive: true }] }); - await runMain(); + await runHandler(buildConfig(), [{ type: "upload_artifact", path: "app.bin", skip_archive: true }]); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("skip_archive=true is not permitted")); }); it("fails when skip_archive=true with multiple files", async () => { - process.env.GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE = "true"; writeStaging("output/a.json"); writeStaging("output/b.json"); - writeAgentOutput({ - items: [ - { - type: "upload_artifact", - // Use "output/**" which matches output/a.json and output/b.json - filters: { include: ["output/**"] }, - skip_archive: true, - }, - ], - }); - - await runMain(); + + await runHandler(buildConfig({ "allow-skip-archive": true }), [{ type: "upload_artifact", filters: { include: ["output/**"] }, skip_archive: true }]); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("skip_archive=true requires exactly one selected file")); }); }); describe("skip_archive allowed", () => { it("succeeds with skip_archive=true and a single file", async () => { - process.env.GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE = "true"; writeStaging("app.bin", "binary data"); - writeAgentOutput({ items: [{ type: "upload_artifact", path: "app.bin", skip_archive: true }] }); - await runMain(); + await runHandler(buildConfig({ "allow-skip-archive": true }), [{ type: "upload_artifact", path: "app.bin", skip_archive: true }]); expect(mockCore.setFailed).not.toHaveBeenCalled(); expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); @@ -272,19 +218,13 @@ describe("upload_artifact.cjs", () => { writeStaging("reports/daily/summary.json", "{}"); writeStaging("reports/weekly/summary.json", "{}"); writeStaging("reports/private/secret.json", "{}"); - writeAgentOutput({ - items: [ - { - type: "upload_artifact", - filters: { - include: ["reports/**/*.json"], - exclude: ["reports/private/**"], - }, - }, - ], - }); - - await runMain(); + + await runHandler(buildConfig(), [ + { + type: "upload_artifact", + filters: { include: ["reports/**/*.json"], exclude: ["reports/private/**"] }, + }, + ]); expect(mockCore.setFailed).not.toHaveBeenCalled(); expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); @@ -292,33 +232,15 @@ describe("upload_artifact.cjs", () => { }); it("handles no-files with if-no-files=ignore", async () => { - process.env.GH_AW_ARTIFACT_DEFAULT_IF_NO_FILES = "ignore"; - writeAgentOutput({ - items: [ - { - type: "upload_artifact", - filters: { include: ["nonexistent/**"] }, - }, - ], - }); - - await runMain(); + await runHandler(buildConfig({ "default-if-no-files": "ignore" }), [{ type: "upload_artifact", filters: { include: ["nonexistent/**"] } }]); expect(mockCore.setFailed).not.toHaveBeenCalled(); - expect(mockCore.setOutput).toHaveBeenCalledWith("artifact_count", "0"); + // No slot output set since skipped + expect(mockCore.setOutput).not.toHaveBeenCalledWith("slot_0_enabled", "true"); }); it("fails when no files match and if-no-files=error (default)", async () => { - writeAgentOutput({ - items: [ - { - type: "upload_artifact", - filters: { include: ["nonexistent/**"] }, - }, - ], - }); - - await runMain(); + await runHandler(buildConfig(), [{ type: "upload_artifact", filters: { include: ["nonexistent/**"] } }]); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("no files matched")); }); @@ -326,19 +248,22 @@ describe("upload_artifact.cjs", () => { describe("allowed-paths policy", () => { it("filters out files not in allowed-paths", async () => { - process.env.GH_AW_ARTIFACT_ALLOWED_PATHS = JSON.stringify(["dist/**"]); writeStaging("dist/app.js"); writeStaging("secret.env"); - writeAgentOutput({ - items: [ - { - type: "upload_artifact", - filters: { include: ["**"] }, - }, - ], - }); - await runMain(); + await runHandler(buildConfig({ "allowed-paths": ["dist/**"] }), [{ type: "upload_artifact", filters: { include: ["**"] } }]); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_file_count", "1"); + }); + }); + + describe("filters-include / filters-exclude from config", () => { + it("uses config filters-include as default when request has no filters", async () => { + writeStaging("dist/app.js"); + writeStaging("secret.env"); + + await runHandler(buildConfig({ "filters-include": ["dist/**"] }), [{ type: "upload_artifact", filters: {} }]); expect(mockCore.setFailed).not.toHaveBeenCalled(); expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_file_count", "1"); @@ -349,9 +274,8 @@ describe("upload_artifact.cjs", () => { it("skips file staging but sets outputs in staged mode", async () => { process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"; writeStaging("report.json"); - writeAgentOutput({ items: [{ type: "upload_artifact", path: "report.json" }] }); - await runMain(); + await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json" }]); expect(mockCore.setFailed).not.toHaveBeenCalled(); expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); @@ -360,14 +284,25 @@ describe("upload_artifact.cjs", () => { const slotFile = path.join(SLOT_BASE_DIR, "slot_0", "report.json"); expect(fs.existsSync(slotFile)).toBe(false); }); + + it("skips file staging when staged=true in config", async () => { + writeStaging("report.json"); + + await runHandler(buildConfig({ staged: true }), [{ type: "upload_artifact", path: "report.json" }]); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); + + const slotFile = path.join(SLOT_BASE_DIR, "slot_0", "report.json"); + expect(fs.existsSync(slotFile)).toBe(false); + }); }); describe("resolver file", () => { it("writes a resolver mapping with temporary IDs", async () => { writeStaging("report.json"); - writeAgentOutput({ items: [{ type: "upload_artifact", path: "report.json" }] }); - await runMain(); + await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json" }]); expect(fs.existsSync(RESOLVER_FILE)).toBe(true); const resolver = JSON.parse(fs.readFileSync(RESOLVER_FILE, "utf8")); diff --git a/pkg/workflow/compiler_safe_output_jobs.go b/pkg/workflow/compiler_safe_output_jobs.go index dabfa512dfa..f1d6d83c436 100644 --- a/pkg/workflow/compiler_safe_output_jobs.go +++ b/pkg/workflow/compiler_safe_output_jobs.go @@ -89,24 +89,6 @@ func (c *Compiler) buildSafeOutputsJobs(data *WorkflowData, jobName, markdownPat compilerSafeOutputJobsLog.Printf("Added separate upload_assets job") } - // Build upload_artifact job as a separate job if configured. - // This is separate from the consolidated safe_outputs job because it needs to: - // 1. Download the staging artifact produced by the main job - // 2. Validate and filter the requested files - // 3. Upload each approved set of files as a proper GitHub Actions artifact - if data.SafeOutputs != nil && data.SafeOutputs.UploadArtifact != nil { - compilerSafeOutputJobsLog.Print("Building separate upload_artifact job") - uploadArtifactJob, err := c.buildUploadArtifactJob(data, jobName, threatDetectionEnabled) - if err != nil { - return fmt.Errorf("failed to build upload_artifact job: %w", err) - } - if err := c.jobManager.AddJob(uploadArtifactJob); err != nil { - return fmt.Errorf("failed to add upload_artifact job: %w", err) - } - safeOutputJobNames = append(safeOutputJobNames, uploadArtifactJob.Name) - compilerSafeOutputJobsLog.Printf("Added separate upload_artifact job") - } - // Build upload_code_scanning_sarif job as a separate job if create-code-scanning-alert is configured. // This job runs after safe_outputs and only when the safe_outputs job exported a SARIF file. // It is separate to avoid the checkout step (needed to restore HEAD to github.sha) from diff --git a/pkg/workflow/compiler_safe_outputs_config.go b/pkg/workflow/compiler_safe_outputs_config.go index 4bc1638fb2e..6b99336dd86 100644 --- a/pkg/workflow/compiler_safe_outputs_config.go +++ b/pkg/workflow/compiler_safe_outputs_config.go @@ -797,6 +797,14 @@ var handlerRegistry = map[string]handlerBuilder{ b = b.AddIfNotEmpty("default-if-no-files", c.Defaults.IfNoFiles) } } + if c.Filters != nil { + if len(c.Filters.Include) > 0 { + b = b.AddStringSlice("filters-include", c.Filters.Include) + } + if len(c.Filters.Exclude) > 0 { + b = b.AddStringSlice("filters-exclude", c.Filters.Exclude) + } + } return b.Build() }, "autofix_code_scanning_alert": func(cfg *SafeOutputsConfig) map[string]any { diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index a354c458971..7d27262a485 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -161,6 +161,7 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa data.SafeOutputs.MissingData != nil || data.SafeOutputs.AssignToAgent != nil || // assign_to_agent is now handled by the handler manager data.SafeOutputs.CreateAgentSessions != nil || // create_agent_session is now handled by the handler manager + data.SafeOutputs.UploadArtifact != nil || // upload_artifact is handled inline in the handler loop len(data.SafeOutputs.Scripts) > 0 || // Custom scripts run in the handler loop len(data.SafeOutputs.Actions) > 0 // Custom actions need handler to export their payloads @@ -175,7 +176,23 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa steps = append(steps, scriptSetupSteps...) } - // 1. Handler Manager step (processes create_issue, update_issue, add_comment, assign_to_agent, etc.) + // Download the upload-artifact staging artifact before the handler manager runs so that + // the upload_artifact handler (which runs inline in the handler loop) can access the files. + if data.SafeOutputs.UploadArtifact != nil { + consolidatedSafeOutputsJobLog.Print("Adding upload-artifact staging download step") + stagingArtifactName := agentArtifactPrefix + SafeOutputsUploadArtifactStagingArtifactName + steps = append(steps, + " - name: Download upload-artifact staging\n", + " continue-on-error: true\n", + fmt.Sprintf(" uses: %s\n", GetActionPin("actions/download-artifact")), + " with:\n", + fmt.Sprintf(" name: %s\n", stagingArtifactName), + fmt.Sprintf(" path: %s\n", artifactStagingDirExpr), + ) + } + + // 1. Handler Manager step (processes create_issue, update_issue, add_comment, assign_to_agent, + // upload_artifact, etc.) // This processes all safe output types that are handled by the unified handler // Critical for workflows that create projects and then add issues/PRs to those projects if hasHandlerManagerTypes { @@ -210,6 +227,32 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa outputs["create_agent_session_session_url"] = "${{ steps.process_safe_outputs.outputs.session_url }}" } + // Export upload_artifact outputs and add conditional slot upload steps. + // The handler sets slot_N_* outputs on the process_safe_outputs step; we expose + // them as upload_artifact_slot_N_* job outputs for external consumers. + if data.SafeOutputs.UploadArtifact != nil { + consolidatedSafeOutputsJobLog.Print("Adding upload_artifact slot upload steps") + cfg := data.SafeOutputs.UploadArtifact + outputs["upload_artifact_count"] = "${{ steps.process_safe_outputs.outputs.upload_artifact_count }}" + for i := range cfg.MaxUploads { + outputs[fmt.Sprintf("upload_artifact_slot_%d_tmp_id", i)] = fmt.Sprintf("${{ steps.process_safe_outputs.outputs.slot_%d_tmp_id }}", i) + } + // Add one conditional actions/upload-artifact step per MaxUploads slot. + for i := range cfg.MaxUploads { + slotDir := fmt.Sprintf("%sslot_%d/", artifactSlotDirExpr, i) + steps = append(steps, + fmt.Sprintf(" - name: Upload artifact slot %d\n", i), + fmt.Sprintf(" if: steps.process_safe_outputs.outputs.slot_%d_enabled == 'true'\n", i), + fmt.Sprintf(" uses: %s\n", GetActionPin("actions/upload-artifact")), + " with:\n", + fmt.Sprintf(" name: ${{ steps.process_safe_outputs.outputs.slot_%d_name }}\n", i), + fmt.Sprintf(" path: %s\n", slotDir), + fmt.Sprintf(" retention-days: ${{ steps.process_safe_outputs.outputs.slot_%d_retention_days }}\n", i), + " if-no-files-found: ignore\n", + ) + } + } + // If create-issue is configured with assignees: copilot, run a follow-up step to // assign the Copilot coding agent. The handler manager exports the list via // steps.process_safe_outputs.outputs.issues_to_assign_copilot. @@ -346,6 +389,12 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa // Add artifact download steps count insertIndex += len(buildAgentOutputDownloadSteps(agentArtifactPrefix)) + // Add upload-artifact staging download step count. + // The step has 6 YAML string entries: name, continue-on-error, uses, with:, name: , path: + if data.SafeOutputs.UploadArtifact != nil { + insertIndex += 6 + } + // Add patch download steps if present // Download from unified agent artifact (prefixed in workflow_call context) if usesPatchesAndCheckouts(data.SafeOutputs) { diff --git a/pkg/workflow/publish_artifacts.go b/pkg/workflow/publish_artifacts.go index d9427882f44..0a53b70c581 100644 --- a/pkg/workflow/publish_artifacts.go +++ b/pkg/workflow/publish_artifacts.go @@ -2,11 +2,9 @@ package workflow import ( "encoding/json" - "errors" "fmt" "strings" - "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" ) @@ -24,22 +22,12 @@ const defaultArtifactMaxRetentionDays = 30 // defaultArtifactMaxSizeBytes is the default maximum total upload size (100 MB). const defaultArtifactMaxSizeBytes int64 = 104857600 -// artifactStagingDir is the path where the model stages files to be uploaded as artifacts. -// Use the shell-variable form only inside `run:` blocks; for `with: path:` inputs use -// artifactStagingDirExpr which uses the GitHub Actions expression syntax. -const artifactStagingDir = "${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/" - -// artifactStagingDirExpr is the GitHub Actions expression form of artifactStagingDir. +// artifactStagingDirExpr is the GitHub Actions expression form of the staging directory. // `actions/upload-artifact` and `actions/download-artifact` do not expand shell variables // in their `path:` inputs, so we must use ${{ runner.temp }} here. const artifactStagingDirExpr = "${{ runner.temp }}/gh-aw/safeoutputs/upload-artifacts/" -// artifactSlotDir is the per-slot directory used by the handler to organise staged files. -// Use the shell-variable form only inside `run:` blocks; for `with: path:` inputs use -// artifactSlotDirExpr which uses the GitHub Actions expression syntax. -const artifactSlotDir = "${RUNNER_TEMP}/gh-aw/upload-artifacts/" - -// artifactSlotDirExpr is the GitHub Actions expression form of artifactSlotDir. +// artifactSlotDirExpr is the GitHub Actions expression form of the per-slot artifact directory. const artifactSlotDirExpr = "${{ runner.temp }}/gh-aw/upload-artifacts/" // SafeOutputsUploadArtifactStagingArtifactName is the artifact that carries the staging directory @@ -202,142 +190,8 @@ func (c *Compiler) parseUploadArtifactConfig(outputMap map[string]any) *UploadAr return config } -// buildUploadArtifactJob creates the upload_artifact standalone job. -// -// Architecture: -// 1. The model stages files to artifactStagingDir during its run. -// 2. The main agent job uploads that directory as a GitHub Actions staging artifact. -// 3. This job downloads the staging artifact, validates each upload_artifact request, -// copies approved files into per-slot directories, and then uploads each slot using -// actions/upload-artifact with a conditional step per MaxUploads slot. -// 4. A temporary artifact ID is returned for each slot via job outputs. -func (c *Compiler) buildUploadArtifactJob(data *WorkflowData, mainJobName string, threatDetectionEnabled bool) (*Job, error) { - publishArtifactsLog.Printf("Building upload_artifact job: workflow=%s, main_job=%s, threat_detection=%v", - data.Name, mainJobName, threatDetectionEnabled) - - if data.SafeOutputs == nil || data.SafeOutputs.UploadArtifact == nil { - return nil, errors.New("safe-outputs.upload-artifact configuration is required") - } - - cfg := data.SafeOutputs.UploadArtifact - - var preSteps []string - - // Add setup step so scripts are available at SetupActionDestination. - setupActionRef := c.resolveActionReference("./actions/setup", data) - if setupActionRef != "" || c.actionMode.IsScript() { - preSteps = append(preSteps, c.generateCheckoutActionsFolder(data)...) - publishTraceID := fmt.Sprintf("${{ needs.%s.outputs.setup-trace-id }}", constants.ActivationJobName) - preSteps = append(preSteps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, publishTraceID)...) - } - - // Download the staging artifact that contains the files staged by the model. - // The agent output artifact (carrying upload_artifact NDJSON records) is NOT added here - // because buildCustomActionStep / buildGitHubScriptStep already prepends that step - // automatically to every safe-output job. - artifactPrefix := artifactPrefixExprForAgentDownstreamJob(data) - stagingArtifactName := artifactPrefix + SafeOutputsUploadArtifactStagingArtifactName - preSteps = append(preSteps, - " - name: Download upload-artifact staging\n", - " continue-on-error: true\n", - fmt.Sprintf(" uses: %s\n", GetActionPin("actions/download-artifact")), - " with:\n", - fmt.Sprintf(" name: %s\n", stagingArtifactName), - fmt.Sprintf(" path: %s\n", artifactStagingDirExpr), - ) - - // Build custom environment variables consumed by upload_artifact.cjs. - var customEnvVars []string - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_MAX_UPLOADS: %d\n", cfg.MaxUploads)) - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_DEFAULT_RETENTION_DAYS: %d\n", cfg.DefaultRetentionDays)) - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_MAX_RETENTION_DAYS: %d\n", cfg.MaxRetentionDays)) - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_MAX_SIZE_BYTES: %d\n", cfg.MaxSizeBytes)) - - if len(cfg.AllowedPaths) > 0 { - allowedPathsJSON := marshalStringSliceJSON(cfg.AllowedPaths) - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_ALLOWED_PATHS: %q\n", allowedPathsJSON)) - } - - if cfg.Allow != nil && cfg.Allow.SkipArchive { - customEnvVars = append(customEnvVars, " GH_AW_ARTIFACT_ALLOW_SKIP_ARCHIVE: \"true\"\n") - } - if cfg.Defaults != nil { - if cfg.Defaults.SkipArchive { - customEnvVars = append(customEnvVars, " GH_AW_ARTIFACT_DEFAULT_SKIP_ARCHIVE: \"true\"\n") - } - if cfg.Defaults.IfNoFiles != "" { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_DEFAULT_IF_NO_FILES: %q\n", cfg.Defaults.IfNoFiles)) - } - } - if cfg.Filters != nil { - if len(cfg.Filters.Include) > 0 { - filtersIncJSON := marshalStringSliceJSON(cfg.Filters.Include) - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_FILTERS_INCLUDE: %q\n", filtersIncJSON)) - } - if len(cfg.Filters.Exclude) > 0 { - filtersExcJSON := marshalStringSliceJSON(cfg.Filters.Exclude) - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ARTIFACT_FILTERS_EXCLUDE: %q\n", filtersExcJSON)) - } - } - - // Add standard env vars (run ID, repo, etc.). - customEnvVars = append(customEnvVars, c.buildStandardSafeOutputEnvVars(data, "")...) - - // Build conditional actions/upload-artifact steps – one per MaxUploads slot. - // The handler sets slot_N_enabled=true and outputs the slot name / retention when - // the Nth upload_artifact request was successfully validated and staged. - var postSteps []string - for i := range cfg.MaxUploads { - slotDir := fmt.Sprintf("%sslot_%d/", artifactSlotDirExpr, i) - postSteps = append(postSteps, - fmt.Sprintf(" - name: Upload artifact slot %d\n", i), - fmt.Sprintf(" if: steps.upload_artifacts.outputs.slot_%d_enabled == 'true'\n", i), - fmt.Sprintf(" uses: %s\n", GetActionPin("actions/upload-artifact")), - " with:\n", - fmt.Sprintf(" name: ${{ steps.upload_artifacts.outputs.slot_%d_name }}\n", i), - fmt.Sprintf(" path: %s\n", slotDir), - fmt.Sprintf(" retention-days: ${{ steps.upload_artifacts.outputs.slot_%d_retention_days }}\n", i), - " if-no-files-found: ignore\n", - ) - } - - // In dev mode, restore the actions/setup folder so the post-step cleanup succeeds. - if c.actionMode.IsDev() { - postSteps = append(postSteps, c.generateRestoreActionsSetupStep()) - publishArtifactsLog.Print("Added restore actions folder step to upload_artifact job (dev mode)") - } - - jobCondition := BuildSafeOutputType("upload_artifact") - needs := []string{mainJobName, string(constants.ActivationJobName)} - - // Collect job outputs for all slots so downstream jobs can reference them. - outputs := map[string]string{ - "artifact_count": "${{ steps.upload_artifacts.outputs.artifact_count }}", - } - for i := range cfg.MaxUploads { - outputs[fmt.Sprintf("slot_%d_tmp_id", i)] = fmt.Sprintf("${{ steps.upload_artifacts.outputs.slot_%d_tmp_id }}", i) - } - - return c.buildSafeOutputJob(data, SafeOutputJobConfig{ - JobName: "upload_artifact", - StepName: "Upload artifacts", - StepID: "upload_artifacts", - ScriptName: "upload_artifact", - MainJobName: mainJobName, - CustomEnvVars: customEnvVars, - Script: "", - Permissions: NewPermissions(), - Outputs: outputs, - Condition: jobCondition, - PreSteps: preSteps, - PostSteps: postSteps, - Token: cfg.GitHubToken, - Needs: needs, - }) -} - // generateSafeOutputsArtifactStagingUpload generates a step in the main agent job that uploads -// the artifact staging directory so the upload_artifact job can download it. +// the artifact staging directory so the safe_outputs job can download it for inline processing. // This step only appears when upload-artifact is configured in safe-outputs. func generateSafeOutputsArtifactStagingUpload(builder *strings.Builder, data *WorkflowData) { if data.SafeOutputs == nil || data.SafeOutputs.UploadArtifact == nil { diff --git a/pkg/workflow/publish_artifacts_test.go b/pkg/workflow/publish_artifacts_test.go index 7642075d9a8..18f24554523 100644 --- a/pkg/workflow/publish_artifacts_test.go +++ b/pkg/workflow/publish_artifacts_test.go @@ -202,67 +202,6 @@ func TestComputeEnabledToolNamesIncludesUploadArtifact(t *testing.T) { assert.True(t, tools["upload_artifact"], "upload_artifact should be in enabled tools") } -func TestBuildUploadArtifactJobBasicStructure(t *testing.T) { - c := NewCompiler() - data := &WorkflowData{ - Name: "Test Workflow", - SafeOutputs: &SafeOutputsConfig{ - UploadArtifact: &UploadArtifactConfig{ - MaxUploads: 2, - DefaultRetentionDays: 7, - MaxRetentionDays: 30, - MaxSizeBytes: defaultArtifactMaxSizeBytes, - AllowedPaths: []string{"dist/**", "reports/**"}, - }, - }, - } - - job, err := c.buildUploadArtifactJob(data, "agent", false) - require.NoError(t, err, "buildUploadArtifactJob should not return error") - require.NotNil(t, job, "job should not be nil") - - assert.Equal(t, "upload_artifact", job.Name, "job name should be upload_artifact") - - // Convert steps to string for inspection. - var stepsStr strings.Builder - for _, step := range job.Steps { - stepsStr.WriteString(step) - } - s := stepsStr.String() - - assert.Contains(t, s, "Download agent output artifact", "should have agent output download step") - assert.Contains(t, s, "Download upload-artifact staging", "should have staging artifact download step") - assert.Contains(t, s, "GH_AW_ARTIFACT_MAX_UPLOADS", "should have max uploads env var") - assert.Contains(t, s, "GH_AW_ARTIFACT_DEFAULT_RETENTION_DAYS", "should have default retention env var") - assert.Contains(t, s, "GH_AW_ARTIFACT_MAX_RETENTION_DAYS", "should have max retention env var") - assert.Contains(t, s, "GH_AW_ARTIFACT_MAX_SIZE_BYTES", "should have max size bytes env var") - assert.Contains(t, s, "GH_AW_ARTIFACT_ALLOWED_PATHS", "should have allowed paths env var") - - // Should have upload steps for each slot (MaxUploads = 2). - assert.Contains(t, s, "Upload artifact slot 0", "should have upload step for slot 0") - assert.Contains(t, s, "Upload artifact slot 1", "should have upload step for slot 1") - assert.NotContains(t, s, "Upload artifact slot 2", "should NOT have upload step for slot 2") -} - -func TestBuildUploadArtifactJobRequiresConfig(t *testing.T) { - c := NewCompiler() - - t.Run("nil SafeOutputs returns error", func(t *testing.T) { - data := &WorkflowData{Name: "Test", SafeOutputs: nil} - _, err := c.buildUploadArtifactJob(data, "agent", false) - assert.Error(t, err, "should return error when SafeOutputs is nil") - }) - - t.Run("nil UploadArtifact returns error", func(t *testing.T) { - data := &WorkflowData{ - Name: "Test", - SafeOutputs: &SafeOutputsConfig{UploadArtifact: nil}, - } - _, err := c.buildUploadArtifactJob(data, "agent", false) - assert.Error(t, err, "should return error when UploadArtifact is nil") - }) -} - func TestGenerateSafeOutputsArtifactStagingUpload(t *testing.T) { t.Run("generates step when UploadArtifact is configured", func(t *testing.T) { var b strings.Builder From 168441656d3a2db189b9bcf4da776cc331b4b4bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:41:32 +0000 Subject: [PATCH 12/23] feat: use @actions/artifact REST API directly instead of actions/upload-artifact YAML steps Agent-Logs-Url: https://github.com/github/gh-aw/sessions/0b72a716-a0a8-4df0-ac4f-a4e5f6637ea3 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/action.yml | 4 ++ actions/setup/js/package.json | 1 + actions/setup/js/upload_artifact.cjs | 60 +++++++++---------- actions/setup/setup.sh | 35 +++++++++++ pkg/workflow/cache.go | 2 +- pkg/workflow/compiler_activation_job.go | 2 +- pkg/workflow/compiler_main_job.go | 2 +- pkg/workflow/compiler_pre_activation_job.go | 2 +- pkg/workflow/compiler_safe_outputs_job.go | 26 +++----- pkg/workflow/compiler_unlock_job.go | 2 +- pkg/workflow/compiler_yaml_step_generation.go | 11 +++- pkg/workflow/notify_comment.go | 2 +- pkg/workflow/publish_artifacts.go | 3 - pkg/workflow/publish_assets.go | 2 +- pkg/workflow/repo_memory.go | 2 +- pkg/workflow/threat_detection.go | 2 +- 16 files changed, 95 insertions(+), 63 deletions(-) diff --git a/actions/setup/action.yml b/actions/setup/action.yml index fe7ff75aa04..d341d85adf8 100644 --- a/actions/setup/action.yml +++ b/actions/setup/action.yml @@ -10,6 +10,10 @@ inputs: description: 'Install @actions/github for handlers that use a per-handler github-token (creates Octokit via getOctokit)' required: false default: 'false' + safe-output-artifact-client: + description: 'Install @actions/artifact so upload_artifact.cjs can upload GitHub Actions artifacts via REST API directly' + required: false + default: 'false' job-name: description: 'Name of the job being set up. When OTEL_EXPORTER_OTLP_ENDPOINT is configured, a gh-aw..setup span is pushed to the OTLP endpoint.' required: false diff --git a/actions/setup/js/package.json b/actions/setup/js/package.json index 9b1a6bae4ca..3863bc8834d 100644 --- a/actions/setup/js/package.json +++ b/actions/setup/js/package.json @@ -1,5 +1,6 @@ { "devDependencies": { + "@actions/artifact": "^6.0.0", "@actions/core": "^3.0.0", "@actions/exec": "^3.0.0", "@actions/github": "^9.0.0", diff --git a/actions/setup/js/upload_artifact.cjs b/actions/setup/js/upload_artifact.cjs index 440769b9ede..e11619852ed 100644 --- a/actions/setup/js/upload_artifact.cjs +++ b/actions/setup/js/upload_artifact.cjs @@ -4,18 +4,18 @@ /** * upload_artifact handler * - * Validates and stages artifact upload requests emitted by the model via the upload_artifact - * safe output tool. The model must have already copied the files it wants to upload to + * Validates artifact upload requests emitted by the model via the upload_artifact safe output + * tool, then uploads the approved files directly via the @actions/artifact REST API client. + * The model must have already copied the files it wants to upload to * ${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/ before calling the tool. * * This handler follows the per-message handler pattern used by the safe_outputs handler loop. * main(config) returns a per-message handler function that: * 1. Validates the request against the workflow's policy configuration. * 2. Resolves the requested files (path or filter-based) from the staging directory. - * 3. Copies approved files into per-slot directories under ${RUNNER_TEMP}/gh-aw/upload-artifacts/slot_N/. - * 4. Sets step outputs (slot_N_enabled, slot_N_name, etc.) so the wrapping job's - * actions/upload-artifact steps can run conditionally. - * 5. Generates a temporary artifact ID for each slot. + * 3. Uploads the approved files directly via DefaultArtifactClient.uploadArtifact(). + * 4. Sets step outputs (slot_N_tmp_id, upload_artifact_count) for downstream consumers. + * 5. Generates a temporary artifact ID for each upload and writes a resolver file. * * Configuration keys (passed via config parameter from handler manager): * max-uploads - Max number of upload_artifact calls allowed (default: 1) @@ -28,12 +28,11 @@ * default-if-no-files - "error" or "ignore" (default: "error") * filters-include - Array of default include glob patterns * filters-exclude - Array of default exclude glob patterns - * staged - true for staged/dry-run mode + * staged - true for staged/dry-run mode (skips actual upload) */ const fs = require("fs"); const path = require("path"); -const crypto = require("crypto"); const { getErrorMessage } = require("./error_helpers.cjs"); const { globPatternToRegex } = require("./glob_pattern_helpers.cjs"); const { ERR_VALIDATION } = require("./error_codes.cjs"); @@ -41,9 +40,6 @@ const { ERR_VALIDATION } = require("./error_codes.cjs"); /** Staging directory where the model places files to be uploaded. */ const STAGING_DIR = `${process.env.RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/`; -/** Base directory for per-slot artifact staging used by actions/upload-artifact. */ -const SLOT_BASE_DIR = `${process.env.RUNNER_TEMP}/gh-aw/upload-artifacts/`; - /** Prefix for temporary artifact IDs returned to the caller. */ const TEMP_ID_PREFIX = "tmp_artifact_"; @@ -251,22 +247,19 @@ function clampRetention(requested, defaultDays, maxDays) { } /** - * Copy resolved files from STAGING_DIR into the per-slot directory. - * @param {string[]} files - Relative paths from STAGING_DIR - * @param {string} slotDir - Absolute target slot directory + * Create or return the @actions/artifact DefaultArtifactClient. + * global.__createArtifactClient can be set in tests to inject a mock client factory. + * @returns {{ uploadArtifact: (name: string, files: string[], rootDir: string, opts: object) => Promise<{id?: number, size?: number}> }} */ -function stageFilesToSlot(files, slotDir) { - fs.mkdirSync(slotDir, { recursive: true }); - for (const relPath of files) { - const src = path.join(STAGING_DIR, relPath); - const dest = path.join(slotDir, relPath); - fs.mkdirSync(path.dirname(dest), { recursive: true }); - fs.copyFileSync(src, dest); +function getArtifactClient() { + if (typeof global.__createArtifactClient === "function") { + return global.__createArtifactClient(); } + const { DefaultArtifactClient } = require("@actions/artifact"); + return new DefaultArtifactClient(); } /** - * Main handler factory for upload_artifact. * Returns a per-message handler function that processes a single upload_artifact request. * * @param {Object} config - Handler configuration from the safe outputs config @@ -368,18 +361,23 @@ async function main(config = {}) { core.info(`Slot ${i}: artifact="${artifactName}", files=${files.length}, size=${totalSize}B, retention=${retentionDays}d, skip_archive=${skipArchive}, tmp_id=${tmpId}`); if (!isStaged) { - // Stage files into the per-slot directory for the actions/upload-artifact step. - const slotDir = path.join(SLOT_BASE_DIR, `slot_${i}`); - stageFilesToSlot(files, slotDir); - core.info(`Staged ${files.length} file(s) to ${slotDir}`); + // Upload files directly via @actions/artifact REST API. + const absoluteFiles = files.map(f => path.join(STAGING_DIR, f)); + const client = getArtifactClient(); + try { + const uploadResult = await client.uploadArtifact(artifactName, absoluteFiles, STAGING_DIR, { retentionDays }); + core.info(`Uploaded artifact "${artifactName}" (id=${uploadResult.id ?? "n/a"}, size=${uploadResult.size ?? totalSize}B)`); + } catch (err) { + return { + success: false, + error: `${ERR_VALIDATION}: upload_artifact: failed to upload artifact "${artifactName}": ${getErrorMessage(err)}`, + }; + } } else { - core.info(`Staged mode: skipping file staging for slot ${i}`); + core.info(`Staged mode: skipping artifact upload for slot ${i}`); } - // Set step outputs for the conditional actions/upload-artifact steps in the safe_outputs job. - core.setOutput(`slot_${i}_enabled`, "true"); - core.setOutput(`slot_${i}_name`, artifactName); - core.setOutput(`slot_${i}_retention_days`, String(retentionDays)); + // Set step outputs so downstream jobs can reference the tmp ID. core.setOutput(`slot_${i}_tmp_id`, tmpId); core.setOutput(`slot_${i}_file_count`, String(files.length)); core.setOutput(`slot_${i}_size_bytes`, String(totalSize)); diff --git a/actions/setup/setup.sh b/actions/setup/setup.sh index 41b5f10cf35..730b9ff74c0 100755 --- a/actions/setup/setup.sh +++ b/actions/setup/setup.sh @@ -67,6 +67,9 @@ DESTINATION="${INPUT_DESTINATION:-${GH_AW_ROOT}/actions}" # Get safe-output-custom-tokens flag from input (default: false) SAFE_OUTPUT_CUSTOM_TOKENS_ENABLED="${INPUT_SAFE_OUTPUT_CUSTOM_TOKENS:-false}" +# Get safe-output-artifact-client flag from input (default: false) +SAFE_OUTPUT_ARTIFACT_CLIENT_ENABLED="${INPUT_SAFE_OUTPUT_ARTIFACT_CLIENT:-false}" + debug_log "Copying activation files to ${DESTINATION}" debug_log "Safe-output custom tokens support: ${SAFE_OUTPUT_CUSTOM_TOKENS_ENABLED}" @@ -401,6 +404,38 @@ else debug_log "Custom tokens not enabled - skipping @actions/github installation" fi +# Install @actions/artifact package if upload-artifact safe output is configured. +# upload_artifact.cjs uses DefaultArtifactClient to upload via Actions REST API directly. +if [ "${SAFE_OUTPUT_ARTIFACT_CLIENT_ENABLED}" = "true" ]; then + echo "Artifact client enabled - installing @actions/artifact package in ${DESTINATION}..." + cd "${DESTINATION}" + + # Check if npm is available + if ! command -v npm &> /dev/null; then + echo "::error::npm is not available. Cannot install @actions/artifact package." + exit 1 + fi + + # Create a minimal package.json if it doesn't exist + if [ ! -f "package.json" ]; then + echo '{"private": true}' > package.json + fi + + # Install @actions/artifact package + npm install --ignore-scripts --no-save --loglevel=error @actions/artifact@^6.0.0 2>&1 | grep -v "npm WARN" || true + if [ -d "node_modules/@actions/artifact" ]; then + echo "✓ Successfully installed @actions/artifact package" + else + echo "::error::Failed to install @actions/artifact package" + exit 1 + fi + + # Return to original directory + cd - > /dev/null +else + debug_log "Artifact client not enabled - skipping @actions/artifact installation" +fi + # Send OTLP job setup span when configured (non-fatal). # Delegates to action_setup_otlp.cjs (same file used by actions/setup/index.js) # to keep dev/release and script mode behavior in sync. diff --git a/pkg/workflow/cache.go b/pkg/workflow/cache.go index cd1de8e9acb..9a31af64471 100644 --- a/pkg/workflow/cache.go +++ b/pkg/workflow/cache.go @@ -926,7 +926,7 @@ func (c *Compiler) buildUpdateCacheMemoryJob(data *WorkflowData, threatDetection // Cache restore job doesn't need project support // Cache job depends on agent job; reuse the agent's trace ID so all jobs share one OTLP trace cacheTraceID := fmt.Sprintf("${{ needs.%s.outputs.setup-trace-id }}", constants.ActivationJobName) - setupSteps = append(setupSteps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, cacheTraceID)...) + setupSteps = append(setupSteps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, false, cacheTraceID)...) } // Prepend setup steps to all cache steps diff --git a/pkg/workflow/compiler_activation_job.go b/pkg/workflow/compiler_activation_job.go index 83217ec9817..f4b6158da73 100644 --- a/pkg/workflow/compiler_activation_job.go +++ b/pkg/workflow/compiler_activation_job.go @@ -40,7 +40,7 @@ func (c *Compiler) buildActivationJob(data *WorkflowData, preActivationJobCreate if preActivationJobCreated { activationSetupTraceID = fmt.Sprintf("${{ needs.%s.outputs.setup-trace-id }}", constants.PreActivationJobName) } - steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, activationSetupTraceID)...) + steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, false, activationSetupTraceID)...) // Expose the trace ID for cross-job span correlation so downstream jobs can reuse it outputs["setup-trace-id"] = "${{ steps.setup.outputs.trace-id }}" diff --git a/pkg/workflow/compiler_main_job.go b/pkg/workflow/compiler_main_job.go index be63d695a13..43f334592f3 100644 --- a/pkg/workflow/compiler_main_job.go +++ b/pkg/workflow/compiler_main_job.go @@ -28,7 +28,7 @@ func (c *Compiler) buildMainJob(data *WorkflowData, activationJobCreated bool) ( // Main job doesn't need project support (no safe outputs processed here) // Pass activation's trace ID so all agent spans share the same OTLP trace agentTraceID := fmt.Sprintf("${{ needs.%s.outputs.setup-trace-id }}", constants.ActivationJobName) - steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, agentTraceID)...) + steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, false, agentTraceID)...) } // Set runtime paths that depend on RUNNER_TEMP via $GITHUB_ENV. diff --git a/pkg/workflow/compiler_pre_activation_job.go b/pkg/workflow/compiler_pre_activation_job.go index 51069ef63bb..b1a02a75469 100644 --- a/pkg/workflow/compiler_pre_activation_job.go +++ b/pkg/workflow/compiler_pre_activation_job.go @@ -40,7 +40,7 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec // Pre-activation job doesn't need project support (no safe outputs processed here) // Pre-activation generates the root trace ID; activation will reuse it via setup-trace-id output - steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, "")...) + steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, false, "")...) // Determine permissions for pre-activation job var perms *Permissions diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index 7d27262a485..21487a5bd45 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -51,9 +51,11 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa // Enable custom-tokens flag if any safe output uses a per-handler github-token enableCustomTokens := c.hasCustomTokenSafeOutputs(data.SafeOutputs) + // Enable artifact client flag if upload-artifact safe output is configured + enableArtifactClient := data.SafeOutputs != nil && data.SafeOutputs.UploadArtifact != nil // Safe outputs job depends on agent job; reuse the agent's trace ID so all jobs share one OTLP trace safeOutputsTraceID := fmt.Sprintf("${{ needs.%s.outputs.setup-trace-id }}", constants.ActivationJobName) - steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, enableCustomTokens, safeOutputsTraceID)...) + steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, enableCustomTokens, enableArtifactClient, safeOutputsTraceID)...) } // Mask OTLP telemetry headers immediately after setup so authentication tokens cannot @@ -227,30 +229,18 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa outputs["create_agent_session_session_url"] = "${{ steps.process_safe_outputs.outputs.session_url }}" } - // Export upload_artifact outputs and add conditional slot upload steps. + // Export upload_artifact outputs. // The handler sets slot_N_* outputs on the process_safe_outputs step; we expose // them as upload_artifact_slot_N_* job outputs for external consumers. + // The actual artifact uploads are performed directly by the JS handler via + // @actions/artifact REST API — no additional YAML steps are required. if data.SafeOutputs.UploadArtifact != nil { - consolidatedSafeOutputsJobLog.Print("Adding upload_artifact slot upload steps") + consolidatedSafeOutputsJobLog.Print("Exposing upload_artifact outputs from handler manager") cfg := data.SafeOutputs.UploadArtifact outputs["upload_artifact_count"] = "${{ steps.process_safe_outputs.outputs.upload_artifact_count }}" for i := range cfg.MaxUploads { outputs[fmt.Sprintf("upload_artifact_slot_%d_tmp_id", i)] = fmt.Sprintf("${{ steps.process_safe_outputs.outputs.slot_%d_tmp_id }}", i) } - // Add one conditional actions/upload-artifact step per MaxUploads slot. - for i := range cfg.MaxUploads { - slotDir := fmt.Sprintf("%sslot_%d/", artifactSlotDirExpr, i) - steps = append(steps, - fmt.Sprintf(" - name: Upload artifact slot %d\n", i), - fmt.Sprintf(" if: steps.process_safe_outputs.outputs.slot_%d_enabled == 'true'\n", i), - fmt.Sprintf(" uses: %s\n", GetActionPin("actions/upload-artifact")), - " with:\n", - fmt.Sprintf(" name: ${{ steps.process_safe_outputs.outputs.slot_%d_name }}\n", i), - fmt.Sprintf(" path: %s\n", slotDir), - fmt.Sprintf(" retention-days: ${{ steps.process_safe_outputs.outputs.slot_%d_retention_days }}\n", i), - " if-no-files-found: ignore\n", - ) - } } // If create-issue is configured with assignees: copilot, run a follow-up step to @@ -383,7 +373,7 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa insertIndex += len(c.generateCheckoutActionsFolder(data)) // Use the same traceID as the real call so the line count matches exactly countTraceID := fmt.Sprintf("${{ needs.%s.outputs.setup-trace-id }}", constants.ActivationJobName) - insertIndex += len(c.generateSetupStep(setupActionRef, SetupActionDestination, c.hasCustomTokenSafeOutputs(data.SafeOutputs), countTraceID)) + insertIndex += len(c.generateSetupStep(setupActionRef, SetupActionDestination, c.hasCustomTokenSafeOutputs(data.SafeOutputs), data.SafeOutputs != nil && data.SafeOutputs.UploadArtifact != nil, countTraceID)) } // Add artifact download steps count diff --git a/pkg/workflow/compiler_unlock_job.go b/pkg/workflow/compiler_unlock_job.go index 0c3c7c5d072..4bdbdef51ef 100644 --- a/pkg/workflow/compiler_unlock_job.go +++ b/pkg/workflow/compiler_unlock_job.go @@ -39,7 +39,7 @@ func (c *Compiler) buildUnlockJob(data *WorkflowData, threatDetectionEnabled boo // Unlock job doesn't need project support // Unlock job depends on activation, reuse its trace ID unlockTraceID := fmt.Sprintf("${{ needs.%s.outputs.setup-trace-id }}", constants.ActivationJobName) - steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, unlockTraceID)...) + steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, false, unlockTraceID)...) // Add unlock step // Build condition: only unlock if issue was locked by activation job diff --git a/pkg/workflow/compiler_yaml_step_generation.go b/pkg/workflow/compiler_yaml_step_generation.go index fbe8be26de0..58ae0b3f83a 100644 --- a/pkg/workflow/compiler_yaml_step_generation.go +++ b/pkg/workflow/compiler_yaml_step_generation.go @@ -111,10 +111,11 @@ func (c *Compiler) generateRestoreActionsSetupStep() string { // - setupActionRef: The action reference for setup action (e.g., "./actions/setup" or "github/gh-aw/actions/setup@sha") // - destination: The destination path where files should be copied (e.g., SetupActionDestination) // - enableCustomTokens: Whether to enable custom-token support (installs @actions/github so handler_auth.cjs can create per-handler Octokit clients) +// - enableArtifactClient: Whether to install @actions/artifact so upload_artifact.cjs can upload via REST API directly // - traceID: Optional OTLP trace ID expression for cross-job span correlation (e.g., "${{ needs.activation.outputs.setup-trace-id }}"). Empty string means a new trace ID is generated. // // Returns a slice of strings representing the YAML lines for the setup step. -func (c *Compiler) generateSetupStep(setupActionRef string, destination string, enableCustomTokens bool, traceID string) []string { +func (c *Compiler) generateSetupStep(setupActionRef string, destination string, enableCustomTokens bool, enableArtifactClient bool, traceID string) []string { // Script mode: run the setup.sh script directly if c.actionMode.IsScript() { lines := []string{ @@ -132,11 +133,14 @@ func (c *Compiler) generateSetupStep(setupActionRef string, destination string, if enableCustomTokens { lines = append(lines, " INPUT_SAFE_OUTPUT_CUSTOM_TOKENS: 'true'\n") } + if enableArtifactClient { + lines = append(lines, " INPUT_SAFE_OUTPUT_ARTIFACT_CLIENT: 'true'\n") + } return lines } // Dev/Release mode: use the setup action - compilerYamlStepGenerationLog.Printf("Generating setup step: ref=%s, destination=%s, customTokens=%t, traceID=%q", setupActionRef, destination, enableCustomTokens, traceID) + compilerYamlStepGenerationLog.Printf("Generating setup step: ref=%s, destination=%s, customTokens=%t, artifactClient=%t, traceID=%q", setupActionRef, destination, enableCustomTokens, enableArtifactClient, traceID) lines := []string{ " - name: Setup Scripts\n", " id: setup\n", @@ -151,6 +155,9 @@ func (c *Compiler) generateSetupStep(setupActionRef string, destination string, if enableCustomTokens { lines = append(lines, " safe-output-custom-tokens: 'true'\n") } + if enableArtifactClient { + lines = append(lines, " safe-output-artifact-client: 'true'\n") + } return lines } diff --git a/pkg/workflow/notify_comment.go b/pkg/workflow/notify_comment.go index 104d494ace3..22b68a7b427 100644 --- a/pkg/workflow/notify_comment.go +++ b/pkg/workflow/notify_comment.go @@ -45,7 +45,7 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa // Notify comment job doesn't need project support // Conclusion/notify job depends on activation, reuse its trace ID notifyTraceID := fmt.Sprintf("${{ needs.%s.outputs.setup-trace-id }}", constants.ActivationJobName) - steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, notifyTraceID)...) + steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, false, notifyTraceID)...) } // Add GitHub App token minting step if app is configured diff --git a/pkg/workflow/publish_artifacts.go b/pkg/workflow/publish_artifacts.go index 0a53b70c581..215bed2785d 100644 --- a/pkg/workflow/publish_artifacts.go +++ b/pkg/workflow/publish_artifacts.go @@ -27,9 +27,6 @@ const defaultArtifactMaxSizeBytes int64 = 104857600 // in their `path:` inputs, so we must use ${{ runner.temp }} here. const artifactStagingDirExpr = "${{ runner.temp }}/gh-aw/safeoutputs/upload-artifacts/" -// artifactSlotDirExpr is the GitHub Actions expression form of the per-slot artifact directory. -const artifactSlotDirExpr = "${{ runner.temp }}/gh-aw/upload-artifacts/" - // SafeOutputsUploadArtifactStagingArtifactName is the artifact that carries the staging directory // from the main agent job to the upload_artifact job. const SafeOutputsUploadArtifactStagingArtifactName = "safe-outputs-upload-artifacts" diff --git a/pkg/workflow/publish_assets.go b/pkg/workflow/publish_assets.go index 83037af629b..3d429ca46e2 100644 --- a/pkg/workflow/publish_assets.go +++ b/pkg/workflow/publish_assets.go @@ -106,7 +106,7 @@ func (c *Compiler) buildUploadAssetsJob(data *WorkflowData, mainJobName string, // Publish assets job doesn't need project support // Publish assets job depends on the agent job; reuse its trace ID so all jobs share one OTLP trace publishTraceID := fmt.Sprintf("${{ needs.%s.outputs.setup-trace-id }}", constants.ActivationJobName) - preSteps = append(preSteps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, publishTraceID)...) + preSteps = append(preSteps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, false, publishTraceID)...) } // Step 1: Checkout repository diff --git a/pkg/workflow/repo_memory.go b/pkg/workflow/repo_memory.go index c073d86c46b..897598e0d59 100644 --- a/pkg/workflow/repo_memory.go +++ b/pkg/workflow/repo_memory.go @@ -605,7 +605,7 @@ func (c *Compiler) buildPushRepoMemoryJob(data *WorkflowData, threatDetectionEna // Repo memory job doesn't need project support // Repo memory job depends on agent job; reuse the agent's trace ID so all jobs share one OTLP trace repoMemoryTraceID := fmt.Sprintf("${{ needs.%s.outputs.setup-trace-id }}", constants.ActivationJobName) - steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, repoMemoryTraceID)...) + steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, false, repoMemoryTraceID)...) } // Add checkout step to configure git (without checking out files) diff --git a/pkg/workflow/threat_detection.go b/pkg/workflow/threat_detection.go index b23831bd461..5c3b47d2b94 100644 --- a/pkg/workflow/threat_detection.go +++ b/pkg/workflow/threat_detection.go @@ -659,7 +659,7 @@ func (c *Compiler) buildDetectionJob(data *WorkflowData) (*Job, error) { steps = append(steps, c.generateCheckoutActionsFolder(data)...) // Detection job depends on agent job; reuse the agent's trace ID so all jobs share one OTLP trace detectionTraceID := fmt.Sprintf("${{ needs.%s.outputs.setup-trace-id }}", constants.ActivationJobName) - steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, detectionTraceID)...) + steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false, false, detectionTraceID)...) } // Download agent output artifact to access output files (prompt.txt, agent_output.json, patches). From f0d89405e68092181fb2e13a37c02be4e6eae4a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:58:26 +0000 Subject: [PATCH 13/23] fix: address review comments - fix range loop, update test mocks, add @actions/artifact dep Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3043f63d-5896-4b6f-b6e0-9d2b770ea81a Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/upload_artifact.test.cjs | 105 +++++++++++++--------- pkg/workflow/compiler_safe_outputs_job.go | 2 +- 2 files changed, 65 insertions(+), 42 deletions(-) diff --git a/actions/setup/js/upload_artifact.test.cjs b/actions/setup/js/upload_artifact.test.cjs index 4b6b3d39c4d..5107f4aa96a 100644 --- a/actions/setup/js/upload_artifact.test.cjs +++ b/actions/setup/js/upload_artifact.test.cjs @@ -2,7 +2,6 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import fs from "fs"; import path from "path"; -import os from "os"; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); @@ -11,11 +10,11 @@ const __dirname = path.dirname(__filename); // Use RUNNER_TEMP as the base so paths match what upload_artifact.cjs computes at runtime. const RUNNER_TEMP = "/tmp"; const STAGING_DIR = `${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/`; -const SLOT_BASE_DIR = `${RUNNER_TEMP}/gh-aw/upload-artifacts/`; const RESOLVER_FILE = `${RUNNER_TEMP}/gh-aw/artifact-resolver.json`; describe("upload_artifact.cjs", () => { let mockCore; + let mockArtifactClient; let originalEnv; /** @@ -29,7 +28,7 @@ describe("upload_artifact.cjs", () => { } /** - * Build a config object (replaces ENV vars in the old standalone approach). + * Build a config object. * @param {object} overrides */ function buildConfig(overrides = {}) { @@ -43,22 +42,22 @@ describe("upload_artifact.cjs", () => { } /** - * Run the handler against a list of messages using the new per-message pattern. - * Simulates what the handler manager does. + * Run the handler against a list of messages using the per-message handler pattern. + * Injects global.__createArtifactClient so tests never hit the real REST API. * @param {object} config * @param {object[]} messages - * @returns {Promise} results from each message handler call + * @returns {Promise} */ async function runHandler(config, messages) { const scriptText = fs.readFileSync(path.join(__dirname, "upload_artifact.cjs"), "utf8"); global.core = mockCore; + global.__createArtifactClient = () => mockArtifactClient; let handlerFn; await eval(`(async () => { ${scriptText}; handlerFn = await main(config); })()`); const results = []; for (const msg of messages) { const result = await handlerFn(msg, {}, new Map()); results.push(result); - // Simulate handler manager calling setFailed on failure if (result && result.success === false && !result.skipped) { mockCore.setFailed(result.error); } @@ -82,6 +81,10 @@ describe("upload_artifact.cjs", () => { }, }; + mockArtifactClient = { + uploadArtifact: vi.fn().mockResolvedValue({ id: 42, size: 100 }), + }; + originalEnv = { ...process.env }; // Set RUNNER_TEMP so the script resolves paths to the same directories as the test helpers. @@ -94,11 +97,6 @@ describe("upload_artifact.cjs", () => { } fs.mkdirSync(STAGING_DIR, { recursive: true }); - // Clean slot dir - if (fs.existsSync(SLOT_BASE_DIR)) { - fs.rmSync(SLOT_BASE_DIR, { recursive: true }); - } - // Clean resolver file if (fs.existsSync(RESOLVER_FILE)) { fs.unlinkSync(RESOLVER_FILE); @@ -107,22 +105,24 @@ describe("upload_artifact.cjs", () => { afterEach(() => { process.env = originalEnv; + delete global.__createArtifactClient; }); describe("path-based upload", () => { - it("stages a single file and sets slot outputs", async () => { + it("uploads a single file via artifact client", async () => { writeStaging("report.json", '{"result": "ok"}'); - await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json", retention_days: 14 }]); + const results = await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json", retention_days: 14 }]); expect(mockCore.setFailed).not.toHaveBeenCalled(); - expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); - expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_retention_days", "14"); + expect(results[0].success).toBe(true); + expect(mockArtifactClient.uploadArtifact).toHaveBeenCalledOnce(); + const [name, files, rootDir, opts] = mockArtifactClient.uploadArtifact.mock.calls[0]; + expect(name).toBe("report.json"); + expect(files).toContain(path.join(STAGING_DIR, "report.json")); + expect(rootDir).toBe(STAGING_DIR); + expect(opts.retentionDays).toBe(14); expect(mockCore.setOutput).toHaveBeenCalledWith("upload_artifact_count", "1"); - - // Verify the file was staged into slot_0. - const slotFile = path.join(SLOT_BASE_DIR, "slot_0", "report.json"); - expect(fs.existsSync(slotFile)).toBe(true); }); it("clamps retention days to max-retention-days", async () => { @@ -131,7 +131,8 @@ describe("upload_artifact.cjs", () => { await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json", retention_days: 999 }]); expect(mockCore.setFailed).not.toHaveBeenCalled(); - expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_retention_days", "30"); + const [, , , opts] = mockArtifactClient.uploadArtifact.mock.calls[0]; + expect(opts.retentionDays).toBe(30); }); it("uses default retention when retention_days is absent", async () => { @@ -139,7 +140,8 @@ describe("upload_artifact.cjs", () => { await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json" }]); - expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_retention_days", "7"); + const [, , , opts] = mockArtifactClient.uploadArtifact.mock.calls[0]; + expect(opts.retentionDays).toBe(7); }); }); @@ -150,38 +152,46 @@ describe("upload_artifact.cjs", () => { await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json", filters: { include: ["**/*.json"] } }]); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("exactly one of 'path' or 'filters'")); + expect(mockArtifactClient.uploadArtifact).not.toHaveBeenCalled(); }); it("fails when neither path nor filters are present", async () => { await runHandler(buildConfig(), [{ type: "upload_artifact", retention_days: 7 }]); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("exactly one of 'path' or 'filters'")); + expect(mockArtifactClient.uploadArtifact).not.toHaveBeenCalled(); }); it("fails when path traverses outside staging dir", async () => { await runHandler(buildConfig(), [{ type: "upload_artifact", path: "../etc/passwd" }]); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("must not traverse outside staging directory")); + expect(mockArtifactClient.uploadArtifact).not.toHaveBeenCalled(); }); it("fails when absolute path is provided", async () => { await runHandler(buildConfig(), [{ type: "upload_artifact", path: "/etc/passwd" }]); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("must be relative")); + expect(mockArtifactClient.uploadArtifact).not.toHaveBeenCalled(); }); it("fails when path does not exist in staging dir", async () => { await runHandler(buildConfig(), [{ type: "upload_artifact", path: "nonexistent.json" }]); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("does not exist in staging directory")); + expect(mockArtifactClient.uploadArtifact).not.toHaveBeenCalled(); }); it("fails when max-uploads is exceeded", async () => { writeStaging("a.json"); writeStaging("b.json"); - await runHandler(buildConfig({ "max-uploads": 1 }), [ + const results = await runHandler(buildConfig({ "max-uploads": 1 }), [ { type: "upload_artifact", path: "a.json" }, { type: "upload_artifact", path: "b.json" }, ]); + expect(results[0].success).toBe(true); + expect(results[1].success).toBe(false); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("exceeded max-uploads policy")); + expect(mockArtifactClient.uploadArtifact).toHaveBeenCalledOnce(); }); it("fails when skip_archive is requested but not allowed", async () => { @@ -190,6 +200,7 @@ describe("upload_artifact.cjs", () => { await runHandler(buildConfig(), [{ type: "upload_artifact", path: "app.bin", skip_archive: true }]); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("skip_archive=true is not permitted")); + expect(mockArtifactClient.uploadArtifact).not.toHaveBeenCalled(); }); it("fails when skip_archive=true with multiple files", async () => { @@ -199,6 +210,17 @@ describe("upload_artifact.cjs", () => { await runHandler(buildConfig({ "allow-skip-archive": true }), [{ type: "upload_artifact", filters: { include: ["output/**"] }, skip_archive: true }]); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("skip_archive=true requires exactly one selected file")); + expect(mockArtifactClient.uploadArtifact).not.toHaveBeenCalled(); + }); + + it("fails when upload client throws", async () => { + writeStaging("report.json"); + mockArtifactClient.uploadArtifact.mockRejectedValue(new Error("network failure")); + + const results = await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json" }]); + + expect(results[0].success).toBe(false); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("network failure")); }); }); @@ -206,10 +228,11 @@ describe("upload_artifact.cjs", () => { it("succeeds with skip_archive=true and a single file", async () => { writeStaging("app.bin", "binary data"); - await runHandler(buildConfig({ "allow-skip-archive": true }), [{ type: "upload_artifact", path: "app.bin", skip_archive: true }]); + const results = await runHandler(buildConfig({ "allow-skip-archive": true }), [{ type: "upload_artifact", path: "app.bin", skip_archive: true }]); expect(mockCore.setFailed).not.toHaveBeenCalled(); - expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); + expect(results[0].success).toBe(true); + expect(mockArtifactClient.uploadArtifact).toHaveBeenCalledOnce(); }); }); @@ -227,7 +250,9 @@ describe("upload_artifact.cjs", () => { ]); expect(mockCore.setFailed).not.toHaveBeenCalled(); - expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); + expect(mockArtifactClient.uploadArtifact).toHaveBeenCalledOnce(); + const [, files] = mockArtifactClient.uploadArtifact.mock.calls[0]; + expect(files).toHaveLength(2); expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_file_count", "2"); }); @@ -235,14 +260,14 @@ describe("upload_artifact.cjs", () => { await runHandler(buildConfig({ "default-if-no-files": "ignore" }), [{ type: "upload_artifact", filters: { include: ["nonexistent/**"] } }]); expect(mockCore.setFailed).not.toHaveBeenCalled(); - // No slot output set since skipped - expect(mockCore.setOutput).not.toHaveBeenCalledWith("slot_0_enabled", "true"); + expect(mockArtifactClient.uploadArtifact).not.toHaveBeenCalled(); }); it("fails when no files match and if-no-files=error (default)", async () => { await runHandler(buildConfig(), [{ type: "upload_artifact", filters: { include: ["nonexistent/**"] } }]); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("no files matched")); + expect(mockArtifactClient.uploadArtifact).not.toHaveBeenCalled(); }); }); @@ -254,6 +279,8 @@ describe("upload_artifact.cjs", () => { await runHandler(buildConfig({ "allowed-paths": ["dist/**"] }), [{ type: "upload_artifact", filters: { include: ["**"] } }]); expect(mockCore.setFailed).not.toHaveBeenCalled(); + const [, files] = mockArtifactClient.uploadArtifact.mock.calls[0]; + expect(files).toHaveLength(1); expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_file_count", "1"); }); }); @@ -271,30 +298,26 @@ describe("upload_artifact.cjs", () => { }); describe("staged mode", () => { - it("skips file staging but sets outputs in staged mode", async () => { + it("skips upload client call in staged mode (env var)", async () => { process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"; writeStaging("report.json"); - await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json" }]); + const results = await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json" }]); expect(mockCore.setFailed).not.toHaveBeenCalled(); - expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); - - // In staged mode, files are NOT copied to the slot directory. - const slotFile = path.join(SLOT_BASE_DIR, "slot_0", "report.json"); - expect(fs.existsSync(slotFile)).toBe(false); + expect(results[0].success).toBe(true); + expect(mockArtifactClient.uploadArtifact).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_tmp_id", expect.stringMatching(/^tmp_artifact_[A-Z0-9]{26}$/)); }); - it("skips file staging when staged=true in config", async () => { + it("skips upload client call when staged=true in config", async () => { writeStaging("report.json"); - await runHandler(buildConfig({ staged: true }), [{ type: "upload_artifact", path: "report.json" }]); + const results = await runHandler(buildConfig({ staged: true }), [{ type: "upload_artifact", path: "report.json" }]); expect(mockCore.setFailed).not.toHaveBeenCalled(); - expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_enabled", "true"); - - const slotFile = path.join(SLOT_BASE_DIR, "slot_0", "report.json"); - expect(fs.existsSync(slotFile)).toBe(false); + expect(results[0].success).toBe(true); + expect(mockArtifactClient.uploadArtifact).not.toHaveBeenCalled(); }); }); diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index 21487a5bd45..9f97f05fcc5 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -238,7 +238,7 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa consolidatedSafeOutputsJobLog.Print("Exposing upload_artifact outputs from handler manager") cfg := data.SafeOutputs.UploadArtifact outputs["upload_artifact_count"] = "${{ steps.process_safe_outputs.outputs.upload_artifact_count }}" - for i := range cfg.MaxUploads { + for i := 0; i < cfg.MaxUploads; i++ { outputs[fmt.Sprintf("upload_artifact_slot_%d_tmp_id", i)] = fmt.Sprintf("${{ steps.process_safe_outputs.outputs.slot_%d_tmp_id }}", i) } } From 6b2777f05ac55e5b80888597af847d8857edd799 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:01:48 +0000 Subject: [PATCH 14/23] merge: merge main and recompile all workflows Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3043f63d-5896-4b6f-b6e0-9d2b770ea81a Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot.lock.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index e7e639e4a1e..cb321fa0e36 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -2262,6 +2262,7 @@ jobs: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + safe-output-artifact-client: 'true' - name: Mask OTLP telemetry headers run: echo '::add-mask::'"$OTEL_EXPORTER_OTLP_HEADERS" - name: Download agent output artifact @@ -2310,14 +2311,6 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Upload artifact slot 0 - if: steps.process_safe_outputs.outputs.slot_0_enabled == 'true' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: ${{ steps.process_safe_outputs.outputs.slot_0_name }} - path: ${{ runner.temp }}/gh-aw/upload-artifacts/slot_0/ - retention-days: ${{ steps.process_safe_outputs.outputs.slot_0_retention_days }} - if-no-files-found: ignore - name: Upload Safe Output Items if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 From 36c1694b189fa0e4d7771fd7a2ad456d9d02f70a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:27:48 +0000 Subject: [PATCH 15/23] merge: merge main and recompile all 182 workflows Agent-Logs-Url: https://github.com/github/gh-aw/sessions/a3c9dd32-79a6-4ba6-be75-d6e916037047 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot.lock.yml | 68 +++++++++++++++--------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 4f7971a6356..87fb89a9d6f 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"6d0a385e47ce5ed241f4358e1578525037722f288b64d3dc18289d01bd352fbd","agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"2a931073663f42902da7a9ca2f3f56370ad310f3e6bbcf1308329503eeabccd9","agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"668228422ae6a00e4ad889ee87cd7109ec5666a7","version":"v5.0.4"},{"repo":"actions/cache/save","sha":"668228422ae6a00e4ad889ee87cd7109ec5666a7","version":"v5.0.4"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"ed597411d8f924073f98dfc5c65a23a2325f34cd","version":"v8"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"bbbca2ddaa5d8feaa63e36b76fdaad77386f024f","version":"v7"},{"repo":"docker/build-push-action","sha":"d08e5c354a6adb9ed34480a06d141179aa583294","version":"v7"},{"repo":"docker/setup-buildx-action","sha":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd","version":"v4"}]} # ___ _ _ # / _ \ | | (_) @@ -231,9 +231,9 @@ jobs: run: | bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh { - cat << 'GH_AW_PROMPT_2d91fec7281e9c47_EOF' + cat << 'GH_AW_PROMPT_9896dd1a279d5d86_EOF' - GH_AW_PROMPT_2d91fec7281e9c47_EOF + GH_AW_PROMPT_9896dd1a279d5d86_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" @@ -241,7 +241,7 @@ jobs: cat "${RUNNER_TEMP}/gh-aw/prompts/agentic_workflows_guide.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_2d91fec7281e9c47_EOF' + cat << 'GH_AW_PROMPT_9896dd1a279d5d86_EOF' Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), add_labels, remove_labels, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message @@ -273,9 +273,9 @@ jobs: {{/if}} - GH_AW_PROMPT_2d91fec7281e9c47_EOF + GH_AW_PROMPT_9896dd1a279d5d86_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_2d91fec7281e9c47_EOF' + cat << 'GH_AW_PROMPT_9896dd1a279d5d86_EOF' ## Serena Code Analysis @@ -315,7 +315,7 @@ jobs: {{#runtime-import .github/workflows/shared/mcp/serena-go.md}} {{#runtime-import .github/workflows/shared/observability-otlp.md}} {{#runtime-import .github/workflows/smoke-copilot.md}} - GH_AW_PROMPT_2d91fec7281e9c47_EOF + GH_AW_PROMPT_9896dd1a279d5d86_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -580,9 +580,9 @@ jobs: mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_8c3103569671ea37_EOF' - {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-copilot"],"allowed_repos":["github/gh-aw"]},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_8c3103569671ea37_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_37135a487e85aeac_EOF' + {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-copilot"],"allowed_repos":["github/gh-aw"]},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"allow-skip-archive":true,"default-retention-days":1,"max-retention-days":1,"max-size-bytes":104857600,"max-uploads":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_37135a487e85aeac_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -979,7 +979,7 @@ jobs: - name: Write MCP Scripts Config run: | mkdir -p ${RUNNER_TEMP}/gh-aw/mcp-scripts/logs - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/tools.json << 'GH_AW_MCP_SCRIPTS_TOOLS_7babc89e6d790778_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/tools.json << 'GH_AW_MCP_SCRIPTS_TOOLS_d58c0e40e52491a9_EOF' { "serverName": "mcpscripts", "version": "1.0.0", @@ -1095,8 +1095,8 @@ jobs: } ] } - GH_AW_MCP_SCRIPTS_TOOLS_7babc89e6d790778_EOF - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs << 'GH_AW_MCP_SCRIPTS_SERVER_ef1fbc7ce3eca295_EOF' + GH_AW_MCP_SCRIPTS_TOOLS_d58c0e40e52491a9_EOF + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs << 'GH_AW_MCP_SCRIPTS_SERVER_dd0c3af6b77b1bf9_EOF' const path = require("path"); const { startHttpServer } = require("./mcp_scripts_mcp_server_http.cjs"); const configPath = path.join(__dirname, "tools.json"); @@ -1110,12 +1110,12 @@ jobs: console.error("Failed to start mcp-scripts HTTP server:", error); process.exit(1); }); - GH_AW_MCP_SCRIPTS_SERVER_ef1fbc7ce3eca295_EOF + GH_AW_MCP_SCRIPTS_SERVER_dd0c3af6b77b1bf9_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs - name: Write MCP Scripts Tool Files run: | - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/gh.sh << 'GH_AW_MCP_SCRIPTS_SH_GH_5a6688685d632c08_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/gh.sh << 'GH_AW_MCP_SCRIPTS_SH_GH_413a2d9b16bce3b7_EOF' #!/bin/bash # Auto-generated mcp-script tool: gh # Execute any gh CLI command. This tool is accessible as 'mcpscripts-gh'. Provide the full command after 'gh' (e.g., args: 'pr list --limit 5'). The tool will run: gh . Use single quotes ' for complex args to avoid shell interpretation issues. @@ -1126,9 +1126,9 @@ jobs: echo " token: ${GH_AW_GH_TOKEN:0:6}..." GH_TOKEN="$GH_AW_GH_TOKEN" gh $INPUT_ARGS - GH_AW_MCP_SCRIPTS_SH_GH_5a6688685d632c08_EOF + GH_AW_MCP_SCRIPTS_SH_GH_413a2d9b16bce3b7_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/gh.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-discussion-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_acccc7340415fad4_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-discussion-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_ecb08d56af922c60_EOF' #!/bin/bash # Auto-generated mcp-script tool: github-discussion-query # Query GitHub discussions with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. @@ -1263,9 +1263,9 @@ jobs: EOF fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_acccc7340415fad4_EOF + GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_ecb08d56af922c60_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-discussion-query.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-issue-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_a6eacbb65c40c0ed_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-issue-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_b2c3240691c382a4_EOF' #!/bin/bash # Auto-generated mcp-script tool: github-issue-query # Query GitHub issues with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. @@ -1344,9 +1344,9 @@ jobs: fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_a6eacbb65c40c0ed_EOF + GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_b2c3240691c382a4_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-issue-query.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-pr-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_cba8eb127506e4a8_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-pr-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_5cd7ef183044e7f8_EOF' #!/bin/bash # Auto-generated mcp-script tool: github-pr-query # Query GitHub pull requests with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. @@ -1431,7 +1431,7 @@ jobs: fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_cba8eb127506e4a8_EOF + GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_5cd7ef183044e7f8_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-pr-query.sh - name: Generate MCP Scripts Server Config @@ -1507,7 +1507,7 @@ jobs: if [ -n "${OTEL_EXPORTER_OTLP_HEADERS:-}" ]; then _GH_AW_OTLP_HEADERS_JSON=$(node -e 'const h=process.env["OTEL_EXPORTER_OTLP_HEADERS"]||"";const o={};h.split(",").forEach(function(p){const i=p.indexOf("=");if(i>0)o[p.slice(0,i).trim()]=p.slice(i+1).trim();});console.log(JSON.stringify(o));' 2>/dev/null || echo "{}") fi - cat << GH_AW_MCP_CONFIG_8d31e9e79e8b0709_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh + cat << GH_AW_MCP_CONFIG_b2fa325b88dbf094_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh { "mcpServers": { "agenticworkflows": { @@ -1633,7 +1633,7 @@ jobs: } } } - GH_AW_MCP_CONFIG_8d31e9e79e8b0709_EOF + GH_AW_MCP_CONFIG_b2fa325b88dbf094_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -1830,6 +1830,15 @@ jobs: with: name: cache-memory path: /tmp/gh-aw/cache-memory + # Upload safe-outputs upload-artifact staging for the upload_artifact job + - name: Upload Upload-Artifact Staging + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-outputs-upload-artifacts + path: ${{ runner.temp }}/gh-aw/safeoutputs/upload-artifacts/ + retention-days: 1 + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -2237,6 +2246,8 @@ jobs: created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + upload_artifact_count: ${{ steps.process_safe_outputs.outputs.upload_artifact_count }} + upload_artifact_slot_0_tmp_id: ${{ steps.process_safe_outputs.outputs.slot_0_tmp_id }} steps: - name: Checkout actions folder uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -2252,6 +2263,7 @@ jobs: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + safe-output-artifact-client: 'true' - name: Mask OTLP telemetry headers run: echo '::add-mask::'"$OTEL_EXPORTER_OTLP_HEADERS" - name: Download agent output artifact @@ -2277,6 +2289,12 @@ jobs: GH_HOST="${GITHUB_SERVER_URL#https://}" GH_HOST="${GH_HOST#http://}" echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Download upload-artifact staging + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: safe-outputs-upload-artifacts + path: ${{ runner.temp }}/gh-aw/safeoutputs/upload-artifacts/ - name: Process Safe Outputs id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -2286,7 +2304,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot\"],\"allowed_repos\":[\"github/gh-aw\"]},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot\"],\"allowed_repos\":[\"github/gh-aw\"]},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"allow-skip-archive\":true,\"default-retention-days\":1,\"max-retention-days\":1,\"max-size-bytes\":104857600,\"max-uploads\":1}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | From 221a3fa50345acd279c497af08b7d834a4efb862 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:52:06 +0000 Subject: [PATCH 16/23] fix: add rw mount and pre-create staging dir for upload_artifact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ${RUNNER_TEMP}/gh-aw directory is mounted read-only in the agent container (awf --mount :ro), preventing the model from writing staged files to upload-artifacts/. Fix by: 1. Pre-creating ${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts/ before awf starts (mcp_setup_generator.go) so it can be bind-mounted 2. Adding --mount :rw for that subdirectory in the awf command (awf_helpers.go) — child mount overrides parent :ro permission 3. Allowlisting upload_artifact.cjs for @actions/artifact in the CJS require validation test (package installed at runtime via setup.sh) Agent-Logs-Url: https://github.com/github/gh-aw/sessions/2842a7ad-1e1e-4045-b23f-312ccc7878d1 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot.lock.yml | 3 ++- pkg/workflow/awf_helpers.go | 10 ++++++++++ pkg/workflow/cjs_require_validation_test.go | 7 +++++-- pkg/workflow/mcp_setup_generator.go | 8 ++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 87fb89a9d6f..5a0478efa50 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -580,6 +580,7 @@ jobs: mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_37135a487e85aeac_EOF' {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-copilot"],"allowed_repos":["github/gh-aw"]},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"allow-skip-archive":true,"default-retention-days":1,"max-retention-days":1,"max-size-bytes":104857600,"max-uploads":1}} GH_AW_SAFE_OUTPUTS_CONFIG_37135a487e85aeac_EOF @@ -1650,7 +1651,7 @@ jobs: set -o pipefail touch /tmp/gh-aw/agent-step-summary.md # shellcheck disable=SC1003 - sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_AW_GH_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains '*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.jsdelivr.net,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,docs.github.com,esm.sh,get.pnpm.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,go.dev,golang.org,googleapis.deno.dev,googlechromelabs.github.io,goproxy.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,lfs.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkg.go.dev,playwright.download.prss.microsoft.com,ppa.launchpad.net,proxy.golang.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,sum.golang.org,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.npmjs.com,www.npmjs.org,yarnpkg.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.14 --skip-pull --enable-api-proxy \ + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts:${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts:rw" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_AW_GH_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains '*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.jsdelivr.net,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,docs.github.com,esm.sh,get.pnpm.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,go.dev,golang.org,googleapis.deno.dev,googlechromelabs.github.io,goproxy.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,lfs.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkg.go.dev,playwright.download.prss.microsoft.com,ppa.launchpad.net,proxy.golang.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,sum.golang.org,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.npmjs.com,www.npmjs.org,yarnpkg.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.14 --skip-pull --enable-api-proxy \ -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --autopilot --max-autopilot-continues 2 --allow-all-tools --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE diff --git a/pkg/workflow/awf_helpers.go b/pkg/workflow/awf_helpers.go index 8f763c7d501..e12e6bed6fc 100644 --- a/pkg/workflow/awf_helpers.go +++ b/pkg/workflow/awf_helpers.go @@ -97,6 +97,16 @@ func BuildAWFCommand(config AWFCommandConfig) string { ghAwDir, ghAwDir, ghAwDir, ghAwDir, ) + // When upload_artifact is configured, add a read-write mount for the staging directory + // so the model can copy files there from inside the container. The parent ${RUNNER_TEMP}/gh-aw + // is mounted :ro above; this child mount overrides access for the staging subdirectory only. + // The staging directory must already exist on the host (created in Write Safe Outputs Config step). + if config.WorkflowData != nil && config.WorkflowData.SafeOutputs != nil && config.WorkflowData.SafeOutputs.UploadArtifact != nil { + stagingDir := "${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts" + expandableArgs += fmt.Sprintf(` --mount "%s:%s:rw"`, stagingDir, stagingDir) + awfHelpersLog.Print("Added read-write mount for upload_artifact staging directory") + } + // Add --allow-host-service-ports for services with port mappings. // This is appended as a raw (expandable) arg because the value contains // ${{ job.services..ports[''] }} expressions that include single quotes. diff --git a/pkg/workflow/cjs_require_validation_test.go b/pkg/workflow/cjs_require_validation_test.go index 9cd58c23eb4..413f112b414 100644 --- a/pkg/workflow/cjs_require_validation_test.go +++ b/pkg/workflow/cjs_require_validation_test.go @@ -87,9 +87,12 @@ func TestCJSFilesNoActionsRequires(t *testing.T) { var violations []string // Exception: handler_auth.cjs is allowed to require @actions/github - // because the package is installed at runtime via setup.sh when safe-output-custom-tokens flag is enabled + // because the package is installed at runtime via setup.sh when safe-output-custom-tokens flag is enabled. + // Exception: upload_artifact.cjs is allowed to require @actions/artifact + // because the package is installed at runtime via setup.sh when safe-output-artifact-client flag is enabled. allowedNpmActionsRequires := map[string][]string{ - "handler_auth.cjs": {"@actions/github"}, + "handler_auth.cjs": {"@actions/github"}, + "upload_artifact.cjs": {"@actions/artifact"}, } for _, filename := range cjsFiles { diff --git a/pkg/workflow/mcp_setup_generator.go b/pkg/workflow/mcp_setup_generator.go index 8560b52d5f9..31fb832ebf5 100644 --- a/pkg/workflow/mcp_setup_generator.go +++ b/pkg/workflow/mcp_setup_generator.go @@ -228,6 +228,14 @@ func (c *Compiler) generateMCPSetup(yaml *strings.Builder, tools map[string]any, yaml.WriteString(" mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs\n") yaml.WriteString(" mkdir -p /tmp/gh-aw/safeoutputs\n") yaml.WriteString(" mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs\n") + // Create the upload-artifact staging directory before the agent runs so it exists + // as a bind-mount source for the read-write mount added to the awf command. + // The directory is inside ${RUNNER_TEMP}/gh-aw which is mounted :ro in the agent + // container; a child :rw mount on this subdirectory allows the model to write staged + // files there. The directory must exist on the host before awf starts. + if workflowData.SafeOutputs != nil && workflowData.SafeOutputs.UploadArtifact != nil { + yaml.WriteString(" mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts\n") + } // Write the safe-outputs configuration to config.json delimiter := GenerateHeredocDelimiterFromSeed("SAFE_OUTPUTS_CONFIG", workflowData.FrontmatterHash) From e1b9b28fa15051be5c129c60bef5171d3a1fa01f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:25:12 +0000 Subject: [PATCH 17/23] fix: clarify upload_artifact tool is available in smoke-copilot prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent was using missing_tool for upload_artifact because it inferred the tool "wasn't available" (reading PR #25002 that adds it). Added an explicit note to the step 10 prompt making clear that upload_artifact IS configured and available in this run — agents must use it directly, not report it as missing. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/ec3258ef-7bfb-40b6-a4b7-db15e0eab9db Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index 554c219011b..8b47336854f 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -146,7 +146,7 @@ strict: false - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) - Use the `add_comment` tool with `discussion_number: ` to add a fun, playful comment stating that the smoke test agent was here 9. **Build gh-aw**: Run `GOCACHE=/tmp/go-cache GOMODCACHE=/tmp/go-mod make build` to verify the agent can successfully build the gh-aw project (both caches must be set to /tmp because the default cache locations are not writable). If the command fails, mark this test as ❌ and report the failure. -10. **Upload gh-aw binary as artifact**: After a successful build, use bash to copy the `./gh-aw` binary into the staging directory (`mkdir -p $RUNNER_TEMP/gh-aw/safeoutputs/upload-artifacts && cp ./gh-aw $RUNNER_TEMP/gh-aw/safeoutputs/upload-artifacts/gh-aw`), then call the `upload_artifact` safe-output tool with `path: "gh-aw"`, `retention_days: 1`, and `skip_archive: true`. Mark this test as ❌ if the build in step 9 failed. +10. **Upload gh-aw binary as artifact**: After a successful build, use bash to copy the `./gh-aw` binary into the staging directory (`mkdir -p $RUNNER_TEMP/gh-aw/safeoutputs/upload-artifacts && cp ./gh-aw $RUNNER_TEMP/gh-aw/safeoutputs/upload-artifacts/gh-aw`), then call the `upload_artifact` safe-output tool with `path: "gh-aw"`, `retention_days: 1`, and `skip_archive: true`. The `upload_artifact` tool is available and configured in this workflow run — use it directly, do NOT use `missing_tool` for it. Mark this test as ❌ if the build in step 9 failed. 11. **Discussion Creation Testing**: Use the `create_discussion` safe-output tool to create a discussion in the announcements category titled "copilot was here" with the label "ai-generated" 12. **Workflow Dispatch Testing**: Use the `dispatch_workflow` safe output tool to trigger the `haiku-printer` workflow with a haiku as the message input. Create an original, creative haiku about software testing or automation. 13. **PR Review Testing**: Review the diff of the current pull request. Leave 1-2 inline `create_pull_request_review_comment` comments on specific lines, then call `submit_pull_request_review` with a brief body summarizing your review and event `COMMENT`. To test `reply_to_pull_request_review_comment`: use the `pull_request_read` tool (with `method: "get_review_comments"` and `pullNumber: ${{ github.event.pull_request.number }}`) to fetch the PR's existing review comments, then reply to the most recent one using `reply_to_pull_request_review_comment` with its actual numeric `id` as the `comment_id`. Note: `create_pull_request_review_comment` does not return a `comment_id` — you must fetch existing comment IDs from the GitHub API. If the PR has no existing review comments, skip the reply sub-test. From b07adc9d04068e4149872081ae5d36e94bb2b085 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:16:23 +0000 Subject: [PATCH 18/23] fix: wire upload-artifact into import merge and add shared workflow - `imports.go`: add `upload-artifact` case to `hasSafeOutputType()` so conflict detection catches duplicate import definitions - `imports.go`: add `UploadArtifact` field merge in `mergeSafeOutputConfig()` so `upload-artifact` configured in a shared workflow is correctly propagated to the compiled output - `shared/safe-output-upload-artifact.md`: new shared workflow file that workflows can import to enable the `upload_artifact` tool (max-uploads: 3, 7-day default retention, allow skip-archive) Agent-Logs-Url: https://github.com/github/gh-aw/sessions/9b0a2995-8016-4ea7-aaed-43f1d35d1fe5 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../shared/safe-output-upload-artifact.md | 58 +++++++++++++++++++ pkg/workflow/imports.go | 5 ++ 2 files changed, 63 insertions(+) create mode 100644 .github/workflows/shared/safe-output-upload-artifact.md diff --git a/.github/workflows/shared/safe-output-upload-artifact.md b/.github/workflows/shared/safe-output-upload-artifact.md new file mode 100644 index 00000000000..e6a89d9f1f6 --- /dev/null +++ b/.github/workflows/shared/safe-output-upload-artifact.md @@ -0,0 +1,58 @@ +--- +safe-outputs: + upload-artifact: + max-uploads: 3 + default-retention-days: 7 + max-retention-days: 30 + allow: + skip-archive: true +--- + + diff --git a/pkg/workflow/imports.go b/pkg/workflow/imports.go index 8990c05f16a..ff17098219e 100644 --- a/pkg/workflow/imports.go +++ b/pkg/workflow/imports.go @@ -310,6 +310,8 @@ func hasSafeOutputType(config *SafeOutputsConfig, key string) bool { return config.PushToPullRequestBranch != nil case "upload-asset": return config.UploadAssets != nil + case "upload-artifact": + return config.UploadArtifact != nil case "update-release": return config.UpdateRelease != nil case "create-agent-session": @@ -448,6 +450,9 @@ func mergeSafeOutputConfig(result *SafeOutputsConfig, config map[string]any, c * if result.UploadAssets == nil && importedConfig.UploadAssets != nil { result.UploadAssets = importedConfig.UploadAssets } + if result.UploadArtifact == nil && importedConfig.UploadArtifact != nil { + result.UploadArtifact = importedConfig.UploadArtifact + } if result.UpdateRelease == nil && importedConfig.UpdateRelease != nil { result.UpdateRelease = importedConfig.UpdateRelease } From 568e236541bbaa6315daf31fb4b92cb37c4da313 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:25:06 +0000 Subject: [PATCH 19/23] Add changeset --- .changeset/patch-add-upload-artifact-safe-output.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/patch-add-upload-artifact-safe-output.md b/.changeset/patch-add-upload-artifact-safe-output.md index 6159e700eeb..bb9ebf5231c 100644 --- a/.changeset/patch-add-upload-artifact-safe-output.md +++ b/.changeset/patch-add-upload-artifact-safe-output.md @@ -2,4 +2,4 @@ "gh-aw": patch --- -Add a new `upload-artifact` safe output type for run-scoped GitHub Actions artifact uploads, including frontmatter configuration, validation, and runtime handling that returns temporary artifact IDs for downstream resolution. +Add an `upload-artifact` safe output type for run-scoped GitHub Actions artifact uploads, including frontmatter config, inline handler processing, staged file mounting, and shared workflow support. From 39c15485d20b6c4adab1d0097b6980ffae53cf24 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:14:50 +0000 Subject: [PATCH 20/23] fix: add upload_artifact tool to actions/setup/js/safe_outputs_tools.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tool definition existed in pkg/workflow/js/safe_outputs_tools.json (embedded in the Go binary) but was missing from actions/setup/js/safe_outputs_tools.json (the file deployed to the runner). At runtime, setup.sh copies actions/setup/js/safe_outputs_tools.json to $RUNNER_TEMP/gh-aw/actions/safe_outputs_tools.json, and generate_safe_outputs_tools.cjs reads from there to build tools.json. Because upload_artifact was absent from the runtime file, the tool was never included in the agent's tool list even when upload-artifact was configured in the workflow — causing the agent to report it as missing. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d8fb3535-969a-447e-b290-f30a63e3a461 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_tools.json | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/actions/setup/js/safe_outputs_tools.json b/actions/setup/js/safe_outputs_tools.json index 0899c39c14c..61c2c67a592 100644 --- a/actions/setup/js/safe_outputs_tools.json +++ b/actions/setup/js/safe_outputs_tools.json @@ -849,6 +849,58 @@ "additionalProperties": false } }, + { + "name": "upload_artifact", + "description": "Upload files as a run-scoped GitHub Actions artifact. The model must first copy files to $RUNNER_TEMP/gh-aw/safeoutputs/upload-artifacts/ then request upload using this tool. Returns a temporary artifact ID that can be resolved to a download URL by an authorised step. Exactly one of path or filters must be present.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Path to the file or directory to upload, relative to $RUNNER_TEMP/gh-aw/safeoutputs/upload-artifacts/ (e.g., \"report.json\" or \"dist/\"). Required unless filters is provided." + }, + "filters": { + "type": "object", + "description": "Glob-based file selection filters. Required unless path is provided.", + "properties": { + "include": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Glob patterns for files to include (e.g., [\"reports/**/*.json\"])" + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Glob patterns for files to exclude (e.g., [\"**/*.env\", \"**/*.pem\"])" + } + }, + "additionalProperties": false + }, + "retention_days": { + "type": "integer", + "minimum": 1, + "description": "Number of days to retain the artifact. Capped by workflow configuration." + }, + "skip_archive": { + "type": "boolean", + "description": "Upload the file directly without archiving. Only allowed for single-file uploads when enabled in workflow configuration." + }, + "secrecy": { + "type": "string", + "description": "Confidentiality level of the artifact content (e.g., \"public\", \"internal\", \"private\")." + }, + "integrity": { + "type": "string", + "description": "Trustworthiness level of the artifact source (e.g., \"low\", \"medium\", \"high\")." + } + }, + "additionalProperties": false + } + }, { "name": "update_release", "description": "Update a GitHub release description by replacing, appending to, or prepending to the existing content. Use this to add release notes, changelogs, or additional information to an existing release.", From b7c0b49e993da3953201aeabec3937f8b09f0eca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:34:42 +0000 Subject: [PATCH 21/23] fix: forward safe-output-artifact-client input to setup.sh in index.js index.js spawns setup.sh with an explicit env object, but INPUT_SAFE_OUTPUT_ARTIFACT_CLIENT was never included in that object. As a result, setup.sh always saw the variable as unset/empty and skipped the @actions/artifact npm install even when the action input safe-output-artifact-client: true was passed by the caller. Added getActionInput("SAFE_OUTPUT_ARTIFACT_CLIENT") and forwarded it as INPUT_SAFE_OUTPUT_ARTIFACT_CLIENT alongside the existing INPUT_SAFE_OUTPUT_CUSTOM_TOKENS forwarding. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/78c8b762-e517-4e8b-9576-251f6fe7059b Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/actions/setup/index.js b/actions/setup/index.js index 437c6499946..1c556227b10 100644 --- a/actions/setup/index.js +++ b/actions/setup/index.js @@ -12,6 +12,7 @@ const setupStartMs = Date.now(); // runner versions preserve the original hyphen form. getActionInput() handles // both forms automatically. const safeOutputCustomTokens = getActionInput("SAFE_OUTPUT_CUSTOM_TOKENS") || "false"; +const safeOutputArtifactClient = getActionInput("SAFE_OUTPUT_ARTIFACT_CLIENT") || "false"; const inputTraceId = getActionInput("TRACE_ID"); const inputJobName = getActionInput("JOB_NAME"); @@ -19,6 +20,7 @@ const result = spawnSync(path.join(__dirname, "setup.sh"), [], { stdio: "inherit", env: Object.assign({}, process.env, { INPUT_SAFE_OUTPUT_CUSTOM_TOKENS: safeOutputCustomTokens, + INPUT_SAFE_OUTPUT_ARTIFACT_CLIENT: safeOutputArtifactClient, INPUT_TRACE_ID: inputTraceId, INPUT_JOB_NAME: inputJobName, // Tell setup.sh to skip the OTLP span: in action mode index.js sends it From 44a92dd94b8426b8985f3b06418cf74e61a6ac5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 00:10:01 +0000 Subject: [PATCH 22/23] fix: use dynamic import() for @actions/artifact ESM compatibility in upload_artifact handler Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d43a1646-c12c-46d3-8803-cb939423efc5 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/package-lock.json | 1911 +++++++++++++++++++++++++- actions/setup/js/upload_artifact.cjs | 9 +- 2 files changed, 1886 insertions(+), 34 deletions(-) diff --git a/actions/setup/js/package-lock.json b/actions/setup/js/package-lock.json index bd1b93a188b..4ebeb144be7 100644 --- a/actions/setup/js/package-lock.json +++ b/actions/setup/js/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "devDependencies": { + "@actions/artifact": "^6.0.0", "@actions/core": "^3.0.0", "@actions/exec": "^3.0.0", "@actions/github": "^9.0.0", @@ -21,6 +22,191 @@ "vitest": "^4.1.3" } }, + "node_modules/@actions/artifact": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@actions/artifact/-/artifact-6.2.1.tgz", + "integrity": "sha512-sJGH0mhEbEjBCw7o6SaLhUU66u27aFW8HTfkIb5Tk2/Wy0caUDc+oYQEgnuFN7a0HCpAbQyK0U6U7XUJDgDWrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/core": "^3.0.0", + "@actions/github": "^9.0.0", + "@actions/http-client": "^4.0.0", + "@azure/storage-blob": "^12.30.0", + "@octokit/core": "^7.0.6", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-retry": "^8.0.0", + "@octokit/request": "^10.0.7", + "@octokit/request-error": "^7.1.0", + "@protobuf-ts/plugin": "^2.2.3-alpha.1", + "@protobuf-ts/runtime": "^2.9.4", + "archiver": "^7.0.1", + "jwt-decode": "^4.0.0", + "unzip-stream": "^0.3.1" + } + }, + "node_modules/@actions/artifact/node_modules/@actions/http-client": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", + "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/artifact/node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@actions/artifact/node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@actions/artifact/node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@actions/artifact/node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@actions/artifact/node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@actions/artifact/node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@actions/artifact/node_modules/@octokit/plugin-retry": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.1.0.tgz", + "integrity": "sha512-O1FZgXeiGb2sowEr/hYTr6YunGdSAFWnr2fyW39Ah85H8O33ELASQxcvOFF5LE6Tjekcyu2ms4qAzJVhSaJxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=7" + } + }, + "node_modules/@actions/artifact/node_modules/@octokit/request": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@actions/artifact/node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@actions/artifact/node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@actions/artifact/node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@actions/artifact/node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC" + }, "node_modules/@actions/core": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", @@ -394,6 +580,221 @@ "dev": true, "license": "MIT" }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.2.tgz", + "integrity": "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-xml": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.5.0.tgz", + "integrity": "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-xml-parser": "^5.0.7", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/storage-blob": { + "version": "12.31.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.31.0.tgz", + "integrity": "sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.3", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/core-xml": "^1.4.5", + "@azure/logger": "^1.1.4", + "@azure/storage-common": "^12.3.0", + "events": "^3.0.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/storage-common": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/@azure/storage-common/-/storage-common-12.3.0.tgz", + "integrity": "sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.1.4", + "events": "^3.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -454,23 +855,56 @@ "node": ">=18" } }, - "node_modules/@emnapi/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "node_modules/@bufbuild/protobuf": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", "dev": true, - "license": "MIT", - "optional": true, - "peer": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@bufbuild/protoplugin": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.11.0.tgz", + "integrity": "sha512-lyZVNFUHArIOt4W0+dwYBe5GBwbKzbOy8ObaloEqsw9Mmiwv2O48TwddDoHN4itylC+BaEGqFdI1W8WQt2vWJQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@emnapi/wasi-threads": "1.2.0", - "tslib": "^2.4.0" + "@bufbuild/protobuf": "2.11.0", + "@typescript/vfs": "^1.6.2", + "typescript": "5.4.5" } }, - "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "node_modules/@bufbuild/protoplugin/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", "dev": true, "license": "MIT", "optional": true, @@ -491,6 +925,24 @@ "tslib": "^2.4.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -751,6 +1203,17 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -758,6 +1221,66 @@ "dev": true, "license": "MIT" }, + "node_modules/@protobuf-ts/plugin": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/plugin/-/plugin-2.11.1.tgz", + "integrity": "sha512-HyuprDcw0bEEJqkOWe1rnXUP0gwYLij8YhPuZyZk6cJbIgc/Q0IFgoHQxOXNIXAcXM4Sbehh6kjVnCzasElw1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^2.4.0", + "@bufbuild/protoplugin": "^2.4.0", + "@protobuf-ts/protoc": "^2.11.1", + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1", + "typescript": "^3.9" + }, + "bin": { + "protoc-gen-dump": "bin/protoc-gen-dump", + "protoc-gen-ts": "bin/protoc-gen-ts" + } + }, + "node_modules/@protobuf-ts/plugin/node_modules/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@protobuf-ts/protoc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/protoc/-/protoc-2.11.1.tgz", + "integrity": "sha512-mUZJaV0daGO6HUX90o/atzQ6A7bbN2RSuHtdwo8SSF2Qoe3zHwa4IHyCN1evftTeHfLmdz+45qo47sL+5P8nyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "protoc": "protoc.js" + } + }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", + "dev": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@protobuf-ts/runtime-rpc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.11.1.tgz", + "integrity": "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", @@ -1080,6 +1603,34 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript/vfs": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.4.tgz", + "integrity": "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.4.tgz", + "integrity": "sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@vitest/coverage-v8": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.3.tgz", @@ -1246,6 +1797,93 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1268,6 +1906,28 @@ "js-tokens": "^10.0.0" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1275,6 +1935,124 @@ "dev": true, "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.6.0.tgz", + "integrity": "sha512-2YkS7NuiJceSEbyEOdSNLE9tsGd+f4+f7C+Nik/MCk27SYdwIMPT/yRKvg++FZhQXgk0KWJKJyXX9RhVV0RGqA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz", + "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.12.0.tgz", + "integrity": "sha512-w28i8lkBgREV3rPXGbgK+BO66q+ZpKqRWrZLiCdmmUlLPrQ45CzkvRhN+7lnv00Gpi2zy5naRxnUFAxCECDm9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -1282,6 +2060,20 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", @@ -1312,6 +2104,50 @@ "node": "18 || 20 || >=22" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1322,21 +2158,138 @@ "node": ">=18" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", "dev": true, - "license": "MIT" + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT" - }, - "node_modules/deprecation": { + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", @@ -1353,6 +2306,20 @@ "node": ">=8" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/es-module-lexer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", @@ -1370,6 +2337,36 @@ "@types/estree": "^1.0.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -1397,6 +2394,50 @@ ], "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.10", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.10.tgz", + "integrity": "sha512-go2J2xODMc32hT+4Xr/bBGXMaIoiCwrwp2mMtAvKyvEFW6S/v5Gn2pBmE4nvbwNjGhpcAiOwEv7R6/GZ6XRa9w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.1", + "strnum": "^2.2.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1429,6 +2470,23 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1444,6 +2502,61 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1461,6 +2574,99 @@ "dev": true, "license": "MIT" }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -1500,6 +2706,22 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -1507,6 +2729,69 @@ "dev": true, "license": "MIT" }, + "node_modules/json-with-bigint": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", + "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -1768,9 +3053,23 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", @@ -1822,6 +3121,39 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -1832,6 +3164,13 @@ "node": ">=10" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1851,6 +3190,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -1872,6 +3221,56 @@ "wrappy": "1" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-expression-matcher": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.4.0.tgz", + "integrity": "sha512-s4DQMxIdhj3jLFWd9LxHOplj4p9yQ4ffMGowFf3cpEgrrJjEhN0V5nxw4Ye1EViAGDoL4/1AeO6qHpqYPOzE4Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -1944,6 +3343,73 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/rolldown": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", @@ -1978,6 +3444,27 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -1991,6 +3478,29 @@ "node": ">=10" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -1998,6 +3508,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -2037,6 +3560,145 @@ "dev": true, "license": "MIT" }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2050,6 +3712,39 @@ "node": ">=8" } }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2104,13 +3799,22 @@ "node": ">=6" } }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true, + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/tunnel": { "version": "0.0.6", @@ -2160,6 +3864,24 @@ "dev": true, "license": "ISC" }, + "node_modules/unzip-stream": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.4.tgz", + "integrity": "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary": "^0.3.0", + "mkdirp": "^0.5.1" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "8.0.5", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.5.tgz", @@ -2328,6 +4050,22 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -2345,12 +4083,125 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } } } } diff --git a/actions/setup/js/upload_artifact.cjs b/actions/setup/js/upload_artifact.cjs index e11619852ed..a8914390be9 100644 --- a/actions/setup/js/upload_artifact.cjs +++ b/actions/setup/js/upload_artifact.cjs @@ -249,13 +249,14 @@ function clampRetention(requested, defaultDays, maxDays) { /** * Create or return the @actions/artifact DefaultArtifactClient. * global.__createArtifactClient can be set in tests to inject a mock client factory. - * @returns {{ uploadArtifact: (name: string, files: string[], rootDir: string, opts: object) => Promise<{id?: number, size?: number}> }} + * Uses dynamic import() because @actions/artifact v2+ is an ES module. + * @returns {Promise<{ uploadArtifact: (name: string, files: string[], rootDir: string, opts: object) => Promise<{id?: number, size?: number}> }>} */ -function getArtifactClient() { +async function getArtifactClient() { if (typeof global.__createArtifactClient === "function") { return global.__createArtifactClient(); } - const { DefaultArtifactClient } = require("@actions/artifact"); + const { DefaultArtifactClient } = await import("@actions/artifact"); return new DefaultArtifactClient(); } @@ -363,7 +364,7 @@ async function main(config = {}) { if (!isStaged) { // Upload files directly via @actions/artifact REST API. const absoluteFiles = files.map(f => path.join(STAGING_DIR, f)); - const client = getArtifactClient(); + const client = await getArtifactClient(); try { const uploadResult = await client.uploadArtifact(artifactName, absoluteFiles, STAGING_DIR, { retentionDays }); core.info(`Uploaded artifact "${artifactName}" (id=${uploadResult.id ?? "n/a"}, size=${uploadResult.size ?? totalSize}B)`); From cf2d96fca6905ccf0600fa7be5a5dbde1eb7b2ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 00:34:13 +0000 Subject: [PATCH 23/23] fix: use for i := range cfg.MaxUploads (intrange lint, Go 1.22+) Agent-Logs-Url: https://github.com/github/gh-aw/sessions/1955c5ca-a0aa-465b-b360-54bc4b0934f0 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_safe_outputs_job.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index a7f64939a45..ed029659b3f 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -252,7 +252,7 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa consolidatedSafeOutputsJobLog.Print("Exposing upload_artifact outputs from handler manager") cfg := data.SafeOutputs.UploadArtifact outputs["upload_artifact_count"] = "${{ steps.process_safe_outputs.outputs.upload_artifact_count }}" - for i := 0; i < cfg.MaxUploads; i++ { + for i := range cfg.MaxUploads { outputs[fmt.Sprintf("upload_artifact_slot_%d_tmp_id", i)] = fmt.Sprintf("${{ steps.process_safe_outputs.outputs.slot_%d_tmp_id }}", i) } }