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..bb9ebf5231c --- /dev/null +++ b/.changeset/patch-add-upload-artifact-safe-output.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +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. 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/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 35d69a4b4c6..322afeaa5f3 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,10 @@ 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 + 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 - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -979,7 +980,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 +1096,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 +1111,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 +1127,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 +1264,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 +1345,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 +1432,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 +1508,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 +1634,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: @@ -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 @@ -1831,6 +1832,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 @@ -2239,6 +2249,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 @@ -2254,6 +2266,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 @@ -2279,6 +2292,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 @@ -2288,7 +2307,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: | diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index 8c6087573ed..8b47336854f 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`. 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. ## Output 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/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 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/package.json b/actions/setup/js/package.json index da4dba01a7e..6ff9740b864 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/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs index 05ea9f2cef7..2600089c7b0 100644 --- a/actions/setup/js/safe_output_handler_manager.cjs +++ b/actions/setup/js/safe_output_handler_manager.cjs @@ -74,6 +74,7 @@ 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", }; /** 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.", diff --git a/actions/setup/js/upload_artifact.cjs b/actions/setup/js/upload_artifact.cjs new file mode 100644 index 00000000000..a8914390be9 --- /dev/null +++ b/actions/setup/js/upload_artifact.cjs @@ -0,0 +1,409 @@ +// @ts-check +/// + +/** + * upload_artifact handler + * + * 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. 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) + * 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 (skips actual upload) + */ + +const fs = require("fs"); +const path = require("path"); +const { getErrorMessage } = require("./error_helpers.cjs"); +const { globPatternToRegex } = require("./glob_pattern_helpers.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/`; + +/** 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 = `${process.env.RUNNER_TEMP}/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; +} + +/** + * 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); +} + +/** + * Create or return the @actions/artifact DefaultArtifactClient. + * global.__createArtifactClient can be set in tests to inject a mock client factory. + * 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}> }>} + */ +async function getArtifactClient() { + if (typeof global.__createArtifactClient === "function") { + return global.__createArtifactClient(); + } + const { DefaultArtifactClient } = await import("@actions/artifact"); + return new DefaultArtifactClient(); +} + +/** + * 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)"}`); + + // Slot index tracks which slot each successful request maps to. + let slotIndex = 0; + + /** @type {Record} resolver: tmpId → artifact name */ + const resolver = {}; + + /** + * 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.`, + }; + } + + const i = slotIndex; + + // Resolve skip_archive. + const skipArchive = typeof message.skip_archive === "boolean" ? message.skip_archive : defaultSkipArchive; + if (skipArchive && !allowSkipArchive) { + 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(message, allowedPaths, filtersInclude, filtersExclude); + if (resolveError) { + return { success: false, error: `${ERR_VALIDATION}: upload_artifact: ${resolveError}` }; + } + + if (files.length === 0) { + if (defaultIfNoFiles === "ignore") { + 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) { + return { success: false, error: `${ERR_VALIDATION}: upload_artifact: ${skipArchiveError}` }; + } + + // Validate total size. + const totalSize = computeTotalSize(files); + if (totalSize > maxSizeBytes) { + 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 message.retention_days === "number" ? message.retention_days : undefined, defaultRetentionDays, maxRetentionDays); + + // Derive artifact name and generate temporary ID. + const artifactName = deriveArtifactName(message, 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) { + // Upload files directly via @actions/artifact REST API. + const absoluteFiles = files.map(f => path.join(STAGING_DIR, f)); + 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)`); + } catch (err) { + return { + success: false, + error: `${ERR_VALIDATION}: upload_artifact: failed to upload artifact "${artifactName}": ${getErrorMessage(err)}`, + }; + } + } else { + core.info(`Staged mode: skipping artifact upload for slot ${i}`); + } + + // 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)); + + slotIndex++; + + // Update the count output. + core.setOutput("upload_artifact_count", String(slotIndex)); + + // 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)}`); + } + + 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 new file mode 100644 index 00000000000..5107f4aa96a --- /dev/null +++ b/actions/setup/js/upload_artifact.test.cjs @@ -0,0 +1,337 @@ +// @ts-check +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +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 RESOLVER_FILE = `${RUNNER_TEMP}/gh-aw/artifact-resolver.json`; + +describe("upload_artifact.cjs", () => { + let mockCore; + let mockArtifactClient; + let originalEnv; + + /** + * @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); + } + + /** + * Build a config object. + * @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 per-message handler pattern. + * Injects global.__createArtifactClient so tests never hit the real REST API. + * @param {object} config + * @param {object[]} messages + * @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); + if (result && result.success === false && !result.skipped) { + mockCore.setFailed(result.error); + } + } + return results; + } + + 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), + }, + }; + + 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. + process.env.RUNNER_TEMP = RUNNER_TEMP; + 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 resolver file + if (fs.existsSync(RESOLVER_FILE)) { + fs.unlinkSync(RESOLVER_FILE); + } + }); + + afterEach(() => { + process.env = originalEnv; + delete global.__createArtifactClient; + }); + + describe("path-based upload", () => { + it("uploads a single file via artifact client", async () => { + writeStaging("report.json", '{"result": "ok"}'); + + const results = await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json", retention_days: 14 }]); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + 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"); + }); + + it("clamps retention days to max-retention-days", async () => { + writeStaging("report.json"); + + await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json", retention_days: 999 }]); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + const [, , , opts] = mockArtifactClient.uploadArtifact.mock.calls[0]; + expect(opts.retentionDays).toBe(30); + }); + + it("uses default retention when retention_days is absent", async () => { + writeStaging("report.json"); + + await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json" }]); + + const [, , , opts] = mockArtifactClient.uploadArtifact.mock.calls[0]; + expect(opts.retentionDays).toBe(7); + }); + }); + + describe("validation errors", () => { + it("fails when both path and filters are present", async () => { + writeStaging("report.json"); + + 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"); + + 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 () => { + writeStaging("app.bin"); + + 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 () => { + writeStaging("output/a.json"); + writeStaging("output/b.json"); + + 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")); + }); + }); + + describe("skip_archive allowed", () => { + it("succeeds with skip_archive=true and a single file", async () => { + writeStaging("app.bin", "binary data"); + + const results = await runHandler(buildConfig({ "allow-skip-archive": true }), [{ type: "upload_artifact", path: "app.bin", skip_archive: true }]); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(results[0].success).toBe(true); + expect(mockArtifactClient.uploadArtifact).toHaveBeenCalledOnce(); + }); + }); + + 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", "{}"); + + await runHandler(buildConfig(), [ + { + type: "upload_artifact", + filters: { include: ["reports/**/*.json"], exclude: ["reports/private/**"] }, + }, + ]); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockArtifactClient.uploadArtifact).toHaveBeenCalledOnce(); + const [, files] = mockArtifactClient.uploadArtifact.mock.calls[0]; + expect(files).toHaveLength(2); + expect(mockCore.setOutput).toHaveBeenCalledWith("slot_0_file_count", "2"); + }); + + it("handles no-files with if-no-files=ignore", async () => { + await runHandler(buildConfig({ "default-if-no-files": "ignore" }), [{ type: "upload_artifact", filters: { include: ["nonexistent/**"] } }]); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + 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(); + }); + }); + + describe("allowed-paths policy", () => { + it("filters out files not in allowed-paths", async () => { + writeStaging("dist/app.js"); + writeStaging("secret.env"); + + 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"); + }); + }); + + 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"); + }); + }); + + describe("staged mode", () => { + it("skips upload client call in staged mode (env var)", async () => { + process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"; + writeStaging("report.json"); + + const results = await runHandler(buildConfig(), [{ type: "upload_artifact", path: "report.json" }]); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + 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 upload client call when staged=true in config", async () => { + writeStaging("report.json"); + + const results = await runHandler(buildConfig({ staged: true }), [{ type: "upload_artifact", path: "report.json" }]); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(results[0].success).toBe(true); + expect(mockArtifactClient.uploadArtifact).not.toHaveBeenCalled(); + }); + }); + + describe("resolver file", () => { + it("writes a resolver mapping with temporary IDs", async () => { + writeStaging("report.json"); + + 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")); + const keys = Object.keys(resolver); + expect(keys.length).toBe(1); + expect(keys[0]).toMatch(/^tmp_artifact_[A-Z0-9]{26}$/); + }); + }); +}); diff --git a/actions/setup/setup.sh b/actions/setup/setup.sh index 8dee7d07330..7c8e7814bb7 100755 --- a/actions/setup/setup.sh +++ b/actions/setup/setup.sh @@ -82,6 +82,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}" @@ -416,6 +419,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/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 4458825a4f8..9b47e5a6ba1 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -4343,7 +4343,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": [ { @@ -7460,6 +7460,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": [ { 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/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/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/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_config.go b/pkg/workflow/compiler_safe_outputs_config.go index 1766702d9bb..f2f9f3c7d77 100644 --- a/pkg/workflow/compiler_safe_outputs_config.go +++ b/pkg/workflow/compiler_safe_outputs_config.go @@ -769,6 +769,45 @@ 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) + } + } + 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 { if cfg.AutofixCodeScanningAlert == nil { return nil diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index c0c36eb50fa..ed029659b3f 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -51,6 +51,8 @@ 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 // When custom tokens are enabled and the safe_outputs job runs on a custom image runner, // emit a Node.js setup step before actions/setup. @@ -67,7 +69,7 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa // 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 @@ -175,6 +177,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 @@ -189,7 +192,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 { @@ -224,6 +243,20 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa outputs["create_agent_session_session_url"] = "${{ steps.process_safe_outputs.outputs.session_url }}" } + // 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("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) + } + } + // 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. @@ -354,12 +387,18 @@ 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 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/compiler_types.go b/pkg/workflow/compiler_types.go index f7cf8e83261..dd9c1ad6e48 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -535,6 +535,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_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_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/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/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 } diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index 9614a6f42dd..a0579d792a1 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 $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 + } } ] 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) diff --git a/pkg/workflow/notify_comment.go b/pkg/workflow/notify_comment.go index 928ca60fc91..6ef1b445d84 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 new file mode 100644 index 00000000000..215bed2785d --- /dev/null +++ b/pkg/workflow/publish_artifacts.go @@ -0,0 +1,222 @@ +package workflow + +import ( + "encoding/json" + "fmt" + "strings" + + "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 + +// 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/" + +// 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("upload-artifact enabled with default 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 +} + +// generateSafeOutputsArtifactStagingUpload generates a step in the main agent job that uploads +// 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 { + 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", artifactStagingDirExpr) + 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..18f24554523 --- /dev/null +++ b/pkg/workflow/publish_artifacts_test.go @@ -0,0 +1,253 @@ +//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 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, artifactStagingDirExpr, "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/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/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/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).