diff --git a/.gitattributes b/.gitattributes index f3054f34dd73..80bf2d7025c4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ /**/.yarn/** linguist-generated -* -text \ No newline at end of file +* -text + +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/workflows/code-simplifier.lock.yml b/.github/workflows/code-simplifier.lock.yml new file mode 100644 index 000000000000..f13e0e0ef6af --- /dev/null +++ b/.github/workflows/code-simplifier.lock.yml @@ -0,0 +1,1241 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.55.0). DO NOT EDIT. +# +# To update this file, edit github/gh-aw/.github/workflows/code-simplifier.md@852cb06ad52958b402ed982b69957ffc57ca0619 and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Analyzes recently modified code and creates pull requests with simplifications that improve clarity, consistency, and maintainability while preserving functionality +# +# Source: github/gh-aw/.github/workflows/code-simplifier.md@852cb06ad52958b402ed982b69957ffc57ca0619 +# +# Resolved workflow manifest: +# Imports: +# - shared/mood.md +# - shared/reporting.md +# +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"dbddcd7da0eefb6c24a3380b4e555d7aacd3ba78c14d79ebc131c33cb86f02ea","compiler_version":"v0.55.0","strict":true} + +name: "Code Simplifier" +"on": + schedule: + - cron: "6 12 * * *" + # Friendly format: daily (scattered) + # skip-if-match: is:pr is:open in:title "[code-simplifier]" # Skip-if-match processed as search check in pre-activation job + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Code Simplifier" + +jobs: + activation: + needs: pre_activation + if: needs.pre_activation.outputs.activated == 'true' + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@e211c855a20aa6cf9297b411466e1c382a8686db # v0.55.0 + with: + destination: /opt/gh-aw/actions + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_INFO_VERSION: "" + GH_AW_INFO_AGENT_VERSION: "latest" + GH_AW_INFO_CLI_VERSION: "v0.55.0" + GH_AW_INFO_WORKFLOW_NAME: "Code Simplifier" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.23.0" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { main } = require('/opt/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + .github + .agents + sparse-checkout-cone-mode: true + fetch-depth: 1 + persist-credentials: false + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_WORKFLOW_FILE: "code-simplifier.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + { + cat << 'GH_AW_PROMPT_EOF' + + GH_AW_PROMPT_EOF + cat "/opt/gh-aw/prompts/xpia.md" + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" + cat "/opt/gh-aw/prompts/markdown.md" + cat "/opt/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_EOF' + + Tools: create_pull_request, missing_tool, missing_data, noop + GH_AW_PROMPT_EOF + cat "/opt/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat << 'GH_AW_PROMPT_EOF' + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/mood.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/reporting.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/code-simplifier.md}} + GH_AW_PROMPT_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_WORKFLOW_ID_SANITIZED: codesimplifier + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@e211c855a20aa6cf9297b411466e1c382a8686db # v0.55.0 + with: + destination: /opt/gh-aw/actions + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + (github.event.pull_request) || (github.event.issue.pull_request) + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: /opt/gh-aw/actions/install_copilot_cli.sh latest + - name: Install awf binary + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.23.0 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.23.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.23.0 ghcr.io/github/gh-aw-firewall/squid:0.23.0 ghcr.io/github/gh-aw-mcpg:v0.1.8 ghcr.io/github/github-mcp-server:v0.31.0 node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p /opt/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' + {"create_pull_request":{"expires":24,"max":1,"reviewers":["copilot"],"title_prefix":"[code-simplifier] "},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_EOF + cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' + [ + { + "description": "Create a new GitHub pull request to propose code changes. Use this after making file edits to submit them for review and merging. The PR will be created from the current branch with your committed changes. For code review comments on an existing PR, use create_pull_request_review_comment instead. CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[code-simplifier] \". Labels [\"refactoring\" \"code-quality\" \"automation\"] will be automatically added. Reviewers [\"copilot\"] will be assigned.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "Detailed PR description in Markdown. Include what changes were made, why, testing notes, and any breaking changes. Do NOT repeat the title as a heading.", + "type": "string" + }, + "branch": { + "description": "Source branch name containing the changes. If omitted, uses the current working branch.", + "type": "string" + }, + "draft": { + "description": "Whether to create the PR as a draft. Draft PRs cannot be merged until marked as ready for review. Use mark_pull_request_as_ready_for_review to convert a draft PR. Default: true.", + "type": "boolean" + }, + "integrity": { + "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").", + "type": "string" + }, + "labels": { + "description": "Labels to categorize the PR (e.g., 'enhancement', 'bugfix'). Labels must exist in the repository.", + "items": { + "type": "string" + }, + "type": "array" + }, + "repo": { + "description": "Target repository in 'owner/repo' format. For multi-repo workflows where the target repo differs from the workflow repo, this must match a repo in the allowed-repos list or the configured target-repo. If omitted, defaults to the configured target-repo (from safe-outputs config), NOT the workflow repository. In most cases, you should omit this parameter and let the system use the configured default.", + "type": "string" + }, + "secrecy": { + "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").", + "type": "string" + }, + "title": { + "description": "Concise PR title describing the changes. Follow repository conventions (e.g., conventional commits). The title appears as the main heading.", + "type": "string" + } + }, + "required": [ + "title", + "body" + ], + "type": "object" + }, + "name": "create_pull_request" + }, + { + "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "integrity": { + "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").", + "type": "string" + }, + "reason": { + "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", + "type": "string" + }, + "secrecy": { + "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").", + "type": "string" + }, + "tool": { + "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", + "type": "string" + } + }, + "required": [ + "reason" + ], + "type": "object" + }, + "name": "missing_tool" + }, + { + "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "integrity": { + "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").", + "type": "string" + }, + "message": { + "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", + "type": "string" + }, + "secrecy": { + "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").", + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "name": "noop" + }, + { + "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "context": { + "description": "Additional context about the missing data or where it should come from (max 256 characters).", + "type": "string" + }, + "data_type": { + "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", + "type": "string" + }, + "integrity": { + "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").", + "type": "string" + }, + "reason": { + "description": "Explanation of why this data is needed to complete the task (max 256 characters).", + "type": "string" + }, + "secrecy": { + "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").", + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "name": "missing_data" + } + ] + GH_AW_SAFE_OUTPUTS_TOOLS_EOF + cat > /opt/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF' + { + "create_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + } + } + GH_AW_SAFE_OUTPUTS_VALIDATION_EOF + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash /opt/gh-aw/actions/start_safe_outputs_server.sh + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.8' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.31.0", + "env": { + "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_EOF + - name: Download activation artifact + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8 + with: + name: activation + path: /tmp/gh-aw + - name: Clean git credentials + run: bash /opt/gh-aw/actions/clean_git_credentials.sh + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 30 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + # shellcheck disable=SC1003 + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.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,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.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,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.23.0 --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/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --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 + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Detect inference access error + id: detect-inference-error + if: always() + continue-on-error: true + run: bash /opt/gh-aw/actions/detect_inference_access_error.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: | + # Copy Copilot session state files to logs folder for artifact collection + # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them + SESSION_STATE_DIR="$HOME/.copilot/session-state" + LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" + + if [ -d "$SESSION_STATE_DIR" ]; then + echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" + mkdir -p "$LOGS_DIR" + cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true + echo "Session state files copied successfully" + else + echo "No session-state directory found at $SESSION_STATE_DIR" + fi + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash /opt/gh-aw/actions/append_agent_step_summary.sh + - name: Upload Safe Outputs + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-output + path: ${{ env.GH_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.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,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.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,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Upload sanitized agent output + if: always() && env.GH_AW_AGENT_OUTPUT + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: agent-output + path: ${{ env.GH_AW_AGENT_OUTPUT }} + if-no-files-found: warn + - name: Upload engine output files + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: agent_outputs + path: | + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + if-no-files-found: ignore + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: agent-artifacts + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + /tmp/gh-aw/aw-*.patch + if-no-files-found: ignore + # --- Threat Detection (inline) --- + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ steps.collect_output.outputs.output_types }} + HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + WORKFLOW_NAME: "Code Simplifier" + WORKFLOW_DESCRIPTION: "Analyzes recently modified code and creates pull requests with simplifications that improve clarity, consistency, and maintainability while preserving functionality" + HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool shell(cat) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(tail) + # --allow-tool shell(wc) + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + # shellcheck disable=SC1003 + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.23.0 --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/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(wc)'\'' --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection results + id: parse_detection_results + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: threat-detection.log + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Set detection conclusion + id: detection_conclusion + if: always() + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_SUCCESS: ${{ steps.parse_detection_results.outputs.success }} + run: | + if [[ "$RUN_DETECTION" != "true" ]]; then + echo "conclusion=skipped" >> "$GITHUB_OUTPUT" + echo "success=true" >> "$GITHUB_OUTPUT" + echo "Detection was not needed, marking as skipped" + elif [[ "$DETECTION_SUCCESS" == "true" ]]; then + echo "conclusion=success" >> "$GITHUB_OUTPUT" + echo "success=true" >> "$GITHUB_OUTPUT" + echo "Detection passed successfully" + else + echo "conclusion=failure" >> "$GITHUB_OUTPUT" + echo "success=false" >> "$GITHUB_OUTPUT" + echo "Detection found issues" + fi + + conclusion: + needs: + - activation + - agent + - safe_outputs + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-code-simplifier" + cancel-in-progress: false + outputs: + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@e211c855a20aa6cf9297b411466e1c382a8686db # v0.55.0 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Code Simplifier" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@852cb06ad52958b402ed982b69957ffc57ca0619" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/852cb06ad52958b402ed982b69957ffc57ca0619/.github/workflows/code-simplifier.md" + GH_AW_TRACKER_ID: "code-simplifier" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/noop.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Code Simplifier" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@852cb06ad52958b402ed982b69957ffc57ca0619" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/852cb06ad52958b402ed982b69957ffc57ca0619/.github/workflows/code-simplifier.md" + GH_AW_TRACKER_ID: "code-simplifier" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Code Simplifier" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@852cb06ad52958b402ed982b69957ffc57ca0619" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/852cb06ad52958b402ed982b69957ffc57ca0619/.github/workflows/code-simplifier.md" + GH_AW_TRACKER_ID: "code-simplifier" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "code-simplifier" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_TIMEOUT_MINUTES: "30" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Handle No-Op Message + id: handle_noop_message + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Code Simplifier" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@852cb06ad52958b402ed982b69957ffc57ca0619" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/852cb06ad52958b402ed982b69957ffc57ca0619/.github/workflows/code-simplifier.md" + GH_AW_TRACKER_ID: "code-simplifier" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Handle Create Pull Request Error + id: handle_create_pr_error + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Code Simplifier" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@852cb06ad52958b402ed982b69957ffc57ca0619" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/852cb06ad52958b402ed982b69957ffc57ca0619/.github/workflows/code-simplifier.md" + GH_AW_TRACKER_ID: "code-simplifier" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_create_pr_error.cjs'); + await main(); + + pre_activation: + runs-on: ubuntu-slim + outputs: + activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_skip_if_match.outputs.skip_check_ok == 'true') }} + matched_command: '' + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@e211c855a20aa6cf9297b411466e1c382a8686db # v0.55.0 + with: + destination: /opt/gh-aw/actions + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_REQUIRED_ROLES: admin,maintainer,write + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_membership.cjs'); + await main(); + - name: Check skip-if-match query + id: check_skip_if_match + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_SKIP_QUERY: "is:pr is:open in:title \"[code-simplifier]\"" + GH_AW_WORKFLOW_NAME: "Code Simplifier" + GH_AW_SKIP_MAX_MATCHES: "1" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_skip_if_match.cjs'); + await main(); + + safe_outputs: + needs: + - activation + - agent + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true') + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/code-simplifier" + GH_AW_ENGINE_ID: "copilot" + GH_AW_TRACKER_ID: "code-simplifier" + GH_AW_WORKFLOW_ID: "code-simplifier" + GH_AW_WORKFLOW_NAME: "Code Simplifier" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@852cb06ad52958b402ed982b69957ffc57ca0619" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/852cb06ad52958b402ed982b69957ffc57ca0619/.github/workflows/code-simplifier.md" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_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 }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@e211c855a20aa6cf9297b411466e1c382a8686db # v0.55.0 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8 + with: + name: agent-artifacts + path: /tmp/gh-aw/ + - name: Checkout repository + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.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,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.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,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"expires\":24,\"labels\":[\"refactoring\",\"code-quality\",\"automation\"],\"max\":1,\"max_patch_size\":1024,\"reviewers\":[\"copilot\"],\"title_prefix\":\"[code-simplifier] \"},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload safe output items manifest + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-output-items + path: /tmp/safe-output-items.jsonl + if-no-files-found: warn + diff --git a/.github/workflows/code-simplifier.md b/.github/workflows/code-simplifier.md new file mode 100644 index 000000000000..c69190ea788a --- /dev/null +++ b/.github/workflows/code-simplifier.md @@ -0,0 +1,371 @@ +--- +name: Code Simplifier +description: Analyzes recently modified code and creates pull requests with simplifications that improve clarity, consistency, and maintainability while preserving functionality +on: + schedule: daily + skip-if-match: 'is:pr is:open in:title "[code-simplifier]"' + +permissions: + contents: read + issues: read + pull-requests: read + +tracker-id: code-simplifier + +imports: + - shared/mood.md + - shared/reporting.md + +safe-outputs: + create-pull-request: + title-prefix: '[code-simplifier] ' + labels: [refactoring, code-quality, automation] + reviewers: [copilot] + expires: 1d + +tools: + github: + toolsets: [default] + +timeout-minutes: 30 +strict: true +source: github/gh-aw/.github/workflows/code-simplifier.md@852cb06ad52958b402ed982b69957ffc57ca0619 +engine: copilot +--- + + + + +# Code Simplifier Agent + +You are an expert code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality. Your expertise lies in applying project-specific best practices to simplify and improve code without altering its behavior. You prioritize readable, explicit code over overly compact solutions. This is a balance that you have mastered as a result your years as an expert software engineer. + +## Your Mission + +Analyze recently modified code from the last 24 hours and apply refinements that improve code quality while preserving all functionality. Create a pull request with the simplified code if improvements are found. + +## Current Context + +- **Repository**: ${{ github.repository }} +- **Analysis Date**: $(date +%Y-%m-%d) +- **Workspace**: ${{ github.workspace }} + +## Phase 1: Identify Recently Modified Code + +### 1.1 Find Recent Changes + +Search for merged pull requests and commits from the last 24 hours: + +```bash +# Get yesterday's date in ISO format +YESTERDAY=$(date -d '1 day ago' '+%Y-%m-%d' 2>/dev/null || date -v-1d '+%Y-%m-%d') + +# List recent commits +git log --since="24 hours ago" --pretty=format:"%H %s" --no-merges +``` + +Use GitHub tools to: + +- Search for pull requests merged in the last 24 hours: `repo:${{ github.repository }} is:pr is:merged merged:>=${YESTERDAY}` +- Get details of merged PRs to understand what files were changed +- List commits from the last 24 hours to identify modified files + +### 1.2 Extract Changed Files + +For each merged PR or recent commit: + +- Use `pull_request_read` with `method: get_files` to list changed files +- Use `get_commit` to see file changes in recent commits +- Focus on source code files (`.go`, `.js`, `.ts`, `.tsx`, `.cjs`, `.py`, `.cs`, etc.) +- Exclude test files, lock files, and generated files + +### 1.3 Determine Scope + +If **no files were changed in the last 24 hours**, exit gracefully without creating a PR: + +``` +✅ No code changes detected in the last 24 hours. +Code simplifier has nothing to process today. +``` + +If **files were changed**, proceed to Phase 2. + +## Phase 2: Analyze and Simplify Code + +### 2.1 Review Project Standards + +Before simplifying, review the project's coding standards from relevant documentation: + +- For Go projects: Check `AGENTS.md`, `DEVGUIDE.md`, or similar files +- For JavaScript/TypeScript: Look for `CLAUDE.md`, style guides, or coding conventions +- For Python: Check for style guides, PEP 8 adherence, or project-specific conventions +- For .NET/C#: Check `.editorconfig`, `Directory.Build.props`, or coding conventions in docs + +**Key Standards to Apply:** + +For **JavaScript/TypeScript** projects: + +- Use ES modules with proper import sorting and extensions +- Prefer `function` keyword over arrow functions for top-level functions +- Use explicit return type annotations for top-level functions +- Follow proper React component patterns with explicit Props types +- Use proper error handling patterns (avoid try/catch when possible) +- Maintain consistent naming conventions + +### 2.2 Simplification Principles + +Apply these refinements to the recently modified code: + +#### 1. Preserve Functionality + +- **NEVER** change what the code does - only how it does it +- All original features, outputs, and behaviors must remain intact +- Run tests before and after to ensure no behavioral changes + +#### 2. Enhance Clarity + +- Reduce unnecessary complexity and nesting +- Eliminate redundant code and abstractions +- Improve readability through clear variable and function names +- Consolidate related logic +- Remove unnecessary comments that describe obvious code +- **IMPORTANT**: Avoid nested ternary operators - prefer switch statements or if/else chains +- Choose clarity over brevity - explicit code is often better than compact code + +#### 3. Apply Project Standards + +- Use project-specific conventions and patterns +- Follow established naming conventions +- Apply consistent formatting +- Use appropriate language features (modern syntax where beneficial) + +#### 4. Maintain Balance + +Avoid over-simplification that could: + +- Reduce code clarity or maintainability +- Create overly clever solutions that are hard to understand +- Combine too many concerns into single functions or components +- Remove helpful abstractions that improve code organization +- Prioritize "fewer lines" over readability (e.g., nested ternaries, dense one-liners) +- Make the code harder to debug or extend + +### 2.3 Perform Code Analysis + +For each changed file: + +1. **Read the file contents** using the edit or view tool +2. **Identify refactoring opportunities**: + - Long functions that could be split + - Duplicate code patterns + - Complex conditionals that could be simplified + - Unclear variable names + - Missing or excessive comments + - Non-standard patterns +3. **Design the simplification**: + - What specific changes will improve clarity? + - How can complexity be reduced? + - What patterns should be applied? + - Will this maintain all functionality? + +### 2.4 Apply Simplifications + +Use the **edit** tool to modify files: + +```bash +# For each file with improvements: +# 1. Read the current content +# 2. Apply targeted edits to simplify code +# 3. Ensure all functionality is preserved +``` + +**Guidelines for edits:** + +- Make surgical, targeted changes +- One logical improvement per edit (but batch multiple edits in a single response) +- Preserve all original behavior +- Keep changes focused on recently modified code +- Don't refactor unrelated code unless it improves understanding of the changes + +## Phase 3: Validate Changes + +### 3.1 Run Tests + +After making simplifications, run the project's test suite to ensure no functionality was broken: + +# For JavaScript/TypeScript projects + +yarn --cwd code vitest run --changed + +```` + +If tests fail: + +- Review the failures carefully +- Revert changes that broke functionality +- Adjust simplifications to preserve behavior +- Re-run tests until they pass + +### 3.2 Run Linters + +Ensure code style is consistent: + +``` +yarn lint +``` + +Fix any linting issues introduced by the simplifications. + +### 3.3 Check Build + +Verify the project still builds successfully: + +``` +yarn task --task "compile" +``` + +## Phase 4: Create Pull Request + +### 4.1 Determine If PR Is Needed + +Only create a PR if: + +- ✅ You made actual code simplifications +- ✅ All tests pass +- ✅ Linting is clean +- ✅ Build succeeds +- ✅ Changes improve code quality without breaking functionality + +If no improvements were made or changes broke tests, exit gracefully: + +``` +✅ Code analyzed from last 24 hours. +No simplifications needed - code already meets quality standards. +``` + +### 4.2 Generate PR Description + +If creating a PR, use this structure: + +```markdown +## Code Simplification - [Date] + +This PR simplifies recently modified code to improve clarity, consistency, and maintainability while preserving all functionality. + +### Files Simplified + +- `path/to/file1.go` - [Brief description of improvements] +- `path/to/file2.js` - [Brief description of improvements] + +### Improvements Made + +1. **Reduced Complexity** + - Simplified nested conditionals in `file1.go` + - Extracted helper function for repeated logic + +2. **Enhanced Clarity** + - Renamed variables for better readability + - Removed redundant comments + - Applied consistent naming conventions + +3. **Applied Project Standards** + - Used `function` keyword instead of arrow functions + - Added explicit type annotations + - Followed established patterns + +### Changes Based On + +Recent changes from: + +- #[PR_NUMBER] - [PR title] +- Commit [SHORT_SHA] - [Commit message] + +### Testing + +- ✅ All tests pass (`make test-unit`) +- ✅ Linting passes (`make lint`) +- ✅ Build succeeds (`make build`) +- ✅ No functional changes - behavior is identical + +### Review Focus + +Please verify: + +- Functionality is preserved +- Simplifications improve code quality +- Changes align with project conventions +- No unintended side effects + +--- + +_Automated by Code Simplifier Agent - analyzing code from the last 24 hours_ +``` + +### 4.3 Use Safe Outputs + +Create the pull request using the safe-outputs configuration: + +- Title will be prefixed with `[code-simplifier]` +- Labeled with `refactoring`, `code-quality`, `automation` +- Assigned to `copilot` for review +- Set as ready for review (not draft) + +## Important Guidelines + +### Scope Control + +- **Focus on recent changes**: Only refine code modified in the last 24 hours +- **Don't over-refactor**: Avoid touching unrelated code +- **Preserve interfaces**: Don't change public APIs or exported functions +- **Incremental improvements**: Make targeted, surgical changes + +### Quality Standards + +- **Test first**: Always run tests after simplifications +- **Preserve behavior**: Functionality must remain identical +- **Follow conventions**: Apply project-specific patterns consistently +- **Clear over clever**: Prioritize readability and maintainability + +### Exit Conditions + +Exit gracefully without creating a PR if: + +- No code was changed in the last 24 hours +- No simplifications are beneficial +- Tests fail after changes +- Build fails after changes +- Changes are too risky or complex + +### Success Metrics + +A successful simplification: + +- ✅ Improves code clarity without changing behavior +- ✅ Passes all tests and linting +- ✅ Applies project-specific conventions +- ✅ Makes code easier to understand and maintain +- ✅ Focuses on recently modified code +- ✅ Provides clear documentation of changes + +## Output Requirements + +Your output MUST either: + +1. **If no changes in last 24 hours**: + + ``` + ✅ No code changes detected in the last 24 hours. + Code simplifier has nothing to process today. + ``` + +2. **If no simplifications beneficial**: + + ``` + ✅ Code analyzed from last 24 hours. + No simplifications needed - code already meets quality standards. + ``` + +3. **If simplifications made**: Create a PR with the changes using safe-outputs + +Begin your code simplification analysis now. Find recently modified code, assess simplification opportunities, apply improvements while preserving functionality, validate changes, and create a PR if beneficial. +```` diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml new file mode 100644 index 000000000000..ddc15bcdc28b --- /dev/null +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -0,0 +1,1158 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.55.0). DO NOT EDIT. +# +# To update this file, edit github/gh-aw/.github/workflows/duplicate-code-detector.md@852cb06ad52958b402ed982b69957ffc57ca0619 and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Identifies duplicate code patterns across the codebase and suggests refactoring opportunities +# +# Source: github/gh-aw/.github/workflows/duplicate-code-detector.md@852cb06ad52958b402ed982b69957ffc57ca0619 +# +# Resolved workflow manifest: +# Imports: +# - shared/mood.md +# +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"8f718997fab9f4077b50cf09e67d1c93cb5d11105def34e97ec4d56929ca4323","compiler_version":"v0.55.0","strict":true} + +name: "Duplicate Code Detector" +"on": + schedule: + - cron: "12 8 * * *" + # Friendly format: daily (scattered) + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Duplicate Code Detector" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@e211c855a20aa6cf9297b411466e1c382a8686db # v0.55.0 + with: + destination: /opt/gh-aw/actions + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_INFO_VERSION: "" + GH_AW_INFO_AGENT_VERSION: "latest" + GH_AW_INFO_CLI_VERSION: "v0.55.0" + GH_AW_INFO_WORKFLOW_NAME: "Duplicate Code Detector" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.23.0" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { main } = require('/opt/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + .github + .agents + sparse-checkout-cone-mode: true + fetch-depth: 1 + persist-credentials: false + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_WORKFLOW_FILE: "duplicate-code-detector.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_HEAD_COMMIT_ID: ${{ github.event.head_commit.id }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + { + cat << 'GH_AW_PROMPT_EOF' + + GH_AW_PROMPT_EOF + cat "/opt/gh-aw/prompts/xpia.md" + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" + cat "/opt/gh-aw/prompts/markdown.md" + cat "/opt/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_EOF' + + Tools: create_issue, missing_tool, missing_data, noop + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/mood.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/duplicate-code-detector.md}} + GH_AW_PROMPT_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_HEAD_COMMIT_ID: ${{ github.event.head_commit.id }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_HEAD_COMMIT_ID: ${{ github.event.head_commit.id }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_HEAD_COMMIT_ID: process.env.GH_AW_GITHUB_EVENT_HEAD_COMMIT_ID, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_WORKFLOW_ID_SANITIZED: duplicatecodedetector + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@e211c855a20aa6cf9297b411466e1c382a8686db # v0.55.0 + with: + destination: /opt/gh-aw/actions + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + (github.event.pull_request) || (github.event.issue.pull_request) + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: /opt/gh-aw/actions/install_copilot_cli.sh latest + - name: Install awf binary + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.23.0 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.23.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.23.0 ghcr.io/github/gh-aw-firewall/squid:0.23.0 ghcr.io/github/gh-aw-mcpg:v0.1.8 ghcr.io/github/github-mcp-server:v0.31.0 ghcr.io/github/serena-mcp-server:latest node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p /opt/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' + {"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_EOF + cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' + [ + { + "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Assignees [\"copilot\"] will be automatically assigned.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "Detailed issue description in Markdown. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate.", + "type": "string" + }, + "integrity": { + "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").", + "type": "string" + }, + "labels": { + "description": "Labels to categorize the issue (e.g., 'bug', 'enhancement'). Labels must exist in the repository.", + "items": { + "type": "string" + }, + "type": "array" + }, + "parent": { + "description": "Parent issue number for creating sub-issues. This is the numeric ID from the GitHub URL (e.g., 42 in github.com/owner/repo/issues/42). Can also be a temporary_id (e.g., 'aw_abc123', 'aw_Test123') from a previously created issue in the same workflow run.", + "type": [ + "number", + "string" + ] + }, + "secrecy": { + "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").", + "type": "string" + }, + "temporary_id": { + "description": "Unique temporary identifier for referencing this issue before it's created. Format: 'aw_' followed by 3 to 12 alphanumeric characters (e.g., 'aw_abc1', 'aw_Test123'). Use '#aw_ID' in body text to reference other issues by their temporary_id; these are replaced with actual issue numbers after creation.", + "pattern": "^aw_[A-Za-z0-9]{3,12}$", + "type": "string" + }, + "title": { + "description": "Concise issue title summarizing the bug, feature, or task. The title appears as the main heading, so keep it brief and descriptive.", + "type": "string" + } + }, + "required": [ + "title", + "body" + ], + "type": "object" + }, + "name": "create_issue" + }, + { + "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "integrity": { + "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").", + "type": "string" + }, + "reason": { + "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", + "type": "string" + }, + "secrecy": { + "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").", + "type": "string" + }, + "tool": { + "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", + "type": "string" + } + }, + "required": [ + "reason" + ], + "type": "object" + }, + "name": "missing_tool" + }, + { + "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "integrity": { + "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").", + "type": "string" + }, + "message": { + "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", + "type": "string" + }, + "secrecy": { + "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").", + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "name": "noop" + }, + { + "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "context": { + "description": "Additional context about the missing data or where it should come from (max 256 characters).", + "type": "string" + }, + "data_type": { + "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", + "type": "string" + }, + "integrity": { + "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").", + "type": "string" + }, + "reason": { + "description": "Explanation of why this data is needed to complete the task (max 256 characters).", + "type": "string" + }, + "secrecy": { + "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").", + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "name": "missing_data" + } + ] + GH_AW_SAFE_OUTPUTS_TOOLS_EOF + cat > /opt/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF' + { + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "parent": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + } + } + GH_AW_SAFE_OUTPUTS_VALIDATION_EOF + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash /opt/gh-aw/actions/start_safe_outputs_server.sh + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.8' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.31.0", + "env": { + "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + } + }, + "serena": { + "type": "stdio", + "container": "ghcr.io/github/serena-mcp-server:latest", + "args": ["--network", "host"], + "entrypoint": "serena", + "entrypointArgs": ["start-mcp-server", "--context", "codex", "--project", "\${GITHUB_WORKSPACE}"], + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw"] + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_EOF + - name: Download activation artifact + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8 + with: + name: activation + path: /tmp/gh-aw + - name: Clean git credentials + run: bash /opt/gh-aw/actions/clean_git_credentials.sh + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 15 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + # shellcheck disable=SC1003 + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.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,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.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,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.23.0 --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/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --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 + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Detect inference access error + id: detect-inference-error + if: always() + continue-on-error: true + run: bash /opt/gh-aw/actions/detect_inference_access_error.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: | + # Copy Copilot session state files to logs folder for artifact collection + # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them + SESSION_STATE_DIR="$HOME/.copilot/session-state" + LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" + + if [ -d "$SESSION_STATE_DIR" ]; then + echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" + mkdir -p "$LOGS_DIR" + cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true + echo "Session state files copied successfully" + else + echo "No session-state directory found at $SESSION_STATE_DIR" + fi + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash /opt/gh-aw/actions/append_agent_step_summary.sh + - name: Upload Safe Outputs + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-output + path: ${{ env.GH_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.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,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.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,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Upload sanitized agent output + if: always() && env.GH_AW_AGENT_OUTPUT + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: agent-output + path: ${{ env.GH_AW_AGENT_OUTPUT }} + if-no-files-found: warn + - name: Upload engine output files + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: agent_outputs + path: | + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + if-no-files-found: ignore + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: agent-artifacts + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + if-no-files-found: ignore + # --- Threat Detection (inline) --- + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ steps.collect_output.outputs.output_types }} + HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + WORKFLOW_NAME: "Duplicate Code Detector" + WORKFLOW_DESCRIPTION: "Identifies duplicate code patterns across the codebase and suggests refactoring opportunities" + HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool shell(cat) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(tail) + # --allow-tool shell(wc) + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + # shellcheck disable=SC1003 + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.23.0 --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/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(wc)'\'' --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection results + id: parse_detection_results + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: threat-detection.log + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Set detection conclusion + id: detection_conclusion + if: always() + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_SUCCESS: ${{ steps.parse_detection_results.outputs.success }} + run: | + if [[ "$RUN_DETECTION" != "true" ]]; then + echo "conclusion=skipped" >> "$GITHUB_OUTPUT" + echo "success=true" >> "$GITHUB_OUTPUT" + echo "Detection was not needed, marking as skipped" + elif [[ "$DETECTION_SUCCESS" == "true" ]]; then + echo "conclusion=success" >> "$GITHUB_OUTPUT" + echo "success=true" >> "$GITHUB_OUTPUT" + echo "Detection passed successfully" + else + echo "conclusion=failure" >> "$GITHUB_OUTPUT" + echo "success=false" >> "$GITHUB_OUTPUT" + echo "Detection found issues" + fi + + conclusion: + needs: + - activation + - agent + - safe_outputs + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + concurrency: + group: "gh-aw-conclusion-duplicate-code-detector" + cancel-in-progress: false + outputs: + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@e211c855a20aa6cf9297b411466e1c382a8686db # v0.55.0 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Duplicate Code Detector" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/duplicate-code-detector.md@852cb06ad52958b402ed982b69957ffc57ca0619" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/852cb06ad52958b402ed982b69957ffc57ca0619/.github/workflows/duplicate-code-detector.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/noop.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Duplicate Code Detector" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/duplicate-code-detector.md@852cb06ad52958b402ed982b69957ffc57ca0619" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/852cb06ad52958b402ed982b69957ffc57ca0619/.github/workflows/duplicate-code-detector.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Duplicate Code Detector" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/duplicate-code-detector.md@852cb06ad52958b402ed982b69957ffc57ca0619" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/852cb06ad52958b402ed982b69957ffc57ca0619/.github/workflows/duplicate-code-detector.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "duplicate-code-detector" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_TIMEOUT_MINUTES: "15" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Handle No-Op Message + id: handle_noop_message + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Duplicate Code Detector" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/duplicate-code-detector.md@852cb06ad52958b402ed982b69957ffc57ca0619" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/852cb06ad52958b402ed982b69957ffc57ca0619/.github/workflows/duplicate-code-detector.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); + await main(); + + safe_outputs: + needs: agent + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/duplicate-code-detector" + GH_AW_ENGINE_ID: "copilot" + GH_AW_WORKFLOW_ID: "duplicate-code-detector" + GH_AW_WORKFLOW_NAME: "Duplicate Code Detector" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/duplicate-code-detector.md@852cb06ad52958b402ed982b69957ffc57ca0619" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/852cb06ad52958b402ed982b69957ffc57ca0619/.github/workflows/duplicate-code-detector.md" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} + 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 }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@e211c855a20aa6cf9297b411466e1c382a8686db # v0.55.0 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.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,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.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,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"assignees\":[\"copilot\"],\"max\":1},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_ASSIGN_COPILOT: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Assign Copilot to created issues + if: steps.process_safe_outputs.outputs.issues_to_assign_copilot != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_ISSUES_TO_ASSIGN_COPILOT: ${{ steps.process_safe_outputs.outputs.issues_to_assign_copilot }} + with: + github-token: ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/assign_copilot_to_created_issues.cjs'); + await main(); + - name: Upload safe output items manifest + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-output-items + path: /tmp/safe-output-items.jsonl + if-no-files-found: warn + diff --git a/.github/workflows/duplicate-code-detector.md b/.github/workflows/duplicate-code-detector.md new file mode 100644 index 000000000000..fff4fc5f4edf --- /dev/null +++ b/.github/workflows/duplicate-code-detector.md @@ -0,0 +1,260 @@ +--- +name: Duplicate Code Detector +description: Identifies duplicate code patterns across the codebase and suggests refactoring opportunities +on: + workflow_dispatch: + schedule: daily +permissions: + contents: read + issues: read + pull-requests: read +engine: copilot +tools: + serena: ['typescript'] +safe-outputs: + create-issue: + expires: 2d + title-prefix: '[duplicate-code] ' + labels: [code-quality, automated-analysis, cookie] + assignees: copilot + group: true + max: 3 +timeout-minutes: 15 +strict: true +imports: + - shared/mood.md +source: github/gh-aw/.github/workflows/duplicate-code-detector.md@852cb06ad52958b402ed982b69957ffc57ca0619 +--- + +# Duplicate Code Detection + +Analyze code to identify duplicated patterns using Serena's semantic code analysis capabilities. Report significant findings that require refactoring. + +## Task + +Detect and report code duplication by: + +1. **Analyzing Recent Commits**: Review changes in the latest commits +2. **Detecting Duplicated Code**: Identify similar or duplicated code patterns using semantic analysis +3. **Reporting Findings**: Create a detailed issue if significant duplication is detected (threshold: >10 lines or 3+ similar patterns) + +## Context + +- **Repository**: ${{ github.repository }} +- **Commit ID**: ${{ github.event.head_commit.id }} +- **Triggered by**: @${{ github.actor }} + +## Analysis Workflow + +### 1. Project Activation + +Activate the project in Serena: + +- Use `activate_project` tool with workspace path `${{ github.workspace }}` (mounted repository directory) +- This sets up the semantic code analysis environment + +### 2. Changed Files Analysis + +Identify and analyze modified files: + +- Determine files changed in the recent commits +- **ONLY analyze .ts and .tsx files** - exclude all other file types +- **Exclude JavaScript files except .cjs** from analysis (files matching patterns: `*.js`, `*.mjs`, `*.jsx`, `*.ts`, `*.tsx`) +- **Exclude test files** from analysis (files matching patterns: `*.test.`, `*.spec.`, or located in directories named `test`, `tests`, `__tests__`, or `spec`) +- **Exclude workflow files** from analysis (files under `.github/workflows/*`) +- Use `get_symbols_overview` to understand file structure +- Use `read_file` to examine modified file contents + +### 3. Duplicate Detection + +Apply semantic code analysis to find duplicates: + +**Symbol-Level Analysis**: + +- For significant functions/methods in changed files, use `find_symbol` to search for similarly named symbols +- Use `find_referencing_symbols` to understand usage patterns +- Identify functions with similar names in different files (e.g., `processData` across modules) + +**Pattern Search**: + +- Use `search_for_pattern` to find similar code patterns +- Search for duplication indicators: + - Similar function signatures + - Repeated logic blocks + - Similar variable naming patterns + - Near-identical code blocks + +**Structural Analysis**: + +- Use `list_dir` and `find_file` to identify files with similar names or purposes +- Compare symbol overviews across files for structural similarities + +### 4. Duplication Evaluation + +Assess findings to identify true code duplication: + +**Duplication Types**: + +- **Exact Duplication**: Identical code blocks in multiple locations +- **Structural Duplication**: Same logic with minor variations (different variable names, etc.) +- **Functional Duplication**: Different implementations of the same functionality +- **Copy-Paste Programming**: Similar code blocks that could be extracted into shared utilities + +**Assessment Criteria**: + +- **Severity**: Amount of duplicated code (lines of code, number of occurrences) +- **Impact**: Where duplication occurs (critical paths, frequently called code) +- **Maintainability**: How duplication affects code maintainability +- **Refactoring Opportunity**: Whether duplication can be easily refactored + +### 5. Issue Reporting + +Create separate issues for each distinct duplication pattern found (maximum 3 patterns per run). Each pattern should get its own issue to enable focused remediation. + +**When to Create Issues**: + +- Only create issues if significant duplication is found (threshold: >10 lines of duplicated code OR 3+ instances of similar patterns) +- **Create one issue per distinct pattern** - do NOT bundle multiple patterns in a single issue +- Limit to the top 3 most significant patterns if more are found +- Use the `create_issue` tool from safe-outputs MCP **once for each pattern** + +**Issue Contents for Each Pattern**: + +- **Executive Summary**: Brief description of this specific duplication pattern +- **Duplication Details**: Specific locations and code blocks for this pattern only +- **Severity Assessment**: Impact and maintainability concerns for this pattern +- **Refactoring Recommendations**: Suggested approaches to eliminate this pattern +- **Code Examples**: Concrete examples with file paths and line numbers for this pattern + +## Detection Scope + +### Report These Issues + +- Identical or nearly identical functions in different files +- Repeated code blocks that could be extracted to utilities +- Similar classes or modules with overlapping functionality +- Copy-pasted code with minor modifications +- Duplicated business logic across components + +### Skip These Patterns + +- Standard boilerplate code (imports, exports, etc.) +- Test setup/teardown code (acceptable duplication in tests) +- **All test files** (files matching: `*.test.`, `*.spec.`, or in `test/`, `tests/`, `__tests__/`, `spec/` directories) +- **All workflow files** (files under `.github/workflows/*`) +- Configuration files with similar structure +- Language-specific patterns (constructors, getters/setters) +- Small code snippets (<5 lines) unless highly repetitive + +### Analysis Depth + +- **File Type Restriction**: ONLY analyze .ts and .tsx files - ignore all other file types +- **Primary Focus**: All .ts and .tsx files changed in the current push (excluding test files and workflow files) +- **Secondary Analysis**: Check for duplication with existing .ts and .tsx codebase (excluding test files and workflow files) +- **Cross-Reference**: Look for patterns across .ts and .tsx files in the repository +- **Historical Context**: Consider if duplication is new or existing + +## Issue Template + +For each distinct duplication pattern found, create a separate issue using this structure: + +````markdown +# 🔍 Duplicate Code Detected: [Pattern Name] + +_Analysis of commit ${{ github.event.head_commit.id }}_ + +**Assignee**: @copilot + +## Summary + +[Brief overview of this specific duplication pattern] + +## Duplication Details + +### Pattern: [Description] + +- **Severity**: High/Medium/Low +- **Occurrences**: [Number of instances] +- **Locations**: + - `path/to/file1.ext` (lines X-Y) + - `path/to/file2.ext` (lines A-B) +- **Code Sample**: + ```[language] + [Example of duplicated code] + ``` +```` + +## Impact Analysis + +- **Maintainability**: [How this affects code maintenance] +- **Bug Risk**: [Potential for inconsistent fixes] +- **Code Bloat**: [Impact on codebase size] + +## Refactoring Recommendations + +1. **[Recommendation 1]** + - Extract common functionality to: `suggested/path/utility.ext` + - Estimated effort: [hours/complexity] + - Benefits: [specific improvements] + +2. **[Recommendation 2]** + [... additional recommendations ...] + +## Implementation Checklist + +- [ ] Review duplication findings +- [ ] Prioritize refactoring tasks +- [ ] Create refactoring plan +- [ ] Implement changes +- [ ] Update tests +- [ ] Verify no functionality broken + +## Analysis Metadata + +- **Analyzed Files**: [count] +- **Detection Method**: Serena semantic code analysis +- **Commit**: ${{ github.event.head_commit.id }} +- **Analysis Date**: [timestamp] + +``` + +## Operational Guidelines + +### Security +- Never execute untrusted code or commands +- Only use Serena's read-only analysis tools +- Do not modify files during analysis + +### Efficiency +- Focus on recently changed files first +- Use semantic analysis for meaningful duplication, not superficial matches +- Stay within timeout limits (balance thoroughness with execution time) + +### Accuracy +- Verify findings before reporting +- Distinguish between acceptable patterns and true duplication +- Consider language-specific idioms and best practices +- Provide specific, actionable recommendations + +### Issue Creation +- Create **one issue per distinct duplication pattern** - do NOT bundle multiple patterns in a single issue +- Limit to the top 3 most significant patterns if more are found +- Only create issues if significant duplication is found +- Include sufficient detail for SWE agents to understand and act on findings +- Provide concrete examples with file paths and line numbers +- Suggest practical refactoring approaches +- Assign issue to @copilot for automated remediation +- Use descriptive titles that clearly identify the specific pattern (e.g., "Duplicate Code: Error Handling Pattern in Parser Module") + +## Tool Usage Sequence + +1. **Project Setup**: `activate_project` with repository path +2. **File Discovery**: `list_dir`, `find_file` for changed files +3. **Symbol Analysis**: `get_symbols_overview` for structure understanding +4. **Content Review**: `read_file` for detailed code examination +5. **Pattern Matching**: `search_for_pattern` for similar code +6. **Symbol Search**: `find_symbol` for duplicate function names +7. **Reference Analysis**: `find_referencing_symbols` for usage patterns + +**Objective**: Improve code quality by identifying and reporting meaningful code duplication that impacts maintainability. Focus on actionable findings that enable automated or manual refactoring. +``` diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d17bb5ec191b..c3d65e243846 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -107,8 +107,6 @@ jobs: env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} if: steps.publish-needed.outputs.published == 'false' - env: - NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} run: yarn release:publish --tag ${{ steps.is-prerelease.outputs.prerelease == 'true' && 'next' || 'latest' }} --verbose - name: Get target branch @@ -283,8 +281,6 @@ jobs: env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} working-directory: scripts - env: - NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} run: yarn release:publish --tag canary --verbose - name: Replace Pull Request Body diff --git a/.github/workflows/shared/mood.md b/.github/workflows/shared/mood.md new file mode 100644 index 000000000000..945c9b46d684 --- /dev/null +++ b/.github/workflows/shared/mood.md @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/.github/workflows/shared/reporting.md b/.github/workflows/shared/reporting.md new file mode 100644 index 000000000000..bc08afb42be9 --- /dev/null +++ b/.github/workflows/shared/reporting.md @@ -0,0 +1,73 @@ +--- +# Report formatting guidelines +--- + +## Report Structure Guidelines + +### 1. Header Levels +**Use h3 (###) or lower for all headers in your issue report to maintain proper document hierarchy.** + +When creating GitHub issues or discussions: +- Use `###` (h3) for main sections (e.g., "### Test Summary") +- Use `####` (h4) for subsections (e.g., "#### Device-Specific Results") +- Never use `##` (h2) or `#` (h1) in reports - these are reserved for titles + +### 2. Progressive Disclosure +**Wrap detailed test results in `
Section Name` tags to improve readability and reduce scrolling.** + +Use collapsible sections for: +- Verbose details (full test logs, raw data) +- Secondary information (minor warnings, extra context) +- Per-item breakdowns when there are many items + +Always keep critical information visible (summary, critical issues, key metrics). + +### 3. Report Structure Pattern + +1. **Overview**: 1-2 paragraphs summarizing key findings +2. **Critical Information**: Show immediately (summary stats, critical issues) +3. **Details**: Use `
Section Name` for expanded content +4. **Context**: Add helpful metadata (workflow run, date, trigger) + +### Design Principles (Airbnb-Inspired) + +Reports should: +- **Build trust through clarity**: Most important info immediately visible +- **Exceed expectations**: Add helpful context like trends, comparisons +- **Create delight**: Use progressive disclosure to reduce overwhelm +- **Maintain consistency**: Follow patterns across all reports + +### Example Report Structure + +```markdown +### Summary +- Key metric 1: value +- Key metric 2: value +- Status: ✅/⚠️/❌ + +### Critical Issues +[Always visible - these are important] + +
+View Detailed Results + +[Comprehensive details, logs, traces] + +
+ +
+View All Warnings + +[Minor issues and potential problems] + +
+ +### Recommendations +[Actionable next steps - keep visible] +``` + +## Workflow Run References + +- Format run IDs as links: `[§12345](https://github.com/owner/repo/actions/runs/12345)` +- Include up to 3 most relevant run URLs at end under `**References:**` +- Do NOT add footer attribution (system adds automatically) diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 000000000000..2e510aff5855 --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1,2 @@ +/cache +/project.local.yml diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md new file mode 100644 index 000000000000..f1dbb34bc476 --- /dev/null +++ b/.serena/memories/project_overview.md @@ -0,0 +1,48 @@ +# Storybook - Project Overview + +## Purpose +Storybook is an open-source UI development tool for building, testing, and documenting UI components in isolation. +It supports multiple frontend frameworks (React, Vue, Angular, Svelte, Web Components, Preact, Ember, HTML, etc.) +and integrates with various build tools (Vite, Webpack5). + +## Version +Current version: 10.2.x (as of March 2026) + +## Tech Stack +- **Language**: TypeScript (strict mode), targeting ES2020 +- **Package Manager**: Yarn 4.10.3 (with workspaces) +- **Node.js**: 22.21.1 (specified in `.nvmrc`) +- **Monorepo Tool**: NX (with `--no-cloud` flag required to avoid NX Cloud login issues) +- **Test Runner**: Vitest (primary), Playwright (E2E) +- **Linting**: ESLint 8 +- **Formatting**: Prettier 3.7+ +- **Bundlers**: Vite 7, Webpack 5, esbuild +- **UI Libraries**: React 18, react-aria (use specific submodules, not root imports) +- **Build System**: Custom build via `jiti ./scripts/build/build-package.ts` + +## Repository Structure +``` +storybook/ +├── code/ # Main codebase +│ ├── core/ # Core package (UI, API, manager, preview, server, etc.) +│ ├── addons/ # Official addons (docs, controls, a11y, interactions, vitest, etc.) +│ ├── builders/ # Build integrations (vite, webpack5, manager) +│ ├── frameworks/ # Framework integrations (react-vite, nextjs, angular, vue3-vite, etc.) +│ ├── renderers/ # Framework renderers (react, vue3, svelte, html, etc.) +│ ├── lib/ # Shared libraries (cli, codemod, csf-tools, etc.) +│ ├── presets/ # Preset packages +│ ├── e2e-tests/ # Playwright E2E tests +│ └── .storybook/ # Internal Storybook config (dogfooding) +├── scripts/ # Build/CI/task scripts +├── docs/ # Documentation +├── sandbox/ # Generated sandbox environments for testing +└── test-storybooks/ # Test storybook configurations +``` + +## Key Packages +- `@storybook/core` - Core functionality (UI, API, server, preview, channels, etc.) +- `@storybook/react`, `@storybook/vue3`, etc. - Framework renderers +- `@storybook/react-vite`, `@storybook/nextjs`, etc. - Framework integrations +- `@storybook/addon-docs`, `@storybook/addon-a11y`, etc. - Official addons +- `storybook` - CLI package +- `create-storybook` - Project scaffolding diff --git a/.serena/memories/style_and_conventions.md b/.serena/memories/style_and_conventions.md new file mode 100644 index 000000000000..341ef193bb4b --- /dev/null +++ b/.serena/memories/style_and_conventions.md @@ -0,0 +1,53 @@ +# Code Style & Conventions + +## TypeScript +- **Strict mode** enabled (`strict: true` in tsconfig) +- Target: ES2020, Module: Preserve, ModuleResolution: bundler +- `noImplicitAny: true` +- JSX: preserve +- No emit (handled by build tools, not tsc) + +## Prettier Configuration +- Print width: 100 +- Tab width: 2 +- Single quotes: yes +- Trailing commas: es5 +- Arrow parens: always +- Brace style: 1tbs (one true brace style) +- Import order (via @trivago/prettier-plugin-sort-imports): + 1. `node:` builtins + 2. `vitest`, `@testing-library` + 3. `react`, `react-dom` + 4. `storybook/internal` + 5. `@storybook/[non-addon]` + 6. `@storybook/addon-*` + 7. Third-party modules + 8. Relative imports (`./`, `../`) +- Import order separation: yes (blank lines between groups) +- Import specifiers sorted: yes + +## ESLint Rules (Notable) +- `react-aria` and `react-stately`: must import from specific submodules (e.g., `@react-aria/overlays`), NOT root +- `react-aria-components`: must use `react-aria-components/patched-dist/ComponentX` entrypoints for tree-shaking +- `es-toolkit`: must use sub-exports (e.g., `es-toolkit/array`), NOT root import +- `import-x/no-extraneous-dependencies`: off +- `react/react-in-jsx-scope`: off +- TypeScript `dot-notation` with `allowIndexSignaturePropertyAccess` +- Custom local rules: `no-uncategorized-errors`, `storybook-monorepo-imports`, `no-duplicated-error-codes` + +## Naming Conventions +- Files: kebab-case for most files (e.g., `my-component.ts`) +- Components: PascalCase for React components +- Types/Interfaces: PascalCase +- Variables/functions: camelCase +- Constants: UPPER_SNAKE_CASE for true constants, camelCase otherwise + +## Test Files +- Pattern: `*.test.ts`, `*.test.tsx`, `*.spec.ts`, `*.spec.tsx` +- Stories: `*.stories.ts`, `*.stories.tsx` +- Test fixtures in `__testfixtures__/` directories +- Tests in `__tests__/` directories or alongside source files + +## Monorepo Import Rules +- Internal packages use `workspace:*` for dependencies +- Custom ESLint rule `storybook-monorepo-imports` enforces correct import patterns within the monorepo diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md new file mode 100644 index 000000000000..a63c1ab401d2 --- /dev/null +++ b/.serena/memories/suggested_commands.md @@ -0,0 +1,101 @@ +# Suggested Commands + +## Installation +```bash +yarn install # Install all dependencies (from repo root) +``` + +## Building +```bash +# Compile a single package (always use --no-cloud to avoid NX Cloud issues) +yarn nx compile core --no-cloud +yarn nx compile @storybook/react --no-cloud + +# Compile all packages +yarn nx run-many -t compile --no-cloud + +# Production build of a package +cd code && yarn build +``` + +## Testing +```bash +# Run all unit tests (from repo root or code/) +cd code && yarn test +# or from root: +yarn test + +# Run tests in watch mode +cd code && yarn test:watch + +# Run specific test file +cd code && yarn test:watch -- --project + +# E2E tests (Playwright) +cd code && npx playwright test +``` + +## Linting & Formatting +```bash +# Run all linting +cd code && yarn lint + +# Lint JS/TS only +cd code && yarn lint:js + +# Format with Prettier +cd code && yarn lint:prettier '**/*.{css,html,json,md,yml}' + +# Run knip (unused code detection) +cd code && yarn knip +``` + +## Running the Internal Dev Storybook +```bash +# Must compile core first! +yarn nx compile core --no-cloud + +# Then start the dev server (from code/ dir) +cd code && yarn storybook:ui + +# Or with --ci flag (no interactive): +cd code && yarn storybook:ui --ci + +# Build static storybook +cd code && yarn storybook:ui:build +``` + +## Sandbox / Task System +```bash +# Start a sandbox with a specific template +yarn start # defaults to react-vite/default-ts + +# Run a task with a specific template +yarn task --task dev --template react-vite/default-ts --start-from=install +``` + +## NX Commands +```bash +# Always use --no-cloud flag! +yarn nx compile --no-cloud +yarn nx run-many -t compile --no-cloud +yarn nx show projects --affected +``` + +## Git +```bash +git status +git diff +git log --oneline -10 +git checkout next # main branch is "next" +``` + +## System Utilities (macOS/Darwin) +```bash +ls, cd, pwd, cat, head, tail +grep, find, xargs +curl, wget +python3, node +open # open in default app +pbcopy, pbpaste # clipboard +``` diff --git a/.serena/memories/task_completion_checklist.md b/.serena/memories/task_completion_checklist.md new file mode 100644 index 000000000000..999e01e9da80 --- /dev/null +++ b/.serena/memories/task_completion_checklist.md @@ -0,0 +1,39 @@ +# Task Completion Checklist + +After completing a coding task, run through these steps: + +## 1. TypeScript Compilation +Ensure the modified package(s) compile without errors: +```bash +yarn nx compile --no-cloud +``` + +## 2. Linting +Run lint on the changed files or the whole codebase: +```bash +cd code && yarn lint:js +``` + +## 3. Formatting +Ensure code is properly formatted: +```bash +cd code && yarn lint:prettier '' +``` + +## 4. Unit Tests +Run relevant tests: +```bash +cd code && yarn test +``` + +## 5. Pre-commit Checks +The project uses husky + lint-staged for pre-commit hooks: +- JS/TS files: ESLint with `--fix` +- EJS files: ejslint +- CSS/HTML/JSON/MD/YML: Prettier +- package.json: lint:package + +## Notes +- The main branch is `next` (not `main` or `master`) +- Always use `--no-cloud` with NX commands +- Before starting the dev server, ensure core is compiled: `yarn nx compile core --no-cloud` diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 000000000000..09c6279d5f4b --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,17 @@ +project_name: "storybook" + +languages: + - typescript + +encoding: "utf-8" + +ignore_all_files_in_gitignore: true +ignored_paths: [] + +read_only: false + +excluded_tools: [] +included_optional_tools: [] +fixed_tools: [] + +read_only_memory_patterns: [] diff --git a/CHANGELOG.md b/CHANGELOG.md index 1708a49b53e4..9f72d8afe58d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 10.2.16 + +- CSF-Factories: Fix ConfigFile parser false warning on `definePreview({...}).type()` export default - [#33885](https://github.com/storybookjs/storybook/pull/33885), thanks @copilot-swe-agent! +- Core: Add host/origin validation to requests and websocket connections - [#33835](https://github.com/storybookjs/storybook/pull/33835), thanks @ghengeveld! +- Core: Add vike metadata frameworks - [#33965](https://github.com/storybookjs/storybook/pull/33965), thanks @yannbf! +- Core: Resolve builder preset path correctly in pnpm strict mode - [#34032](https://github.com/storybookjs/storybook/pull/34032), thanks @braedenfoster! +- Core: Update default allowed hosts in host validation middleware - [#34045](https://github.com/storybookjs/storybook/pull/34045), thanks @ghengeveld! + ## 10.2.15 - Core: Storybook failed to load iframe.html when publishing - [#33896](https://github.com/storybookjs/storybook/pull/33896), thanks @danielalanbates! diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 0e865c883a0d..d1454b01be1f 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,11 +1,19 @@ ## 10.3.0-alpha.15 +- Actions: Add expandLevel parameter to configure tree depth - [#33977](https://github.com/storybookjs/storybook/pull/33977), thanks @mixelburg! - Actions: Fix HandlerFunction type to support async callback props - [#33864](https://github.com/storybookjs/storybook/pull/33864), thanks @mixelburg! - Addon-Vitest: Refactor Vitest setup to eliminate the need for a dedicated setup file - [#34025](https://github.com/storybookjs/storybook/pull/34025), thanks @valentinpalkovic! +- Build: Update @types/node - [#34037](https://github.com/storybookjs/storybook/pull/34037), thanks @valentinpalkovic! +- Builder-Vite: Fix cold-cache vitest failures for story paths containing glob special characters - [#34044](https://github.com/storybookjs/storybook/pull/34044), thanks @copilot-swe-agent! - CI:: declare explicit permissions for stale and weekly cron workflows - [#33902](https://github.com/storybookjs/storybook/pull/33902), thanks @Rohan5commit! - Core: Add vike metadata frameworks - [#33965](https://github.com/storybookjs/storybook/pull/33965), thanks @yannbf! - Core: Resolve builder preset path correctly in pnpm strict mode - [#34032](https://github.com/storybookjs/storybook/pull/34032), thanks @braedenfoster! +- Core: Update default allowed hosts in host validation middleware - [#34045](https://github.com/storybookjs/storybook/pull/34045), thanks @ghengeveld! +- Next.js: Add support for v16.2 - [#34046](https://github.com/storybookjs/storybook/pull/34046), thanks @valentinpalkovic! +- UI: Fix code/copy buttons overlap with content - [#33889](https://github.com/storybookjs/storybook/pull/33889), thanks @Sidnioulz! - UI: Fix modal text selection - [#33967](https://github.com/storybookjs/storybook/pull/33967), thanks @Sidnioulz! +- UI: Fix tab navigation after closing addon panel - [#33971](https://github.com/storybookjs/storybook/pull/33971), thanks @copilot-swe-agent! +- UI: Handle kb nav edge cases when preview and panel are hidden - [#33588](https://github.com/storybookjs/storybook/pull/33588), thanks @Sidnioulz! ## 10.3.0-alpha.14 @@ -66,7 +74,7 @@ ## 10.3.0-alpha.7 - Core: Require token for websocket connections - [#33820](https://github.com/storybookjs/storybook/pull/33820), thanks @ghengeveld! -- Next.js: Handle legacyBehavior prop in Link mock component - [#33862](https://github.com/storybookjs/storybook/pull/33862), thanks @yatishgoel! +- Next.js: Handle legacyBehavior prop in Link mock component - [#33862](https://github.com/storybookjs/storybook/pull/33862), thanks @yatishgoel! - Preact: Support inferring props from component types - [#33828](https://github.com/storybookjs/storybook/pull/33828), thanks @JoviDeCroock! ## 10.3.0-alpha.6 @@ -80,7 +88,7 @@ - Builder-Vite: Use relative path for mocker entry in production builds - [#33792](https://github.com/storybookjs/storybook/pull/33792), thanks @DukeDeSouth! - CLI: Support addon-vitest setup when --skip-install is passed - [#33718](https://github.com/storybookjs/storybook/pull/33718), thanks @valentinpalkovic! -- CSF: Fix cross-file story imports in csf-factories codemod - [#33723](https://github.com/storybookjs/storybook/pull/33723), thanks @yatishgoel! +- CSF: Fix cross-file story imports in csf-factories codemod - [#33723](https://github.com/storybookjs/storybook/pull/33723), thanks @yatishgoel! - Compile: reduce VCPUs for CI check task from 4 to 3 - [#33822](https://github.com/storybookjs/storybook/pull/33822), thanks @valentinpalkovic! - Core: Ignore empty files when indexing - [#33782](https://github.com/storybookjs/storybook/pull/33782), thanks @JReinhold! - Globals: Repair dynamicTitle: false for user-defined tools - [#33284](https://github.com/storybookjs/storybook/pull/33284), thanks @ia319! @@ -271,7 +279,7 @@ ## 10.2.0-alpha.8 -- React: Fix several CSF factory bugs - [#33354](https://github.com/storybookjs/storybook/pull/33354), thanks @kasperpeulen! +- React: Fix several CSF factory bugs - [#33354](https://github.com/storybookjs/storybook/pull/33354), thanks @kasperpeulen! ## 10.2.0-alpha.7 @@ -298,7 +306,7 @@ ## 10.2.0-alpha.5 -- Addon-Vitest: Added timeout for fetching localhost 6006 during global setup. - [#33232](https://github.com/storybookjs/storybook/pull/33232), thanks @snippy4! +- Addon-Vitest: Added timeout for fetching localhost 6006 during global setup. - [#33232](https://github.com/storybookjs/storybook/pull/33232), thanks @snippy4! - CLI: Skip vitest transform for CSF Factories in a11y-addon-test automigration - [#31941](https://github.com/storybookjs/storybook/pull/31941), thanks @mrginglymus! - Controls: Allow resetting the Select control - [#33289](https://github.com/storybookjs/storybook/pull/33289), thanks @Sidnioulz! - Core: Ensure /project.json route is up before builders serve local FS - [#33303](https://github.com/storybookjs/storybook/pull/33303), thanks @Sidnioulz! @@ -347,7 +355,6 @@ ## 10.2.0-alpha.0 - ## 10.1.0-beta.6 - Angular: Don't kill dev command by using observables - [#33185](https://github.com/storybookjs/storybook/pull/33185), thanks @valentinpalkovic! @@ -406,7 +413,6 @@ ## 10.1.0-beta.0 - ## 10.1.0-alpha.14 - Angular: Add support for v21 - [#33098](https://github.com/storybookjs/storybook/pull/33098), thanks @valentinpalkovic! @@ -494,7 +500,6 @@ ## 10.1.0-alpha.0 - ## 10.0.0-rc.4 - Core: Add `experimental_devServer` preset - [#32862](https://github.com/storybookjs/storybook/pull/32862), thanks @yannbf! diff --git a/code/addons/docs/src/blocks/components/Preview.stories.tsx b/code/addons/docs/src/blocks/components/Preview.stories.tsx index b2f3b5ceff5f..042aaac4a8c3 100644 --- a/code/addons/docs/src/blocks/components/Preview.stories.tsx +++ b/code/addons/docs/src/blocks/components/Preview.stories.tsx @@ -51,6 +51,19 @@ export const CodeError = () => ( ); +export const ActionBarWrapping = { + render: () => ( + + + + ), + globals: { + viewport: { value: 'mobile1' }, + }, +}; + export const Single = () => ( + )} + {hasValidSource && ( + <> + setExpanded(!expanded)} + variant="ghost" + className={`docblock-code-toggle${expanded ? ' docblock-code-toggle--expanded' : ''}`} + > + {expanded ? 'Hide code' : 'Show code'} + + + + )} + {additionalActionItems.map(({ title, className, onClick, disabled }, index: number) => ( + + ))} + + )} + ); }; diff --git a/code/addons/docs/src/blocks/components/Source.tsx b/code/addons/docs/src/blocks/components/Source.tsx index 5b0e7b36037b..164aadaf38cf 100644 --- a/code/addons/docs/src/blocks/components/Source.tsx +++ b/code/addons/docs/src/blocks/components/Source.tsx @@ -45,6 +45,8 @@ export interface SourceCodeProps { format?: ComponentProps['format']; /** Display the source snippet in a dark mode. */ dark?: boolean; + /** Whether to show the copy button. Defaults to true. */ + copyable?: boolean; } export interface SourceProps extends SourceCodeProps { @@ -91,6 +93,7 @@ const Source: FunctionComponent = ({ code, dark, format = true, + copyable = true, ...rest }) => { const { typography } = useTheme(); @@ -104,7 +107,7 @@ const Source: FunctionComponent = ({ const syntaxHighlighter = ( { + it('should not modify a plain path without special characters', () => { + expect(escapeGlobPath('./src/Button.stories.tsx')).toBe('./src/Button.stories.tsx'); + }); + + it('should escape parentheses in path segments (e.g. Next.js route groups)', () => { + expect(escapeGlobPath('./src/(group)/Button.stories.tsx')).toBe( + './src/\\(group\\)/Button.stories.tsx' + ); + }); + + it('should escape square brackets in path segments', () => { + expect(escapeGlobPath('./src/[id]/Button.stories.tsx')).toBe( + './src/\\[id\\]/Button.stories.tsx' + ); + }); + + it('should escape curly braces in path segments', () => { + expect(escapeGlobPath('./src/{group}/Button.stories.tsx')).toBe( + './src/\\{group\\}/Button.stories.tsx' + ); + }); + + it('should escape glob wildcard characters', () => { + expect(escapeGlobPath('./src/Button*.stories.tsx')).toBe('./src/Button\\*.stories.tsx'); + expect(escapeGlobPath('./src/Button?.stories.tsx')).toBe('./src/Button\\?.stories.tsx'); + }); + + it('should escape all special glob characters together', () => { + expect(escapeGlobPath('./src/(group)/[id]/{name}/*.stories.tsx')).toBe( + './src/\\(group\\)/\\[id\\]/\\{name\\}/\\*.stories.tsx' + ); + }); + + it('should not modify paths that contain no special glob characters', () => { + expect(escapeGlobPath('./src/my-component/Button.stories.tsx')).toBe( + './src/my-component/Button.stories.tsx' + ); + }); +}); diff --git a/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts index 7bee90301998..e75ab293dff1 100644 --- a/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts +++ b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts @@ -8,6 +8,16 @@ import { type Plugin } from 'vite'; import { processPreviewAnnotation } from '../utils/process-preview-annotation'; import { getUniqueImportPaths } from '../utils/unique-import-paths'; +/** + * Escapes special glob characters in a file path so Vite's dep optimizer treats it as a literal + * path rather than a glob pattern. This is necessary for paths containing characters like `(` and + * `)` (e.g. Next.js route group directories such as `src/(group)/...`) which would otherwise be + * interpreted as extglob patterns by fast-glob. + */ +export function escapeGlobPath(filePath: string): string { + return filePath.replace(/[()[\]{}!*?|+@]/g, '\\$&'); +} + /** A Vite plugin that configures dependency optimization for Storybook's dev server. */ export function storybookOptimizeDepsPlugin(options: Options): Plugin { return { @@ -40,12 +50,14 @@ export function storybookOptimizeDepsPlugin(options: Options): Plugin { // Story files + preview annotation files as entry points for the dep optimizer. // Vite will crawl these to discover all transitive CJS dependencies that need // pre-bundling, removing the need for a hard-coded include list. + // Paths are escaped so that special glob characters (e.g. parentheses in Next.js route + // group directories) are treated as literal characters, not glob syntax. entries: [ ...(typeof config.optimizeDeps?.entries === 'string' ? [config.optimizeDeps.entries] : (config.optimizeDeps?.entries ?? [])), - ...getUniqueImportPaths(index), - ...previewAnnotationEntries, + ...getUniqueImportPaths(index).map(escapeGlobPath), + ...previewAnnotationEntries.map(escapeGlobPath), ], // Extra deps explicitly included by Storybook presets (e.g. framework-specific packages). include: [...extraOptimizeDeps, ...(config.optimizeDeps?.include ?? [])], diff --git a/code/core/src/actions/components/ActionLogger/index.tsx b/code/core/src/actions/components/ActionLogger/index.tsx index 1da7339e09c8..4ab90a5b9130 100644 --- a/code/core/src/actions/components/ActionLogger/index.tsx +++ b/code/core/src/actions/components/ActionLogger/index.tsx @@ -30,6 +30,7 @@ interface InspectorProps { showNonenumerable: boolean; name: any; data: any; + expandLevel?: number; } const ThemedInspector = withTheme(({ theme, ...props }: InspectorProps) => ( @@ -38,10 +39,11 @@ const ThemedInspector = withTheme(({ theme, ...props }: InspectorProps) => ( interface ActionLoggerProps { actions: ActionDisplay[]; + expandLevel?: number; onClear: () => void; } -export const ActionLogger = ({ actions, onClear }: ActionLoggerProps) => { +export const ActionLogger = ({ actions, expandLevel, onClear }: ActionLoggerProps) => { const wrapperRef = useRef>(null); const wrapper = wrapperRef.current; const wasAtBottom = wrapper && wrapper.scrollHeight - wrapper.scrollTop === wrapper.clientHeight; @@ -67,6 +69,7 @@ export const ActionLogger = ({ actions, onClear }: ActionLoggerProps) => { showNonenumerable={false} name={action.data.name} data={action.data.args ?? action.data} + expandLevel={expandLevel} /> diff --git a/code/core/src/actions/containers/ActionLogger/index.tsx b/code/core/src/actions/containers/ActionLogger/index.tsx index e8ae2bab4566..b56398b11b24 100644 --- a/code/core/src/actions/containers/ActionLogger/index.tsx +++ b/code/core/src/actions/containers/ActionLogger/index.tsx @@ -1,23 +1,21 @@ -import React, { Component } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { STORY_CHANGED } from 'storybook/internal/core-events'; import { dequal as deepEqual } from 'dequal'; import type { API } from 'storybook/manager-api'; +import { useParameter } from 'storybook/manager-api'; import { ActionLogger as ActionLoggerComponent } from '../../components/ActionLogger'; -import { CLEAR_ID, EVENT_ID } from '../../constants'; +import { CLEAR_ID, EVENT_ID, PARAM_KEY } from '../../constants'; import type { ActionDisplay } from '../../models'; +import type { ActionsParameters } from '../../types'; interface ActionLoggerProps { active: boolean; api: API; } -interface ActionLoggerState { - actions: ActionDisplay[]; -} - const safeDeepEqual = (a: any, b: any): boolean => { try { return deepEqual(a, b); @@ -26,69 +24,54 @@ const safeDeepEqual = (a: any, b: any): boolean => { } }; -export default class ActionLogger extends Component { - private mounted: boolean; - - constructor(props: ActionLoggerProps) { - super(props); - - this.mounted = false; - - this.state = { actions: [] }; - } - - override componentDidMount() { - this.mounted = true; - const { api } = this.props; - - api.on(EVENT_ID, this.addAction); - api.on(STORY_CHANGED, this.handleStoryChange); - } - - override componentWillUnmount() { - this.mounted = false; - const { api } = this.props; - - api.off(STORY_CHANGED, this.handleStoryChange); - api.off(EVENT_ID, this.addAction); - } +export default function ActionLogger({ active, api }: ActionLoggerProps) { + const [actions, setActions] = useState([]); + const parameter = useParameter(PARAM_KEY); + const expandLevel = parameter?.expandLevel ?? 1; - handleStoryChange = () => { - const { actions } = this.state; - if (actions.length > 0 && actions[0].options.clearOnStoryChange) { - this.clearActions(); - } - }; + const clearActions = useCallback(() => { + api.emit(CLEAR_ID); + setActions([]); + }, [api]); - addAction = (action: ActionDisplay) => { - this.setState((prevState: ActionLoggerState) => { - const actions = [...prevState.actions]; - const previous = actions.length && actions[actions.length - 1]; + const addAction = useCallback((action: ActionDisplay) => { + setActions((prevActions) => { + const newActions = [...prevActions]; + const previous = newActions.length && newActions[newActions.length - 1]; if (previous && safeDeepEqual(previous.data, action.data)) { previous.count++; } else { action.count = 1; - actions.push(action); + newActions.push(action); } - return { actions: actions.slice(0, action.options.limit) }; + return newActions.slice(0, action.options.limit); }); - }; + }, []); - clearActions = () => { - const { api } = this.props; + const handleStoryChange = useCallback(() => { + if (actions.length > 0 && actions[0].options.clearOnStoryChange) { + clearActions(); + } + }, [actions, clearActions]); - // clear number of actions - api.emit(CLEAR_ID); - this.setState({ actions: [] }); - }; + useEffect(() => { + api.on(EVENT_ID, addAction); + api.on(STORY_CHANGED, handleStoryChange); - override render() { - const { actions = [] } = this.state; - const { active } = this.props; - const props = { - actions, - onClear: this.clearActions, + return () => { + api.off(EVENT_ID, addAction); + api.off(STORY_CHANGED, handleStoryChange); }; - return active ? : null; - } + }, [api, addAction, handleStoryChange]); + + const props = useMemo( + () => ({ + actions, + expandLevel, + onClear: clearActions, + }), + [actions, expandLevel, clearActions] + ); + + return active ? : null; } diff --git a/code/core/src/actions/types.ts b/code/core/src/actions/types.ts index 72d6fe17f950..c45588f6ab20 100644 --- a/code/core/src/actions/types.ts +++ b/code/core/src/actions/types.ts @@ -38,6 +38,13 @@ export interface ActionsParameters { * @example `handles: ['mouseover', 'click .btn']` */ handles?: string[]; + + /** + * An integer specifying to which level the tree should be initially expanded. + * + * @default 1 + */ + expandLevel?: number; }; } diff --git a/code/core/src/backgrounds/decorator.ts b/code/core/src/backgrounds/decorator.ts index bb0d35de9cfc..a58e7e14cfb0 100644 --- a/code/core/src/backgrounds/decorator.ts +++ b/code/core/src/backgrounds/decorator.ts @@ -34,8 +34,14 @@ export const withBackgroundAndGrid: DecoratorFunction = (StoryFn, context) => { const showGrid = typeof data === 'string' ? false : data.grid || false; const shownBackground = !!item && !disable; - const backgroundSelector = viewMode === 'docs' ? `#anchor--${id} .docs-story` : '.sb-show-main'; - const gridSelector = viewMode === 'docs' ? `#anchor--${id} .docs-story` : '.sb-show-main'; + const backgroundSelector = + viewMode === 'docs' + ? `#anchor--${id} .docs-story, #anchor--primary--${id} .docs-story` + : '.sb-show-main'; + const gridSelector = + viewMode === 'docs' + ? `#anchor--${id} .docs-story, #anchor--primary--${id} .docs-story` + : '.sb-show-main'; const isLayoutPadded = parameters.layout === undefined || parameters.layout === 'padded'; const defaultOffset = viewMode === 'docs' ? 20 : isLayoutPadded ? 16 : 0; diff --git a/code/core/src/components/components/ActionBar/ActionBar.tsx b/code/core/src/components/components/ActionBar/ActionBar.tsx index 25dd5c668fef..59f6694501f5 100644 --- a/code/core/src/components/components/ActionBar/ActionBar.tsx +++ b/code/core/src/components/components/ActionBar/ActionBar.tsx @@ -3,15 +3,25 @@ import React from 'react'; import { styled } from 'storybook/theming'; -const Container = styled.div(({ theme }) => ({ - position: 'absolute', - bottom: 0, - right: 0, - maxWidth: '100%', - display: 'flex', - background: theme.background.content, - zIndex: 1, -})); +const Container = styled.div<{ $flexLayout?: boolean }>(({ theme, $flexLayout = false }) => [ + { + background: theme.background.content, + }, + $flexLayout + ? { + display: 'inline-flex', + marginInlineStart: 'auto', + alignSelf: 'flex-end', + } + : { + position: 'absolute', + bottom: 0, + right: 0, + maxWidth: '100%', + display: 'flex', + zIndex: 1, + }, +]); export const ActionButton = styled.button<{ disabled: boolean }>( ({ theme }) => ({ @@ -67,15 +77,23 @@ export interface ActionItem { } export interface ActionBarProps { + /** Items to render in this ActionBar. */ actionItems: ActionItem[]; + /** + * When true, ActionBar aligns to the flex end and inline end of a wrapping row flex container. + * When false, ActionBar is positioned absolutely at the bottom right of its relative parent. + */ + flexLayout?: boolean; } -export const ActionBar = ({ actionItems, ...props }: ActionBarProps) => ( - - {actionItems.map(({ title, className, onClick, disabled }, index: number) => ( - - {title} - - ))} - -); +export const ActionBar = ({ actionItems, flexLayout = false, ...props }: ActionBarProps) => { + return ( + + {actionItems.map(({ title, className, onClick, disabled }, index: number) => ( + + {title} + + ))} + + ); +}; diff --git a/code/core/src/components/components/syntaxhighlighter/syntaxhighlighter.tsx b/code/core/src/components/components/syntaxhighlighter/syntaxhighlighter.tsx index 494afdc587e2..80fe57c24823 100644 --- a/code/core/src/components/components/syntaxhighlighter/syntaxhighlighter.tsx +++ b/code/core/src/components/components/syntaxhighlighter/syntaxhighlighter.tsx @@ -68,6 +68,8 @@ export interface WrapperProps { const Wrapper = styled.div( ({ theme }) => ({ position: 'relative', + display: 'flex', + flexWrap: 'wrap', overflow: 'hidden', color: theme.color.defaultText, }), @@ -97,7 +99,10 @@ const UnstyledScroller = ({ children, className }: ScrollAreaProps) => ( ); const Scroller = styled(UnstyledScroller)( { - position: 'relative', + flex: 1, + flexShrink: 0, + flexBasis: 'fit-content', + maxWidth: '100%', }, ({ theme }) => themedSyntax(theme) ); @@ -247,7 +252,7 @@ export const SyntaxHighlighter = ({ {copyable ? ( - + ) : null} ); diff --git a/code/core/src/core-server/utils/__tests__/getHostValidationMiddleware.test.ts b/code/core/src/core-server/utils/__tests__/getHostValidationMiddleware.test.ts index 5df097be2e49..fb6a9e958408 100644 --- a/code/core/src/core-server/utils/__tests__/getHostValidationMiddleware.test.ts +++ b/code/core/src/core-server/utils/__tests__/getHostValidationMiddleware.test.ts @@ -155,10 +155,9 @@ describe('isValidHost', () => { expect(isValidHost('127.0.0.1:6006', options)).toBe(true); }); - // TODO: Change default to [] in SB11 - it('when allowedHosts is undefined (default true), allows any host', () => { - expect(isValidHost('malicious-site.com', options)).toBe(true); - expect(isValidHost('any-origin.example.com', options)).toBe(true); + it('when allowedHosts is undefined (default []), rejects foreign hosts', () => { + expect(isValidHost('malicious-site.com', options)).toBe(false); + expect(isValidHost('any-origin.example.com', options)).toBe(false); }); it('when allowedHosts is true, allows any host', () => { diff --git a/code/core/src/core-server/utils/getHostValidationMiddleware.ts b/code/core/src/core-server/utils/getHostValidationMiddleware.ts index d918627ff6b6..cb89c45a8256 100644 --- a/code/core/src/core-server/utils/getHostValidationMiddleware.ts +++ b/code/core/src/core-server/utils/getHostValidationMiddleware.ts @@ -4,8 +4,7 @@ import { isHostAllowed } from 'host-validation-middleware'; import type { Middleware } from '../../types'; -// TODO: Change to `[]` in SB11 to change from opt-in to opt-out -export const DEFAULT_ALLOWED_HOSTS: string[] | true = true; +export const DEFAULT_ALLOWED_HOSTS: string[] | true = []; export type HostValidationOptions = { host?: string; diff --git a/code/core/src/manager-api/modules/layout.ts b/code/core/src/manager-api/modules/layout.ts index 27ed403409da..a0df07067e5b 100644 --- a/code/core/src/manager-api/modules/layout.ts +++ b/code/core/src/manager-api/modules/layout.ts @@ -102,6 +102,21 @@ export interface SubAPI { * account customisations requested by the end user via a layoutCustomisations function. */ getNavSizeWithCustomisations: (navSize: number) => number; + /** + * Attempts to focus an element identified by its ID. + * + * @param elementId - The id of the element to focus. + * @param options - Options for focusing the element. + * @param options.forceFocus - Whether to make the element focusable even though it wasn't. + * @param options.select - Whether to call select() on the element after focusing it. + * @param options.poll - Whether to poll for the element if it is not immediately available. + * Defaults to true. When true, polls every 50ms for up to 500ms. + * @returns Whether the element was successfully focused. Returns a Promise when polling. + */ + focusOnUIElement: ( + elementId?: string, + options?: boolean | { forceFocus?: boolean; select?: boolean; poll?: boolean } + ) => boolean | Promise; } type PartialSubState = Partial; @@ -133,9 +148,11 @@ export const defaultLayoutState: SubState = { }; export const focusableUIElements = { + addonPanel: 'storybook-panel-region', storySearchField: 'storybook-explorer-searchfield', storyListMenu: 'storybook-explorer-menu', storyPanelRoot: 'storybook-panel-root', + showAddonPanel: 'storybook-show-addon-panel', }; const getIsNavShown = (state: State) => { @@ -330,25 +347,32 @@ export const init: ModuleFn = ({ store, provider, singleStory /** * Attempts to focus (and select) an element identified by its ID. It is the responsibility of * the callee to ensure that the element is present in the DOM and that no focus trap is - * available. This API polls and attempts to perform the focus for a set duration (max 500ms), - * so that race conditions can be avoided with the current API design. Because this API is - * historically synchronous, it cannot report errors or failure to focus. It fails silently. + * available. When polling is enabled, this API polls and attempts to perform the focus for a + * set duration (max 500ms), so that race conditions can be avoided with the current API + * design. * * @param elementId The id of the element to focus. - * @param select Whether to call select() on the element after focusing it. + * @param options When a boolean, treated as the `select` option for backwards compatibility. + * When an object, may contain `select` and `poll` options. + * @returns Whether the element was successfully focused. Returns a Promise when polling. */ - focusOnUIElement(elementId?: string, select?: boolean) { + focusOnUIElement( + elementId?: string, + options?: boolean | { forceFocus?: boolean; select?: boolean; poll?: boolean } + ): boolean | Promise { // See RFC https://github.com/storybookjs/storybook/discussions/32983 for // ways to make this API more robust to focus-trap race conditions. + const { + forceFocus = false, + select = false, + poll = true, + } = typeof options === 'boolean' ? { select: options } : (options ?? {}); + if (!elementId) { - return; + return false; } - const startTime = Date.now(); - const maxDuration = 500; - const pollInterval = 50; - const attemptFocus = () => { const element = document.getElementById(elementId); if (!element) { @@ -356,7 +380,16 @@ export const init: ModuleFn = ({ store, provider, singleStory } element.focus(); - if (element !== document.activeElement) { + if ( + element !== document.activeElement && + forceFocus && + element.getAttribute('tabindex') === null + ) { + element.setAttribute('tabindex', '-1'); + element.focus(); + } + + if (element !== document.activeElement && element.id !== document.activeElement?.id) { return false; } @@ -367,22 +400,34 @@ export const init: ModuleFn = ({ store, provider, singleStory }; if (attemptFocus()) { - return; + return true; } - // Poll every 50ms for up to 500ms to account for race conditions. - const intervalId = setInterval(() => { - const elapsed = Date.now() - startTime; + if (!poll) { + return false; + } - if (elapsed >= maxDuration) { - clearInterval(intervalId); - return; - } + // Poll every 50ms for up to 500ms to account for race conditions. + return new Promise((resolve) => { + const startTime = Date.now(); + const maxDuration = 500; + const pollInterval = 50; + + const intervalId = setInterval(() => { + const elapsed = Date.now() - startTime; + + if (attemptFocus()) { + clearInterval(intervalId); + resolve(true); + return; + } - if (attemptFocus()) { - clearInterval(intervalId); - } - }, pollInterval); + if (elapsed >= maxDuration) { + clearInterval(intervalId); + resolve(false); + } + }, pollInterval); + }); }, getInitialOptions() { diff --git a/code/core/src/manager-api/modules/shortcuts.ts b/code/core/src/manager-api/modules/shortcuts.ts index e0dc374bea16..83520110c131 100644 --- a/code/core/src/manager-api/modules/shortcuts.ts +++ b/code/core/src/manager-api/modules/shortcuts.ts @@ -358,7 +358,26 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => { } case 'togglePanel': { + const wasPanelShown = fullAPI.getIsPanelShown(); + const panelElement = document.getElementById(focusableUIElements.storyPanelRoot); + const wasFocusInPanel = + panelElement && document.activeElement && panelElement.contains(document.activeElement); + fullAPI.togglePanel(); + + if (wasPanelShown && wasFocusInPanel) { + // poll: true always returns a Promise. + ( + fullAPI.focusOnUIElement(focusableUIElements.showAddonPanel, { + poll: true, + }) as Promise + ).then((success) => { + // Fallback to body for predictable behavior. + if (success === false) { + document.body.focus(); + } + }); + } break; } diff --git a/code/core/src/manager-api/tests/layout.test.ts b/code/core/src/manager-api/tests/layout.test.ts index f2c8f80c032b..0c382d586455 100644 --- a/code/core/src/manager-api/tests/layout.test.ts +++ b/code/core/src/manager-api/tests/layout.test.ts @@ -1,5 +1,5 @@ import type { Mock } from 'vitest'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { API_Provider } from 'storybook/internal/types'; @@ -540,4 +540,142 @@ describe('layout API', () => { expect(layoutApi.getIsFullscreen()).toBe(false); }); }); + + describe('focusOnUIElement', () => { + let mockActiveElement: any; + let mockGetElementById: ReturnType; + let focusLayoutApi: SubAPI; + + beforeEach(async () => { + mockActiveElement = null; + mockGetElementById = vi.fn().mockReturnValue(null); + + // Set up mock document on globalThis before re-importing layout module. + // @storybook/global resolves to globalThis in Node, so the layout module's + // `const { document } = global;` will capture this mock. + (globalThis as any).document = { + getElementById: mockGetElementById, + get activeElement() { + return mockActiveElement; + }, + }; + + // Re-import the layout module so it captures our mock document + vi.resetModules(); + const { init: freshInit } = await import('../modules/layout'); + focusLayoutApi = freshInit({ + store, + provider, + singleStory: false, + } as unknown as ModuleArgs).api; + }); + + afterEach(() => { + delete (globalThis as any).document; + vi.restoreAllMocks(); + }); + + const createMockElement = (id: string) => { + const element = { + id, + focus: vi.fn(() => { + mockActiveElement = element; + }), + select: vi.fn(), + }; + mockGetElementById.mockImplementation((queryId: string) => (queryId === id ? element : null)); + return element; + }; + + it('should return false when elementId is not provided', () => { + const result = focusLayoutApi.focusOnUIElement(); + expect(result).toBe(false); + }); + + it('should return false when elementId is undefined', () => { + const result = focusLayoutApi.focusOnUIElement(undefined); + expect(result).toBe(false); + }); + + it('should return true and focus element when element exists', () => { + const element = createMockElement('test-element'); + const result = focusLayoutApi.focusOnUIElement('test-element'); + expect(result).toBe(true); + expect(element.focus).toHaveBeenCalled(); + }); + + it('should return true and call select when select option is true (boolean form)', () => { + const element = createMockElement('test-element'); + const result = focusLayoutApi.focusOnUIElement('test-element', true); + expect(result).toBe(true); + expect(element.focus).toHaveBeenCalled(); + expect(element.select).toHaveBeenCalled(); + }); + + it('should return true and call select when select option is true (object form)', () => { + const element = createMockElement('test-element'); + const result = focusLayoutApi.focusOnUIElement('test-element', { select: true }); + expect(result).toBe(true); + expect(element.focus).toHaveBeenCalled(); + expect(element.select).toHaveBeenCalled(); + }); + + it('should not call select when select option is false', () => { + const element = createMockElement('test-element'); + const result = focusLayoutApi.focusOnUIElement('test-element', { select: false }); + expect(result).toBe(true); + expect(element.focus).toHaveBeenCalled(); + expect(element.select).not.toHaveBeenCalled(); + }); + + it('should return false without polling when element does not exist and poll is false', () => { + const result = focusLayoutApi.focusOnUIElement('nonexistent-element', { poll: false }); + expect(result).toBe(false); + }); + + it('should return a Promise when element does not exist and poll is true (default)', () => { + const result = focusLayoutApi.focusOnUIElement('nonexistent-element'); + expect(result).toBeInstanceOf(Promise); + }); + + it('should resolve to true when element appears during polling', async () => { + vi.useFakeTimers(); + + const element = { + id: 'delayed-element', + focus: vi.fn(), + select: vi.fn(), + }; + + // Element not available initially + const result = focusLayoutApi.focusOnUIElement('delayed-element'); + expect(result).toBeInstanceOf(Promise); + + // Make element available and focusable + mockGetElementById.mockImplementation((id: string) => + id === 'delayed-element' ? element : null + ); + element.focus.mockImplementation(() => { + mockActiveElement = element; + }); + + await vi.advanceTimersByTimeAsync(150); + await expect(result).resolves.toBe(true); + expect(element.focus).toHaveBeenCalled(); + + vi.useRealTimers(); + }); + + it('should resolve to false when element never appears during polling', async () => { + vi.useFakeTimers(); + + const result = focusLayoutApi.focusOnUIElement('never-appears'); + expect(result).toBeInstanceOf(Promise); + + await vi.advanceTimersByTimeAsync(600); + await expect(result).resolves.toBe(false); + + vi.useRealTimers(); + }); + }); }); diff --git a/code/core/src/manager/components/layout/Drag.tsx b/code/core/src/manager/components/layout/Drag.tsx new file mode 100644 index 000000000000..ba1fe4053d3d --- /dev/null +++ b/code/core/src/manager/components/layout/Drag.tsx @@ -0,0 +1,65 @@ +import { styled } from 'storybook/theming'; + +/** + * Drag handle for the sidebar and panel resizers. Can be horizontal (bottom panel) or vertical + * (sidebar or right panel). Can optionally be set to not overlap the content area (only render + * outside of it), which is necessary when the panel is collapsed to prevent a layout shift when + * scrollIntoView is used. + */ +export const Drag = styled.div<{ + orientation?: 'horizontal' | 'vertical'; + overlapping?: boolean; + position?: 'left' | 'right'; +}>( + ({ theme }) => ({ + position: 'absolute', + opacity: 0, + transition: 'opacity 0.2s ease-in-out', + zIndex: 100, + + '&:after': { + content: '""', + display: 'block', + backgroundColor: theme.color.secondary, + }, + + '&:hover': { + opacity: 1, + }, + }), + ({ orientation = 'vertical', overlapping = true, position = 'left' }) => + orientation === 'vertical' + ? { + width: overlapping ? (position === 'left' ? 10 : 13) : 7, + height: '100%', + top: 0, + right: position === 'left' ? -7 : undefined, + left: position === 'right' ? -7 : undefined, + + '&:after': { + width: 1, + height: '100%', + marginLeft: position === 'left' ? 3 : 6, + }, + + '&:hover': { + cursor: 'col-resize', + }, + } + : { + width: '100%', + height: overlapping ? 13 : 7, + top: -7, + left: 0, + + '&:after': { + width: '100%', + height: 1, + marginTop: 6, + }, + + '&:hover': { + cursor: 'row-resize', + }, + } +); diff --git a/code/core/src/manager/components/layout/Layout.stories.tsx b/code/core/src/manager/components/layout/Layout.stories.tsx index 2173924c035f..1cabd5957091 100644 --- a/code/core/src/manager/components/layout/Layout.stories.tsx +++ b/code/core/src/manager/components/layout/Layout.stories.tsx @@ -8,7 +8,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { startCase } from 'es-toolkit/string'; import { action } from 'storybook/actions'; import { ManagerContext } from 'storybook/manager-api'; -import { fn } from 'storybook/test'; +import { expect, fn } from 'storybook/test'; import { styled } from 'storybook/theming'; import { isChromatic } from '../../../../../.storybook/isChromatic'; @@ -24,7 +24,7 @@ const PlaceholderBlock = styled.div({ overflow: 'hidden', }); -const PlaceholderClock: FC = ({ children }) => { +const PlaceholderClock: FC<{ id: string } & PropsWithChildren> = ({ children, id }) => { const [count, setCount] = React.useState(0); React.useEffect(() => { if (isChromatic()) { @@ -38,18 +38,21 @@ const PlaceholderClock: FC = ({ children }) => { return (

{count}

+ {children}
); }; -const MockSidebar: FC = () => ; +const MockSidebar: FC = () => ; -const MockPreview: FC = () => ; +const MockPreview: FC = () => ; -const MockPanel: FC = () => ; +const MockPanel: FC = () => ; -const MockPage: FC = () => ; +const MockPage: FC = () => ; const defaultState = { navSize: 150, @@ -145,18 +148,65 @@ export const DesktopHorizontal: Story = { args: { managerLayoutState: { ...defaultState, panelPosition: 'right' }, }, + play: async ({ canvas, step }) => { + await step('Verify preview can be focused', async () => { + const preview = canvas.getByTestId('preview'); + preview.focus(); + expect(preview).toHaveFocus(); + }); + await step('Verify panel can be focused', async () => { + const panel = canvas.getByTestId('panel'); + panel.focus(); + expect(panel).toHaveFocus(); + }); + }, +}; + +export const DesktopCollapsedPanel: Story = { + args: { + managerLayoutState: { ...defaultState, bottomPanelHeight: 0 }, + }, + play: async ({ canvas, step }) => { + await step('Verify panel is not rendered', async () => { + const panel = canvas.queryByTestId('panel'); + expect(panel).not.toBeInTheDocument(); + }); + }, }; export const DesktopDocs: Story = { args: { managerLayoutState: { ...defaultState, viewMode: 'docs' }, }, + play: async ({ canvas, step }) => { + await step('Verify pages main landmark is not rendered', async () => { + const pagesMain = canvas.queryByRole('main', { name: 'Main content' }); + expect(pagesMain).not.toBeInTheDocument(); + }); + await step('Verify preview area is rendered', async () => { + const preview = canvas.getByTestId('preview'); + expect(preview).toBeInTheDocument(); + }); + }, }; export const DesktopPages: Story = { args: { managerLayoutState: { ...defaultState, viewMode: 'settings' }, }, + play: async ({ canvas, step }) => { + await step('Verify pages main landmark is rendered', async () => { + const pagesMain = canvas.queryByRole('main', { name: 'Main content' }); + expect(pagesMain).toBeInTheDocument(); + const page = canvas.getByTestId('page'); + page.focus(); + expect(page).toHaveFocus(); + }); + await step('Verify preview area is not rendered', async () => { + const preview = canvas.queryByTestId('preview'); + expect(preview).not.toBeInTheDocument(); + }); + }, }; export const Mobile = { diff --git a/code/core/src/manager/components/layout/Layout.tsx b/code/core/src/manager/components/layout/Layout.tsx index 20c120169944..f1b019d5a3d3 100644 --- a/code/core/src/manager/components/layout/Layout.tsx +++ b/code/core/src/manager/components/layout/Layout.tsx @@ -1,7 +1,6 @@ -import type { CSSProperties } from 'react'; +import type { CSSProperties, FC } from 'react'; import React, { useEffect, useLayoutEffect, useState } from 'react'; -import { Match } from 'storybook/internal/router'; import type { API_Layout, API_ViewMode } from 'storybook/internal/types'; import { type API, useStorybookApi } from 'storybook/manager-api'; @@ -10,7 +9,10 @@ import { styled } from 'storybook/theming'; import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants'; import { Notifications } from '../../container/Notifications'; import { MobileNavigation } from '../mobile/navigation/MobileNavigation'; +import { Drag } from './Drag'; import { useLayout } from './LayoutProvider'; +import { MainAreaContainer } from './MainAreaContainer'; +import { PanelContainer } from './PanelContainer'; import { useDragging } from './useDragging'; import { useLandmarkIndicator } from './useLandmarkIndicator'; @@ -104,7 +106,9 @@ const useLayoutSyncingState = ({ }, [internalDraggingSizeState, setManagerLayoutState]); const isPagesShown = - managerLayoutState.viewMode !== 'story' && managerLayoutState.viewMode !== 'docs'; + managerLayoutState.viewMode !== undefined && + managerLayoutState.viewMode !== 'story' && + managerLayoutState.viewMode !== 'docs'; const isPanelShown = managerLayoutState.viewMode === 'story' && !hasTab; const { panelResizerRef, sidebarResizerRef } = useDragging({ @@ -127,19 +131,13 @@ const useLayoutSyncingState = ({ panelResizerRef, sidebarResizerRef, showPages: isPagesShown, - showPanel: customisedShowPanel, + showPanel: + customisedShowPanel && + (managerLayoutState.panelPosition === 'right' ? rightPanelWidth > 0 : bottomPanelHeight > 0), isDragging: internalDraggingSizeState.isDragging, }; }; -const MainContentMatcher = ({ children }: { children: React.ReactNode }) => { - return ( - - {({ match }) => {children}} - - ); -}; - const OrderedMobileNavigation = styled(MobileNavigation)({ order: 1, }); @@ -157,7 +155,6 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s sidebarResizerRef, showPages, showPanel, - isDragging, } = useLayoutSyncingState({ api, managerLayoutState, setManagerLayoutState, isDesktop, hasTab }); // Install landmark navigation listener in parent container of all landmarks. @@ -175,7 +172,6 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s } as CSSProperties } > - {showPages && {slots.slotPages}} <> {isDesktop && ( @@ -191,16 +187,19 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s /> )} - {slots.slotMain} + {isDesktop && showPanel && ( - - + {slots.slotPanel} )} @@ -249,104 +248,3 @@ const SidebarContainer = styled.div(({ theme }) => ({ position: 'relative', borderRight: `1px solid ${theme.appBorderColor}`, })); - -const ContentContainer = styled.div<{ shown: boolean }>(({ theme, shown }) => ({ - flex: 1, - position: 'relative', - backgroundColor: theme.appContentBg, - display: shown ? 'grid' : 'none', // This is needed to make the content container fill the available space - overflow: 'auto', - - [MEDIA_DESKTOP_BREAKPOINT]: { - flex: 'auto', - gridArea: 'content', - }, -})); - -const PagesContainer = styled.div(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - gridRowStart: 'sidebar-start', - gridRowEnd: '-1', - gridColumnStart: 'sidebar-end', - gridColumnEnd: '-1', - backgroundColor: theme.appContentBg, - zIndex: 1, -})); - -const PanelContainer = styled.div<{ position: LayoutState['panelPosition'] }>( - ({ theme, position }) => ({ - gridArea: 'panel', - position: 'relative', - backgroundColor: theme.appContentBg, - borderTop: position === 'bottom' ? `1px solid ${theme.appBorderColor}` : undefined, - borderLeft: position === 'right' ? `1px solid ${theme.appBorderColor}` : undefined, - '& > aside': { - overflow: 'hidden', - }, - }) -); - -/** - * Drag handle for the sidebar and panel resizers. Can be horizontal (bottom panel) or vertical - * (sidebar or right panel). Can optionally be set to not overlap the content area (only render - * outside of it), which is necessary when the panel is collapsed to prevent a layout shift when - * scrollIntoView is used. - */ -const Drag = styled.div<{ - orientation?: 'horizontal' | 'vertical'; - overlapping?: boolean; - position?: 'left' | 'right'; -}>( - ({ theme }) => ({ - position: 'absolute', - opacity: 0, - transition: 'opacity 0.2s ease-in-out', - zIndex: 100, - - '&:after': { - content: '""', - display: 'block', - backgroundColor: theme.color.secondary, - }, - - '&:hover': { - opacity: 1, - }, - }), - ({ orientation = 'vertical', overlapping = true, position = 'left' }) => - orientation === 'vertical' - ? { - width: overlapping ? (position === 'left' ? 10 : 13) : 7, - height: '100%', - top: 0, - right: position === 'left' ? -7 : undefined, - left: position === 'right' ? -7 : undefined, - - '&:after': { - width: 1, - height: '100%', - marginLeft: position === 'left' ? 3 : 6, - }, - - '&:hover': { - cursor: 'col-resize', - }, - } - : { - width: '100%', - height: overlapping ? 13 : 7, - top: -7, - left: 0, - - '&:after': { - width: '100%', - height: 1, - marginTop: 6, - }, - - '&:hover': { - cursor: 'row-resize', - }, - } -); diff --git a/code/core/src/manager/components/layout/MainAreaContainer.tsx b/code/core/src/manager/components/layout/MainAreaContainer.tsx new file mode 100644 index 000000000000..401283fd529a --- /dev/null +++ b/code/core/src/manager/components/layout/MainAreaContainer.tsx @@ -0,0 +1,90 @@ +import React, { useRef } from 'react'; + +import { Match } from 'storybook/internal/router'; + +import { styled } from 'storybook/theming'; + +import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants'; +import { useLandmark } from '../../hooks/useLandmark'; + +interface PagesContainerProps { + children: React.ReactNode; +} + +const PagesInnerContainer = styled.main(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gridRowStart: 'sidebar-start', + gridRowEnd: '-1', + gridColumnStart: 'sidebar-end', + gridColumnEnd: '-1', + backgroundColor: theme.appContentBg, + zIndex: 1, +})); + +/** + * Shows Router-controlled pages (e.g. settings/about), inside a landmark for navigability. Assumes + * that the main preview area is not concurrently reachable by assistive technologies, since these + * components both define a `main` role. + */ +const PagesContainer = React.memo(function PagesContainer(props) { + const { children } = props; + + const mainRef = useRef(null); + const { landmarkProps } = useLandmark( + { 'aria-labelledby': 'main-content-heading', role: 'main' }, + mainRef + ); + + return ( + +

+ Main content +

+ {children} +
+ ); +}); + +const MainInnerContainer = styled.div<{ shown: boolean }>(({ theme, shown }) => ({ + flex: 1, + position: 'relative', + backgroundColor: theme.appContentBg, + display: shown ? 'grid' : 'none', // This is needed to make the content container fill the available space + overflow: 'auto', + + [MEDIA_DESKTOP_BREAKPOINT]: { + flex: 'auto', + gridArea: 'content', + }, +})); + +interface MainAreaContainerProps { + showPages: boolean; + slotMain: React.ReactNode; + slotPages: React.ReactNode; +} + +/** + * Shows Router-controlled pages (e.g. settings/about), inside a landmark for navigability, OR shows + * the preview area. Ensures a single `main` landmark exists at a time. + */ +const MainAreaContainer = React.memo(function MainAreaContainer({ + showPages, + slotMain, + slotPages, +}) { + return ( + <> + {showPages ? ( + {slotPages} + ) : ( + + {({ match }) => {slotMain}} + + )} + + ); +}); + +export { MainAreaContainer }; diff --git a/code/core/src/manager/components/layout/PanelContainer.tsx b/code/core/src/manager/components/layout/PanelContainer.tsx new file mode 100644 index 000000000000..9ddce06d569e --- /dev/null +++ b/code/core/src/manager/components/layout/PanelContainer.tsx @@ -0,0 +1,59 @@ +import React from 'react'; + +import { styled } from 'storybook/theming'; + +import type { API_Layout } from '../../../types'; +import { Drag } from './Drag'; + +interface PanelContainerProps { + children: React.ReactNode; + bottomPanelHeight: number; + rightPanelWidth: number; + panelResizerRef: React.Ref; + position: API_Layout['panelPosition']; +} + +const Container = styled.div<{ position: API_Layout['panelPosition'] }>(({ theme, position }) => ({ + gridArea: 'panel', + position: 'relative', + backgroundColor: theme.appContentBg, + borderTop: position === 'bottom' ? `1px solid ${theme.appBorderColor}` : undefined, + borderLeft: position === 'right' ? `1px solid ${theme.appBorderColor}` : undefined, +})); + +const PanelSlot = styled.div({ + height: '100%', +}); + +/** + * Shows the addon panel and its resize drag handle. The drag handle is always rendered so users can + * reopen the panel. The panel is always rendered (to preserve internal state), but it's excluded + * from the Accessibility Object Model when effectively collapsed. + */ +const PanelContainer = React.memo(function PanelContainer(props) { + const { children, bottomPanelHeight, rightPanelWidth, panelResizerRef, position } = props; + + const shouldHidePanelContent = + position === 'bottom' ? bottomPanelHeight === 0 : rightPanelWidth === 0; + + return ( + + + + + ); +}); + +export { PanelContainer }; diff --git a/code/core/src/manager/components/layout/useLandmarkIndicator.ts b/code/core/src/manager/components/layout/useLandmarkIndicator.ts index 7226ef483511..497a1f3b5130 100644 --- a/code/core/src/manager/components/layout/useLandmarkIndicator.ts +++ b/code/core/src/manager/components/layout/useLandmarkIndicator.ts @@ -19,14 +19,49 @@ function findActiveLandmarkElement() { return landmarkElement; } +export function useRegionFocusAnimation() { + const theme = useTheme(); + const reducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)'); + const currentAnimationRef = useRef(null); + + const animateLandmark = (elementToAnimate: HTMLElement | null) => { + if (!elementToAnimate) { + return; + } + + // Cancel previous landmark animation if user switches fast. + if (currentAnimationRef.current) { + currentAnimationRef.current.cancel(); + currentAnimationRef.current = null; + } + + if (!reducedMotion) { + const animation = elementToAnimate.animate( + [{ border: `2px solid ${theme.color.primary}` }, { border: `2px solid transparent` }], + { + duration: 1500, + pseudoElement: '::after', + } + ); + currentAnimationRef.current = animation; + + animation.onfinish = () => { + currentAnimationRef.current = null; + }; + } + }; + + return animateLandmark; +} + // Global keyboard handler for F6/Shift+F6 landmark navigation that // highlights the landmark containing the current element. This helps // users who navigate through landmark shortcuts more quickly visualise // which region of the UI they landed into. +// Call this once at the app root. export function useLandmarkIndicator() { - const theme = useTheme(); - const reducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)'); - const currentAnimationRef = useRef(null); + const animateLandmark = useRegionFocusAnimation(); + useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key !== 'F6') { @@ -38,31 +73,12 @@ export function useLandmarkIndicator() { return; } - // Cancel previous landmark animation if user switches fast. - if (currentAnimationRef.current) { - currentAnimationRef.current.cancel(); - currentAnimationRef.current = null; - } - - if (!reducedMotion) { - const animation = landmarkElement.animate( - [{ border: `2px solid ${theme.color.primary}` }, { border: `2px solid transparent` }], - { - duration: 1500, - pseudoElement: '::after', - } - ); - currentAnimationRef.current = animation; - - animation.onfinish = () => { - currentAnimationRef.current = null; - }; - } + animateLandmark(landmarkElement); }; document.addEventListener('keydown', handleKeyDown, { capture: true }); return () => { document.removeEventListener('keydown', handleKeyDown, { capture: true }); }; - }, [reducedMotion, theme.color.primary]); + }, [animateLandmark]); } diff --git a/code/core/src/manager/components/panel/Panel.tsx b/code/core/src/manager/components/panel/Panel.tsx index 41393b27634b..1ff1b5eb07ba 100644 --- a/code/core/src/manager/components/panel/Panel.tsx +++ b/code/core/src/manager/components/panel/Panel.tsx @@ -160,7 +160,7 @@ export const AddonPanel = React.memo<{ ); return ( -
@@ -94,18 +93,6 @@ For some project setups, the `add` command may be unable to automate the addon a When the addon is set up automatically, it will create or adjust your Vitest configuration files for you. If you're setting up manually, you can use the following examples as a reference when configuring your project. -
- Example Vitest setup file - - Storybook stories contain configuration defined in `.storybook/preview.js|ts`. To ensure that configuration is available to your tests, you can apply it in a Vitest setup file. Here's an example of how to do that: - - {/* prettier-ignore-start */} - - - - {/* prettier-ignore-end */} -
-
Example Vitest config file @@ -379,6 +366,44 @@ The most common fix is to pre-optimize your dependencies. You can do this by add This prevents mid-test dependency optimization, which can interfere with Vitest's test suite management. +### Why do my tests fail in CI with "Failed to fetch dynamically imported module" or "Cannot connect to the iframe"? + +These errors typically occur in CI environments when running a large number of tests simultaneously, which can overwhelm the available resources. They do not usually appear locally because local machines tend to have more available resources or fewer tests run in parallel. + +There are two recommended approaches to fix this: + +**1. Disable isolation mode** + +By default, Vitest isolates each test file in its own environment. Disabling this can reduce resource consumption and prevent the errors: + +```ts title="vitest.config.ts" +export default defineConfig({ + test: { + // ... + isolate: false, + }, +}); +``` + +See [Vitest's `isolate` configuration](https://vitest.dev/config/isolate.html#isolate) for more details. + +**2. Use sharding to split tests across multiple CI jobs** + +Sharding distributes your test files across several parallel CI jobs, reducing the load on each individual runner: + +```bash +# Run shard 1 of 3 +vitest run --shard=1/3 + +# Run shard 2 of 3 +vitest run --shard=2/3 + +# Run shard 3 of 3 +vitest run --shard=3/3 +``` + +See [Vitest's sharding guide](https://vitest.dev/guide/improving-performance.html#sharding) for more details on how to integrate this into your CI pipeline. + ## API ### Exports diff --git a/package.json b/package.json index 8b0f35427773..d6e3c288464f 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@testing-library/user-event@npm:^14.6.1": "patch:@testing-library/user-event@npm%3A14.6.1#~/.yarn/patches/@testing-library-user-event-npm-14.6.1-5da7e1d4e2.patch", "@types/babel__traverse@npm:*": "patch:@types/babel__traverse@npm%3A7.20.6#~/.yarn/patches/@types-babel__traverse-npm-7.20.6-fac4243243.patch", "@types/babel__traverse@npm:^7.18.0": "patch:@types/babel__traverse@npm%3A7.20.6#~/.yarn/patches/@types-babel__traverse-npm-7.20.6-fac4243243.patch", - "@types/node": "^22.0.0", + "@types/node": "^22.19.1", "@types/react": "^18.0.0", "@vitest/expect@npm:3.2.4": "patch:@vitest/expect@npm%3A3.2.4#~/.yarn/patches/@vitest-expect-npm-3.2.4-97c526d5cc.patch", "aria-query@5.3.0": "^5.3.0", diff --git a/yarn.lock b/yarn.lock index 32cf0f4c44cf..002a49e113c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9619,12 +9619,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^22.0.0": - version: 22.1.0 - resolution: "@types/node@npm:22.1.0" +"@types/node@npm:^22.19.1": + version: 22.19.15 + resolution: "@types/node@npm:22.19.15" dependencies: - undici-types: "npm:~6.13.0" - checksum: 10c0/553dafcb842b889c036d43b390d464e8ffcf3ca455ddd5b1a1ef98396381eafbeb0c112a15cc6bf9662b72bc25fc45efc4b6f604760e1e84c410f1b7936c488b + undici-types: "npm:~6.21.0" + checksum: 10c0/f17eaf3d0d1da5e93ad9e287efb78201f8a5282973c004c5f70d91675c5c6b926a23acaa7b158a42b3d7e27e36b349d65a531710c91c308fca53dd7fa280ef98 languageName: node linkType: hard @@ -29853,10 +29853,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.13.0": - version: 6.13.0 - resolution: "undici-types@npm:6.13.0" - checksum: 10c0/2de55181f569c77a4f08063f8bf2722fcbb6ea312a26a9e927bd1f5ea5cf3a281c5ddf23155061db083e0a25838f54813543ff13b0ac34d230d5c1205ead66c1 +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04 languageName: node linkType: hard