Bot Detection #691
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"272cfb82a34c91ce325d104406ba05fd6af653e1b0692a7e6d52de421b56f407","body_hash":"7ac535efaa06f9e78a5e95c876882b1d33c7ce2d5b353512f8189a35b8e32a12","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.63"}} | |
| # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7","digest":"sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7@sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7","digest":"sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7@sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7","digest":"sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7@sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.27","digest":"sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.27@sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.4.0","digest":"sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036","pinned_image":"ghcr.io/github/github-mcp-server:v1.4.0@sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036"}]} | |
| # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md | |
| # | |
| # ___ _ _ | |
| # / _ \ | | (_) | |
| # | |_| | __ _ ___ _ __ | |_ _ ___ | |
| # | _ |/ _` |/ _ \ '_ \| __| |/ __| | |
| # | | | | (_| | __/ | | | |_| | (__ | |
| # \_| |_/\__, |\___|_| |_|\__|_|\___| | |
| # __/ | | |
| # _ _ |___/ | |
| # | | | | / _| | | |
| # | | | | ___ _ __ _ __| |_| | _____ ____ | |
| # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| | |
| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ | |
| # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ | |
| # | |
| # | |
| # To update this file, edit the corresponding .md file 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/ | |
| # | |
| # Investigates suspicious repository activity and maintains a single triage issue | |
| # | |
| # Resolved workflow manifest: | |
| # Imports: | |
| # - shared/otlp.md | |
| # | |
| # Secrets used: | |
| # - COPILOT_GITHUB_TOKEN | |
| # - GH_AW_GITHUB_MCP_SERVER_TOKEN | |
| # - GH_AW_GITHUB_TOKEN | |
| # - GH_AW_OTEL_GRAFANA_AUTHORIZATION | |
| # - GH_AW_OTEL_GRAFANA_ENDPOINT | |
| # - GH_AW_OTEL_SENTRY_AUTHORIZATION | |
| # - GH_AW_OTEL_SENTRY_ENDPOINT | |
| # - GITHUB_TOKEN | |
| # | |
| # Custom actions used: | |
| # - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| # - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) | |
| # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| # | |
| # Container images used: | |
| # - ghcr.io/github/gh-aw-firewall/agent:0.27.7@sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c | |
| # - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7@sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6 | |
| # - ghcr.io/github/gh-aw-firewall/squid:0.27.7@sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96 | |
| # - ghcr.io/github/gh-aw-mcpg:v0.3.27@sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7 | |
| # - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b | |
| # - ghcr.io/github/github-mcp-server:v1.4.0@sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036 | |
| name: "Bot Detection" | |
| on: | |
| schedule: | |
| - cron: "25 */6 * * *" | |
| # Friendly format: every 6h (scattered) | |
| workflow_dispatch: | |
| inputs: | |
| aw_context: | |
| default: "" | |
| description: "Agent caller context (used internally by Agentic Workflows)." | |
| required: false | |
| type: string | |
| permissions: {} | |
| concurrency: | |
| group: "gh-aw-${{ github.workflow }}" | |
| run-name: "Bot Detection" | |
| env: | |
| OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.GH_AW_OTEL_SENTRY_ENDPOINT }} | |
| OTEL_SERVICE_NAME: gh-aw.bot-detection | |
| OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Bot%20Detection,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' | |
| OTEL_EXPORTER_OTLP_HEADERS: x-sentry-auth=${{ secrets.GH_AW_OTEL_SENTRY_AUTHORIZATION }} | |
| GH_AW_OTLP_ALL_HEADERS: x-sentry-auth=${{ secrets.GH_AW_OTEL_SENTRY_AUTHORIZATION }},Authorization=${{ secrets.GH_AW_OTEL_GRAFANA_AUTHORIZATION }} | |
| GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ secrets.GH_AW_OTEL_SENTRY_ENDPOINT }}","headers":"x-sentry-auth=${{ secrets.GH_AW_OTEL_SENTRY_AUTHORIZATION }}"},{"url":"${{ secrets.GH_AW_OTEL_GRAFANA_ENDPOINT }}","headers":"Authorization=${{ secrets.GH_AW_OTEL_GRAFANA_AUTHORIZATION }}"}]' | |
| jobs: | |
| activation: | |
| needs: precompute | |
| if: needs.precompute.outputs.action != 'none' | |
| runs-on: ubuntu-slim | |
| permissions: | |
| actions: read | |
| contents: read | |
| env: | |
| GH_AW_MAX_DAILY_AI_CREDITS: "10000" | |
| GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} | |
| outputs: | |
| comment_id: "" | |
| comment_repo: "" | |
| daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} | |
| daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} | |
| daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} | |
| engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} | |
| lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} | |
| model: ${{ steps.generate_aw_info.outputs.model }} | |
| secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} | |
| setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} | |
| setup-span-id: ${{ steps.setup.outputs.span-id }} | |
| setup-trace-id: ${{ steps.setup.outputs.trace-id }} | |
| stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} | |
| steps: | |
| - name: Checkout actions folder | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| repository: github/gh-aw | |
| sparse-checkout: | | |
| actions | |
| persist-credentials: false | |
| - name: Setup Scripts | |
| id: setup | |
| uses: ./actions/setup | |
| with: | |
| destination: ${{ runner.temp }}/gh-aw/actions | |
| job-name: ${{ github.job }} | |
| safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} | |
| env: | |
| GH_AW_SETUP_WORKFLOW_NAME: "Bot Detection" | |
| GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/bot-detection.lock.yml@${{ github.ref }} | |
| GH_AW_INFO_VERSION: "1.0.63" | |
| GH_AW_INFO_AWF_VERSION: "v0.27.7" | |
| GH_AW_INFO_ENGINE_ID: "copilot" | |
| - name: Mask OTLP telemetry headers | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" | |
| - 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 || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} | |
| GH_AW_INFO_VERSION: "1.0.63" | |
| GH_AW_INFO_AGENT_VERSION: "1.0.63" | |
| GH_AW_INFO_WORKFLOW_NAME: "Bot Detection" | |
| GH_AW_INFO_EXPERIMENTAL: "false" | |
| GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" | |
| GH_AW_INFO_STAGED: "false" | |
| GH_AW_INFO_ALLOWED_DOMAINS: '["*.grafana.net","*.sentry.io","defaults"]' | |
| GH_AW_INFO_FIREWALL_ENABLED: "true" | |
| GH_AW_INFO_AWF_VERSION: "v0.27.7" | |
| GH_AW_INFO_AWMG_VERSION: "" | |
| GH_AW_INFO_FIREWALL_TYPE: "squid" | |
| GH_AW_INFO_FRONTMATTER_EMOJI: "🤖" | |
| GH_AW_COMPILED_STRICT: "true" | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); | |
| await main(core, context); | |
| - name: Restore daily AIC usage cache | |
| id: restore-daily-aic-cache | |
| if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} | |
| continue-on-error: true | |
| uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| key: agentic-workflow-usage-botdetection-${{ github.run_id }} | |
| restore-keys: agentic-workflow-usage-botdetection- | |
| path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl | |
| - name: Restore daily AIC usage cache (artifact fallback) | |
| id: restore-daily-aic-cache-fallback | |
| if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} | |
| continue-on-error: true | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} | |
| GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); | |
| await main(); | |
| - name: Check daily workflow token guardrail | |
| id: daily-effective-workflow-guardrail | |
| if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GH_AW_WORKFLOW_NAME: "Bot Detection" | |
| GH_AW_WORKFLOW_ID: "bot-detection" | |
| GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} | |
| GH_AW_HAS_SLASH_COMMAND: "false" | |
| GH_AW_HAS_LABEL_COMMAND: "false" | |
| GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_AW_MAX_DAILY_AI_CREDITS: "10000" | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); | |
| await main(); | |
| - name: Validate COPILOT_GITHUB_TOKEN secret | |
| id: validate-secret | |
| run: bash "${RUNNER_TEMP}/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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| sparse-checkout: | | |
| .github | |
| .agents | |
| actions/setup | |
| .antigravity | |
| .claude | |
| .codex | |
| .crush | |
| .gemini | |
| .opencode | |
| .pi | |
| sparse-checkout-cone-mode: true | |
| fetch-depth: 1 | |
| - name: Save agent config folders for base branch restoration | |
| env: | |
| GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" | |
| GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" | |
| # poutine:ignore untrusted_checkout_exec | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" | |
| - name: Check workflow lock file | |
| id: check-lock-file | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GH_AW_WORKFLOW_FILE: "bot-detection.lock.yml" | |
| GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" | |
| with: | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); | |
| await main(); | |
| - name: Log runtime features | |
| env: | |
| GH_AW_RUNTIME_FEATURES_IS_SET: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} | |
| run: | | |
| { | |
| echo "## Runtime features" | |
| echo | |
| if [[ "$GH_AW_RUNTIME_FEATURES_IS_SET" != "true" ]]; then | |
| echo "_Not set_" | |
| elif [[ -n "$GH_AW_RUNTIME_FEATURES" ]]; then | |
| echo '```text' | |
| printf '%s\n' "$GH_AW_RUNTIME_FEATURES" | |
| echo '```' | |
| else | |
| echo "_Empty string_" | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Create prompt with built-in context | |
| env: | |
| GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt | |
| GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl | |
| GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} | |
| GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} | |
| GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} | |
| GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} | |
| GH_AW_GITHUB_ACTOR: ${{ github.actor }} | |
| GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} | |
| GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} | |
| GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ACTION: ${{ needs.precompute.outputs.action }} | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_BODY: ${{ needs.precompute.outputs.issue_body }} | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_NUMBER: ${{ needs.precompute.outputs.issue_number }} | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_TITLE: ${{ needs.precompute.outputs.issue_title }} | |
| # poutine:ignore untrusted_checkout_exec | |
| run: | | |
| bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" | |
| { | |
| cat << 'GH_AW_PROMPT_d4bfda8ba1ade689_EOF' | |
| <system> | |
| GH_AW_PROMPT_d4bfda8ba1ade689_EOF | |
| cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" | |
| cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" | |
| cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" | |
| cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" | |
| cat << 'GH_AW_PROMPT_d4bfda8ba1ade689_EOF' | |
| <safe-output-tools> | |
| Tools: create_issue, update_issue, missing_tool, missing_data, noop | |
| </safe-output-tools> | |
| GH_AW_PROMPT_d4bfda8ba1ade689_EOF | |
| cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" | |
| cat << 'GH_AW_PROMPT_d4bfda8ba1ade689_EOF' | |
| <github-context> | |
| The following GitHub context information is available for this workflow: | |
| {{#if github.actor}} | |
| - **actor**: __GH_AW_GITHUB_ACTOR__ | |
| {{/if}} | |
| {{#if github.repository}} | |
| - **repository**: __GH_AW_GITHUB_REPOSITORY__ | |
| {{/if}} | |
| {{#if github.workspace}} | |
| - **workspace**: __GH_AW_GITHUB_WORKSPACE__ | |
| {{/if}} | |
| {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} | |
| - **issue-number**: #__GH_AW_EXPR_802A9F6A__ | |
| {{/if}} | |
| {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} | |
| - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ | |
| {{/if}} | |
| {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} | |
| - **pull-request-number**: #__GH_AW_EXPR_463A214A__ | |
| {{/if}} | |
| {{#if github.event.comment.id || github.aw.context.comment_id}} | |
| - **comment-id**: __GH_AW_EXPR_FF1D34CE__ | |
| {{/if}} | |
| {{#if github.run_id}} | |
| - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ | |
| {{/if}} | |
| </github-context> | |
| GH_AW_PROMPT_d4bfda8ba1ade689_EOF | |
| cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" | |
| cat << 'GH_AW_PROMPT_d4bfda8ba1ade689_EOF' | |
| </system> | |
| {{#runtime-import .github/workflows/shared/otlp.md}} | |
| {{#runtime-import .github/workflows/bot-detection.md}} | |
| GH_AW_PROMPT_d4bfda8ba1ade689_EOF | |
| } > "$GH_AW_PROMPT" | |
| - name: Interpolate variables and render templates | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt | |
| GH_AW_ENGINE_ID: "copilot" | |
| GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ACTION: ${{ needs.precompute.outputs.action }} | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_BODY: ${{ needs.precompute.outputs.issue_body }} | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_NUMBER: ${{ needs.precompute.outputs.issue_number }} | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_TITLE: ${{ needs.precompute.outputs.issue_title }} | |
| with: | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); | |
| await main(); | |
| - name: Substitute placeholders | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt | |
| GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} | |
| GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} | |
| GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} | |
| GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} | |
| GH_AW_GITHUB_ACTOR: ${{ github.actor }} | |
| GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} | |
| GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} | |
| GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} | |
| GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ACTION: ${{ needs.precompute.outputs.action }} | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_BODY: ${{ needs.precompute.outputs.issue_body }} | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_NUMBER: ${{ needs.precompute.outputs.issue_number }} | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_TITLE: ${{ needs.precompute.outputs.issue_title }} | |
| with: | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); | |
| // Call the substitution function | |
| return await substitutePlaceholders({ | |
| file: process.env.GH_AW_PROMPT, | |
| substitutions: { | |
| GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, | |
| GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, | |
| GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, | |
| GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, | |
| GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, | |
| 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_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ACTION: process.env.GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ACTION, | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_BODY: process.env.GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_BODY, | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_NUMBER: process.env.GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_NUMBER, | |
| GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_TITLE: process.env.GH_AW_NEEDS_PRECOMPUTE_OUTPUTS_ISSUE_TITLE | |
| } | |
| }); | |
| - name: Validate prompt placeholders | |
| env: | |
| GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt | |
| # poutine:ignore untrusted_checkout_exec | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" | |
| - name: Print prompt | |
| env: | |
| GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt | |
| # poutine:ignore untrusted_checkout_exec | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" | |
| - name: Upload activation artifact | |
| if: success() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: activation | |
| include-hidden-files: true | |
| path: | | |
| /tmp/gh-aw/aw_info.json | |
| /tmp/gh-aw/models.json | |
| /tmp/gh-aw/aw-prompts/prompt.txt | |
| /tmp/gh-aw/aw-prompts/prompt-template.txt | |
| /tmp/gh-aw/aw-prompts/prompt-import-tree.json | |
| /tmp/gh-aw/github_rate_limits.jsonl | |
| /tmp/gh-aw/base | |
| /tmp/gh-aw/.github/agents | |
| /tmp/gh-aw/.github/skills | |
| if-no-files-found: ignore | |
| retention-days: 1 | |
| agent: | |
| needs: | |
| - activation | |
| - precompute | |
| if: (needs.precompute.outputs.action != 'none') && (needs.activation.outputs.daily_ai_credits_exceeded != 'true') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| contents: read | |
| issues: read | |
| pull-requests: read | |
| concurrency: | |
| group: "gh-aw-copilot-${{ github.workflow }}" | |
| queue: max | |
| 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_PROJECT_UTC: "-08:00" | |
| GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} | |
| GH_AW_WORKFLOW_ID_SANITIZED: botdetection | |
| outputs: | |
| agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} | |
| ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} | |
| aic: ${{ steps.parse-mcp-gateway.outputs.aic }} | |
| ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} | |
| checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} | |
| effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} | |
| has_patch: ${{ steps.collect_output.outputs.has_patch }} | |
| inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} | |
| mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} | |
| model: ${{ needs.activation.outputs.model }} | |
| model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} | |
| output: ${{ steps.collect_output.outputs.output }} | |
| output_types: ${{ steps.collect_output.outputs.output_types }} | |
| setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} | |
| setup-span-id: ${{ steps.setup.outputs.span-id }} | |
| setup-trace-id: ${{ steps.setup.outputs.trace-id }} | |
| unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} | |
| steps: | |
| - name: Checkout actions folder | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| repository: github/gh-aw | |
| sparse-checkout: | | |
| actions | |
| persist-credentials: false | |
| - name: Setup Scripts | |
| id: setup | |
| uses: ./actions/setup | |
| with: | |
| destination: ${{ runner.temp }}/gh-aw/actions | |
| job-name: ${{ github.job }} | |
| trace-id: ${{ needs.activation.outputs.setup-trace-id }} | |
| parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} | |
| env: | |
| GH_AW_SETUP_WORKFLOW_NAME: "Bot Detection" | |
| GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/bot-detection.lock.yml@${{ github.ref }} | |
| GH_AW_INFO_VERSION: "1.0.63" | |
| GH_AW_INFO_AWF_VERSION: "v0.27.7" | |
| GH_AW_INFO_ENGINE_ID: "copilot" | |
| - name: Set runtime paths | |
| id: set-runtime-paths | |
| run: | | |
| { | |
| echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" | |
| echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" | |
| echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Mask OTLP telemetry headers | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Create gh-aw temp directory | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" | |
| - name: Configure gh CLI for GitHub Enterprise | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| - name: Configure Git credentials | |
| env: | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| GITHUB_SERVER_URL: ${{ github.server_url }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" | |
| - name: Checkout PR branch | |
| id: checkout-pr | |
| if: | | |
| github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| 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('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); | |
| await main(); | |
| - name: Install GitHub Copilot CLI | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.63 | |
| env: | |
| GH_HOST: github.com | |
| - name: Install AWF binary | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.7 | |
| - name: Determine automatic lockdown mode for GitHub MCP Server | |
| id: determine-automatic-lockdown | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) | |
| 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('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); | |
| await determineAutomaticLockdown(github, context, core); | |
| - name: Download activation artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: activation | |
| path: /tmp/gh-aw | |
| - name: Restore agent config folders from base branch | |
| if: steps.checkout-pr.outcome == 'success' | |
| env: | |
| GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" | |
| GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" | |
| - name: Restore inline sub-agents from activation artifact | |
| env: | |
| GH_AW_SUB_AGENT_DIR: ".github/agents" | |
| GH_AW_SUB_AGENT_EXT: ".agent.md" | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" | |
| - name: Restore inline skills from activation artifact | |
| env: | |
| GH_AW_SKILL_DIR: ".github/skills" | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" | |
| - name: Download container images | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.7@sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7@sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6 ghcr.io/github/gh-aw-firewall/squid:0.27.7@sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96 ghcr.io/github/gh-aw-mcpg:v0.3.27@sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.4.0@sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036 | |
| - name: Generate Safe Outputs Config | |
| run: | | |
| mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" | |
| mkdir -p /tmp/gh-aw/safeoutputs | |
| mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs | |
| cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_766056c7b5543cde_EOF' | |
| {"create_issue":{"labels":["security","bot-detection"],"max":1},"create_report_incomplete_issue":{},"mentions":{"allowed":["pelikhan"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"*"}} | |
| GH_AW_SAFE_OUTPUTS_CONFIG_766056c7b5543cde_EOF | |
| - name: Generate Safe Outputs Tools | |
| env: | |
| GH_AW_TOOLS_META_JSON: | | |
| { | |
| "description_suffixes": { | |
| "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Labels [\"security\" \"bot-detection\"] will be automatically added.", | |
| "update_issue": " CONSTRAINTS: Maximum 1 issue(s) can be updated. Target: *. Body updates are allowed." | |
| }, | |
| "repo_params": {}, | |
| "dynamic_tools": [] | |
| } | |
| GH_AW_VALIDATION_JSON: | | |
| { | |
| "create_issue": { | |
| "defaultMax": 1, | |
| "fields": { | |
| "body": { | |
| "required": true, | |
| "type": "string", | |
| "sanitize": true, | |
| "maxLength": 65000, | |
| "minLength": 20 | |
| }, | |
| "fields": { | |
| "type": "array" | |
| }, | |
| "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 | |
| } | |
| } | |
| }, | |
| "mentions": { | |
| "allowed": [ | |
| "pelikhan" | |
| ] | |
| }, | |
| "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 | |
| } | |
| } | |
| }, | |
| "report_incomplete": { | |
| "defaultMax": 5, | |
| "fields": { | |
| "details": { | |
| "type": "string", | |
| "sanitize": true, | |
| "maxLength": 65000 | |
| }, | |
| "reason": { | |
| "required": true, | |
| "type": "string", | |
| "sanitize": true, | |
| "maxLength": 1024 | |
| } | |
| } | |
| }, | |
| "update_issue": { | |
| "defaultMax": 1, | |
| "fields": { | |
| "assignees": { | |
| "type": "array", | |
| "itemType": "string", | |
| "itemSanitize": true, | |
| "itemMaxLength": 39 | |
| }, | |
| "body": { | |
| "type": "string", | |
| "sanitize": true, | |
| "maxLength": 65000 | |
| }, | |
| "issue_number": { | |
| "issueOrPRNumber": true | |
| }, | |
| "labels": { | |
| "type": "array", | |
| "itemType": "string", | |
| "itemSanitize": true, | |
| "itemMaxLength": 128 | |
| }, | |
| "milestone": { | |
| "optionalPositiveInteger": true | |
| }, | |
| "operation": { | |
| "type": "string", | |
| "enum": [ | |
| "replace", | |
| "append", | |
| "prepend", | |
| "replace-island" | |
| ] | |
| }, | |
| "repo": { | |
| "type": "string", | |
| "maxLength": 256 | |
| }, | |
| "status": { | |
| "type": "string", | |
| "enum": [ | |
| "open", | |
| "closed" | |
| ] | |
| }, | |
| "title": { | |
| "type": "string", | |
| "sanitize": true, | |
| "maxLength": 128 | |
| } | |
| }, | |
| "customValidation": "requiresOneOf:status,title,body" | |
| } | |
| } | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); | |
| await main(); | |
| - name: Start MCP Gateway | |
| id: start-mcp-gateway | |
| env: | |
| GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} | |
| GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} | |
| GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} | |
| GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} | |
| GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} | |
| GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -eo pipefail | |
| mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" | |
| # Export gateway environment variables for MCP config and gateway script | |
| export MCP_GATEWAY_PORT="8080" | |
| export MCP_GATEWAY_DOMAIN="host.docker.internal" | |
| export MCP_GATEWAY_HOST_DOMAIN="localhost" | |
| 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 GH_AW_MCP_CLI_SERVERS='["safeoutputs"]' | |
| MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') | |
| MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') | |
| case "${DOCKER_HOST:-}" in | |
| unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; | |
| /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; | |
| * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; | |
| esac | |
| DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') | |
| export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --name awmg-mcpg --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -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 RUNNER_TEMP -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.3.27' | |
| mkdir -p "$HOME/.copilot" | |
| GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) | |
| cat << GH_AW_MCP_CONFIG_1fbd4b8aa1d452d2_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" | |
| { | |
| "mcpServers": { | |
| "github": { | |
| "type": "stdio", | |
| "container": "ghcr.io/github/github-mcp-server:v1.4.0", | |
| "env": { | |
| "GITHUB_HOST": "\${GITHUB_SERVER_URL}", | |
| "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", | |
| "GITHUB_READ_ONLY": "1", | |
| "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" | |
| }, | |
| "guard-policies": { | |
| "allow-only": { | |
| "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", | |
| "repos": "$GITHUB_MCP_GUARD_REPOS" | |
| } | |
| } | |
| }, | |
| "safeoutputs": { | |
| "type": "stdio", | |
| "container": "ghcr.io/github/gh-aw-node", | |
| "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], | |
| "args": ["-w", "\${GITHUB_WORKSPACE}"], | |
| "entrypoint": "sh", | |
| "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], | |
| "env": { | |
| "DEBUG": "*", | |
| "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", | |
| "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", | |
| "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", | |
| "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", | |
| "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", | |
| "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", | |
| "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", | |
| "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", | |
| "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", | |
| "GITHUB_TOKEN": "\${GITHUB_TOKEN}", | |
| "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", | |
| "RUNNER_TEMP": "\${RUNNER_TEMP}" | |
| }, | |
| "guard-policies": { | |
| "write-sink": { | |
| "accept": [ | |
| "*" | |
| ] | |
| } | |
| } | |
| } | |
| }, | |
| "gateway": { | |
| "port": $MCP_GATEWAY_PORT, | |
| "domain": "${MCP_GATEWAY_DOMAIN}", | |
| "apiKey": "${MCP_GATEWAY_API_KEY}", | |
| "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", | |
| "opentelemetry": { | |
| "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", | |
| "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", | |
| "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" | |
| } | |
| } | |
| } | |
| GH_AW_MCP_CONFIG_1fbd4b8aa1d452d2_EOF | |
| - name: Mount MCP servers as CLIs | |
| id: mount-mcp-clis | |
| continue-on-error: true | |
| env: | |
| MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} | |
| MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} | |
| MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); | |
| await main(); | |
| - name: Clean credentials | |
| continue-on-error: true | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" | |
| - name: Audit pre-agent workspace | |
| id: pre_agent_audit | |
| continue-on-error: true | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" | |
| - name: Execute GitHub Copilot CLI | |
| id: agentic_execution | |
| # Copilot CLI tool arguments (sorted): | |
| timeout-minutes: 10 | |
| run: | | |
| set -o pipefail | |
| printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt | |
| trap 'rm -f "$HOME/.copilot/settings.json"' EXIT | |
| mkdir -p "$HOME/.copilot" | |
| printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" | |
| export XDG_CONFIG_HOME="$HOME" | |
| export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" | |
| touch /tmp/gh-aw/agent-step-summary.md | |
| GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) | |
| export GH_AW_NODE_BIN | |
| export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" | |
| (umask 177 && touch /tmp/gh-aw/agent-stdio.log) | |
| GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" | |
| printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.7/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.grafana.net\",\"*.sentry.io\",\"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\",\"www.googleapis.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.5\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.7,squid=sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96,agent=sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c,api-proxy=sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6,cli-proxy=sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" | |
| cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json | |
| export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" | |
| GH_AW_DOCKER_HOST="" | |
| if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then | |
| GH_AW_DOCKER_HOST="${DOCKER_HOST}" | |
| fi | |
| GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" | |
| if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then | |
| GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" | |
| python3 - <<'PY' | |
| import json,os,subprocess as sp | |
| from pathlib import Path | |
| try: | |
| p=Path(os.environ["RUNNER_TEMP"])/"gh-aw"/"awf-config.json" | |
| c=json.loads(p.read_text()) | |
| c["chroot"]={"binariesSourcePath":"/tmp/gh-aw","identity":{"user":sp.check_output(["id","-un"],text=True).strip(),"uid":int(sp.check_output(["id","-u"],text=True)),"gid":int(sp.check_output(["id","-g"],text=True)),"home":"/tmp/gh-aw/home"}} | |
| out=json.dumps(c,separators=(",",":"),ensure_ascii=False)+"\n" | |
| p.write_text(out) | |
| Path("/tmp/gh-aw/awf-config.json").write_text(out) | |
| except Exception as e: | |
| raise SystemExit(f"chroot config patch failed: {e}") from e | |
| PY | |
| fi | |
| GH_AW_TOOL_CACHE_MOUNT="" | |
| GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" | |
| if [ -d "$GH_AW_TOOL_CACHE" ]; then | |
| if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then | |
| GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" | |
| fi | |
| fi | |
| # shellcheck disable=SC1003,SC2086 | |
| sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ | |
| -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log | |
| env: | |
| AWF_REFLECT_ENABLED: 1 | |
| COPILOT_AGENT_RUNNER_TYPE: STANDALONE | |
| COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode | |
| COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} | |
| COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} | |
| GH_AW_LLM_PROVIDER: github | |
| GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} | |
| GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} | |
| GH_AW_PHASE: agent | |
| GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt | |
| GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} | |
| GH_AW_TIMEOUT_MINUTES: 10 | |
| GH_AW_VERSION: dev | |
| GITHUB_API_URL: ${{ github.api_url }} | |
| GITHUB_AW: true | |
| GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows | |
| 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 }} | |
| GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com | |
| GIT_AUTHOR_NAME: github-actions[bot] | |
| GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com | |
| GIT_COMMITTER_NAME: github-actions[bot] | |
| RUNNER_TEMP: ${{ runner.temp }} | |
| TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} | |
| - name: Detect agent errors | |
| if: always() | |
| id: detect-agent-errors | |
| continue-on-error: true | |
| run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" | |
| - name: Configure Git credentials | |
| env: | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| GITHUB_SERVER_URL: ${{ github.server_url }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" | |
| - name: Copy Copilot session state files to logs | |
| if: always() | |
| continue-on-error: true | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" | |
| - 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 "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" | |
| - name: Redact secrets in logs | |
| if: always() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/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 "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" | |
| - name: Copy Safe Outputs | |
| if: always() | |
| env: | |
| GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} | |
| run: | | |
| mkdir -p /tmp/gh-aw | |
| cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true | |
| - name: Ingest agent output | |
| id: collect_output | |
| if: always() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} | |
| GH_AW_ALLOWED_DOMAINS: "*.grafana.net,*.sentry.io,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,www.googleapis.com" | |
| GITHUB_SERVER_URL: ${{ github.server_url }} | |
| GITHUB_API_URL: ${{ github.api_url }} | |
| with: | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); | |
| await main(); | |
| - name: Parse agent logs for step summary | |
| if: always() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ | |
| with: | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); | |
| await main(); | |
| - name: Parse MCP Gateway logs for step summary | |
| if: always() | |
| id: parse-mcp-gateway | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/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/audit dirs so they can be uploaded as artifacts | |
| # AWF runs with sudo, creating files owned by root | |
| sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 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: Parse token usage for step summary | |
| if: always() | |
| continue-on-error: true | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); | |
| await main(); | |
| - name: Print AWF reflect summary | |
| if: always() | |
| continue-on-error: true | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); | |
| await main(); | |
| - name: Generate observability summary | |
| if: always() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_observability_summary.cjs'); | |
| await main(core); | |
| - name: Write agent output placeholder if missing | |
| if: always() | |
| run: | | |
| if [ ! -f /tmp/gh-aw/agent_output.json ]; then | |
| echo '{"items":[]}' > /tmp/gh-aw/agent_output.json | |
| fi | |
| - name: Upload agent artifacts | |
| if: always() | |
| continue-on-error: true | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: agent | |
| path: | | |
| /tmp/gh-aw/aw-prompts/prompt.txt | |
| /tmp/gh-aw/sandbox/agent/logs/ | |
| /tmp/gh-aw/redacted-urls.log | |
| /tmp/gh-aw/mcp-logs/ | |
| /tmp/gh-aw/agent_usage.json | |
| /tmp/gh-aw/agent-stdio.log | |
| /tmp/gh-aw/pre-agent-audit.txt | |
| /tmp/gh-aw/agent/ | |
| /tmp/gh-aw/github_rate_limits.jsonl | |
| /tmp/gh-aw/otel.jsonl | |
| /tmp/gh-aw/otlp-export-errors.jsonl | |
| /tmp/gh-aw/safeoutputs.jsonl | |
| /tmp/gh-aw/agent_output.json | |
| /tmp/gh-aw/awf-config.json | |
| /tmp/gh-aw/sandbox/firewall/logs/ | |
| /tmp/gh-aw/sandbox/firewall/audit/ | |
| /tmp/gh-aw/sandbox/firewall/awf-reflect.json | |
| if-no-files-found: ignore | |
| conclusion: | |
| needs: | |
| - activation | |
| - agent | |
| - precompute | |
| - safe_outputs | |
| if: > | |
| always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || | |
| needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') | |
| runs-on: ubuntu-slim | |
| permissions: | |
| contents: read | |
| issues: write | |
| concurrency: | |
| group: "gh-aw-conclusion-bot-detection" | |
| cancel-in-progress: false | |
| queue: max | |
| env: | |
| GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} | |
| outputs: | |
| incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} | |
| 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: Checkout actions folder | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| repository: github/gh-aw | |
| sparse-checkout: | | |
| actions | |
| persist-credentials: false | |
| - name: Setup Scripts | |
| id: setup | |
| uses: ./actions/setup | |
| with: | |
| destination: ${{ runner.temp }}/gh-aw/actions | |
| job-name: ${{ github.job }} | |
| trace-id: ${{ needs.activation.outputs.setup-trace-id }} | |
| parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} | |
| env: | |
| GH_AW_SETUP_WORKFLOW_NAME: "Bot Detection" | |
| GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/bot-detection.lock.yml@${{ github.ref }} | |
| GH_AW_INFO_VERSION: "1.0.63" | |
| GH_AW_INFO_AWF_VERSION: "v0.27.7" | |
| GH_AW_INFO_ENGINE_ID: "copilot" | |
| - name: Download agent output artifact | |
| id: download-agent-output | |
| continue-on-error: true | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: agent | |
| path: /tmp/gh-aw/ | |
| - name: Setup agent output environment variable | |
| id: setup-agent-output-env | |
| if: steps.download-agent-output.outcome == 'success' | |
| run: | | |
| mkdir -p /tmp/gh-aw/ | |
| find "/tmp/gh-aw/" -type f -print | |
| echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" | |
| - name: Collect usage artifact files | |
| if: always() | |
| continue-on-error: true | |
| run: | | |
| mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection | |
| echo "Usage artifact source file status:" | |
| for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do | |
| [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" | |
| done | |
| [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true | |
| [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true | |
| [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true | |
| [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true | |
| [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true | |
| [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true | |
| [ -f /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true | |
| [ -f /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true | |
| [ -f /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true | |
| [ -f /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true | |
| [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true | |
| [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true | |
| [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl | |
| [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl | |
| mkdir -p /tmp/gh-aw/usage/activity | |
| node ${{ runner.temp }}/gh-aw/actions/generate_usage_activity_summary.cjs | |
| find /tmp/gh-aw/usage -type f -print | sort | |
| - name: Upload usage artifact | |
| if: always() | |
| continue-on-error: true | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: usage | |
| path: | | |
| /tmp/gh-aw/usage/aw_info.json | |
| /tmp/gh-aw/usage/aw-info.jsonl | |
| /tmp/gh-aw/usage/agent_usage.json | |
| /tmp/gh-aw/usage/agent_usage.jsonl | |
| /tmp/gh-aw/usage/detection_usage.jsonl | |
| /tmp/gh-aw/usage/github_rate_limits.jsonl | |
| /tmp/gh-aw/usage/agent/token_usage.jsonl | |
| /tmp/gh-aw/usage/detection/token_usage.jsonl | |
| /tmp/gh-aw/usage/activity/summary.json | |
| if-no-files-found: ignore | |
| - name: Restore daily AIC usage cache | |
| id: restore-daily-aic-cache-conclusion | |
| if: always() | |
| continue-on-error: true | |
| uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| key: agentic-workflow-usage-botdetection-${{ github.run_id }} | |
| restore-keys: agentic-workflow-usage-botdetection- | |
| path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl | |
| - name: Write daily AIC usage cache entry | |
| id: write-daily-aic-cache | |
| if: always() | |
| continue-on-error: true | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| github-token: ${{ github.token }} | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); | |
| await main(); | |
| - name: Save daily AIC usage cache | |
| id: save-daily-aic-cache | |
| if: always() | |
| continue-on-error: true | |
| uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| key: agentic-workflow-usage-botdetection-${{ github.run_id }} | |
| path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl | |
| - name: Upload daily AIC usage cache artifact | |
| id: upload-daily-aic-cache | |
| if: always() | |
| continue-on-error: true | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: aic-usage-cache | |
| path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| - name: Process no-op messages | |
| id: noop | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} | |
| GH_AW_NOOP_MAX: "1" | |
| GH_AW_WORKFLOW_NAME: "Bot Detection" | |
| GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/bot-detection.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_REPORT_AS_ISSUE: "true" | |
| GH_AW_AIC: ${{ needs.agent.outputs.aic }} | |
| GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} | |
| GH_AW_WORKFLOW_ID: "bot-detection" | |
| with: | |
| github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); | |
| await main(); | |
| - name: Record missing tool | |
| id: missing_tool | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} | |
| GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" | |
| GH_AW_WORKFLOW_NAME: "Bot Detection" | |
| GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/bot-detection.md" | |
| with: | |
| github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); | |
| await main(); | |
| - name: Record incomplete | |
| id: report_incomplete | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} | |
| GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" | |
| GH_AW_WORKFLOW_NAME: "Bot Detection" | |
| GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/bot-detection.md" | |
| with: | |
| github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); | |
| await main(); | |
| - name: Handle agent failure | |
| id: handle_agent_failure | |
| if: always() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} | |
| GH_AW_WORKFLOW_NAME: "Bot Detection" | |
| GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/bot-detection.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: "bot-detection" | |
| GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "12" | |
| GH_AW_ENGINE_ID: "copilot" | |
| GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} | |
| GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} | |
| GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} | |
| GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} | |
| GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} | |
| GH_AW_AIC: ${{ needs.agent.outputs.aic }} | |
| GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} | |
| GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} | |
| GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} | |
| GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} | |
| GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} | |
| GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" | |
| GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} | |
| GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} | |
| GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} | |
| GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} | |
| GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} | |
| GH_AW_GROUP_REPORTS: "false" | |
| GH_AW_FAILURE_REPORT_AS_ISSUE: "true" | |
| GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" | |
| GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" | |
| GH_AW_TIMEOUT_MINUTES: "10" | |
| with: | |
| github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); | |
| await main(); | |
| precompute: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| contents: read | |
| issues: read | |
| pull-requests: read | |
| outputs: | |
| action: ${{ steps.precompute.outputs.action }} | |
| issue_body: ${{ steps.precompute.outputs.issue_body }} | |
| issue_number: ${{ steps.precompute.outputs.issue_number }} | |
| issue_title: ${{ steps.precompute.outputs.issue_title }} | |
| steps: | |
| - name: Configure GH_HOST for enterprise compatibility | |
| id: ghes-host-config | |
| shell: bash | |
| run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. | |
| # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct | |
| # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. | |
| GH_HOST="${GITHUB_SERVER_URL#https://}" | |
| GH_HOST="${GH_HOST#http://}" | |
| echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" | |
| - name: Precompute deterministic findings | |
| id: precompute | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const HOURS_BACK = 6; | |
| const ISSUE_TITLE = "🔎 Activity Signals: Review Queue"; | |
| const MIN_ACCOUNT_AGE_DAYS = 14; | |
| const MAX_PR = 50; | |
| const MAX_COMMENT_EXAMPLES = 10; | |
| const MAX_TOUCHED_FILES = 10; | |
| const ALLOWED_DOMAINS = new Set([ | |
| // GitHub docs + blog | |
| "docs.github.com", | |
| "github.blog", | |
| // Marketplace + package registries | |
| "marketplace.visualstudio.com", | |
| "npmjs.com", | |
| "pkg.go.dev", | |
| // Language vendor sites | |
| "golang.org", | |
| "go.dev", | |
| "nodejs.org", | |
| ]); | |
| const ALLOWED_ACCOUNTS = new Set([ | |
| // Bots and service accounts | |
| "github-actions[bot]", | |
| "dependabot[bot]", | |
| "renovate[bot]", | |
| "copilot", | |
| "copilot-swe-agent", | |
| ]); | |
| const TRUSTED_ORGS = [owner]; | |
| const MEMBER_ACCOUNTS = new Set(); | |
| function parseJsonList(envName) { | |
| try { | |
| const raw = process.env[envName]; | |
| if (!raw) return []; | |
| const parsed = JSON.parse(raw); | |
| return Array.isArray(parsed) ? parsed : []; | |
| } catch { | |
| return []; | |
| } | |
| } | |
| function toISO(d) { | |
| return new Date(d).toISOString(); | |
| } | |
| function normalizeForDup(s) { | |
| return (s || "") | |
| .toString() | |
| .replace(/https?:\/\/\S+/g, "") | |
| .toLowerCase() | |
| .replace(/\s+/g, " ") | |
| .trim() | |
| .slice(0, 240); | |
| } | |
| function extractDomains(text) { | |
| const domains = []; | |
| const urlRe = /https?:\/\/[^\s)\]]+/g; | |
| const matches = text.match(urlRe) || []; | |
| for (const raw of matches) { | |
| try { | |
| const u = new URL(raw); | |
| domains.push(u.hostname.toLowerCase()); | |
| } catch { | |
| // ignore parse failures | |
| } | |
| } | |
| return domains; | |
| } | |
| function isExternalDomain(host) { | |
| const allowed = new Set([ | |
| "github.com", | |
| "raw.githubusercontent.com", | |
| "avatars.githubusercontent.com", | |
| "api.github.com", | |
| ]); | |
| return host && !allowed.has(host) && !ALLOWED_DOMAINS.has(host); | |
| } | |
| function isAllowedAccount(login) { | |
| const normalized = String(login || "").toLowerCase(); | |
| return ALLOWED_ACCOUNTS.has(normalized) || MEMBER_ACCOUNTS.has(normalized); | |
| } | |
| async function loadMemberAccounts() { | |
| try { | |
| const collaborators = await github.paginate(github.rest.repos.listCollaborators, { | |
| owner, | |
| repo, | |
| per_page: 100, | |
| }); | |
| for (const collaborator of collaborators) { | |
| if (collaborator?.login) { | |
| MEMBER_ACCOUNTS.add(String(collaborator.login).toLowerCase()); | |
| } | |
| } | |
| } catch { | |
| // If collaborator lookup fails, continue without member allowlist. | |
| } | |
| } | |
| async function loadContributorAccounts() { | |
| try { | |
| const contributors = await github.paginate(github.rest.repos.listContributors, { | |
| owner, | |
| repo, | |
| per_page: 100, | |
| }); | |
| for (const contributor of contributors) { | |
| if (contributor?.login) { | |
| MEMBER_ACCOUNTS.add(String(contributor.login).toLowerCase()); | |
| } | |
| } | |
| } catch { | |
| // If contributor lookup fails, continue without contributor allowlist. | |
| } | |
| } | |
| async function loadOrgMembers() { | |
| for (const org of TRUSTED_ORGS) { | |
| try { | |
| const members = await github.paginate(github.rest.orgs.listMembers, { | |
| org, | |
| per_page: 100, | |
| }); | |
| for (const member of members) { | |
| if (member?.login) { | |
| MEMBER_ACCOUNTS.add(String(member.login).toLowerCase()); | |
| } | |
| } | |
| } catch { | |
| // If org member lookup fails, continue without org allowlist. | |
| } | |
| } | |
| } | |
| function isShortener(host) { | |
| const shorteners = new Set(["bit.ly", "tinyurl.com", "t.co", "is.gd", "goo.gl"]); | |
| return shorteners.has(host); | |
| } | |
| function isNonGitHubBinaryUrl(urlStr) { | |
| try { | |
| const u = new URL(urlStr); | |
| const host = u.hostname.toLowerCase(); | |
| if (!isExternalDomain(host)) return false; | |
| const path = u.pathname.toLowerCase(); | |
| return ( | |
| path.endsWith(".exe") || | |
| path.endsWith(".msi") || | |
| path.endsWith(".pkg") || | |
| path.endsWith(".dmg") || | |
| path.endsWith(".zip") || | |
| path.endsWith(".tar.gz") | |
| ); | |
| } catch { | |
| return false; | |
| } | |
| } | |
| async function getRunCreatedAt() { | |
| const runId = context.runId; | |
| const { data } = await github.rest.actions.getWorkflowRun({ | |
| owner, | |
| repo, | |
| run_id: runId, | |
| }); | |
| return new Date(data.created_at); | |
| } | |
| const end = await getRunCreatedAt(); | |
| const start = new Date(end.getTime() - HOURS_BACK * 60 * 60 * 1000); | |
| for (const domain of parseJsonList("BOT_DETECTION_ALLOWED_DOMAINS")) { | |
| if (domain) ALLOWED_DOMAINS.add(String(domain).toLowerCase()); | |
| } | |
| await loadMemberAccounts(); | |
| await loadContributorAccounts(); | |
| await loadOrgMembers(); | |
| // Search issues + PRs updated in window (API requires is:issue or is:pull-request) | |
| const qBase = `repo:${owner}/${repo} updated:>=${toISO(start)}`; | |
| const rawItems = []; | |
| for (const scope of ["is:issue", "is:pull-request"]) { | |
| const search = await github.rest.search.issuesAndPullRequests({ | |
| q: `${qBase} ${scope}`, | |
| per_page: 100, | |
| sort: "updated", | |
| order: "desc", | |
| }); | |
| rawItems.push(...(search.data.items || [])); | |
| } | |
| const seen = new Set(); | |
| let skippedNoLogin = 0; | |
| let skippedAllowed = 0; | |
| const skippedAllowedLogins = new Set(); | |
| const MAX_LOGGED_SKIPPED = 10; | |
| const items = rawItems | |
| .filter(i => new Date(i.updated_at) >= start && new Date(i.updated_at) <= end) | |
| .map(i => ({ | |
| number: i.number, | |
| title: i.title || "", | |
| body: i.body || "", | |
| url: i.html_url, | |
| created_at: i.created_at, | |
| updated_at: i.updated_at, | |
| is_pr: Boolean(i.pull_request), | |
| author: i.user?.login || "", | |
| })) | |
| .filter(i => { | |
| if (seen.has(i.url)) return false; | |
| seen.add(i.url); | |
| return true; | |
| }); | |
| // Deterministic ordering for any downstream processing | |
| items.sort((a, b) => { | |
| const at = a.updated_at.localeCompare(b.updated_at); | |
| if (at !== 0) return at; | |
| const an = a.number - b.number; | |
| if (an !== 0) return an; | |
| return a.url.localeCompare(b.url); | |
| }); | |
| // Collect per-author signals | |
| const perAuthor = new Map(); | |
| const domainAccounts = new Map(); // domain -> Set(logins) | |
| const userCreatedAt = new Map(); | |
| async function ensureUserCreatedAt(login) { | |
| if (!login || userCreatedAt.has(login)) return; | |
| try { | |
| const { data: userInfo } = await github.rest.users.getByUsername({ username: login }); | |
| userCreatedAt.set(login, new Date(userInfo.created_at)); | |
| } catch { | |
| userCreatedAt.set(login, null); | |
| } | |
| } | |
| function ensureAuthor(login) { | |
| if (!perAuthor.has(login)) { | |
| perAuthor.set(login, { | |
| login, | |
| itemCount: 0, | |
| prCount: 0, | |
| issueCount: 0, | |
| commentCount: 0, | |
| reviewCount: 0, | |
| accountAgeDays: null, | |
| externalDomains: new Set(), | |
| hasShortener: false, | |
| hasNonGitHubBinary: false, | |
| touchesWorkflows: false, | |
| touchesCI: false, | |
| touchesDeps: false, | |
| dupTexts: new Map(), | |
| exampleItems: [], | |
| touchedFiles: new Set(), | |
| examples: [], | |
| }); | |
| } | |
| return perAuthor.get(login); | |
| } | |
| for (const it of items) { | |
| const login = it.author; | |
| if (!login) { | |
| skippedNoLogin += 1; | |
| continue; | |
| } | |
| if (isAllowedAccount(login)) { | |
| skippedAllowed += 1; | |
| if (skippedAllowedLogins.size < MAX_LOGGED_SKIPPED) { | |
| skippedAllowedLogins.add(login); | |
| } | |
| continue; | |
| } | |
| const s = ensureAuthor(login); | |
| await ensureUserCreatedAt(login); | |
| s.itemCount += 1; | |
| if (it.is_pr) s.prCount += 1; | |
| else s.issueCount += 1; | |
| if (s.exampleItems.length < 5) { | |
| s.exampleItems.push({ | |
| title: it.title || "", | |
| url: it.url, | |
| is_pr: it.is_pr, | |
| number: it.number, | |
| }); | |
| } | |
| if (s.examples.length < 5) { | |
| s.examples.push({ url: it.url, is_pr: it.is_pr, number: it.number }); | |
| } | |
| const text = `${it.title}\n\n${it.body}`; | |
| const domains = extractDomains(text); | |
| for (const host of domains) { | |
| if (!host) continue; | |
| if (isExternalDomain(host)) { | |
| s.externalDomains.add(host); | |
| if (!domainAccounts.has(host)) domainAccounts.set(host, new Set()); | |
| domainAccounts.get(host).add(login); | |
| } | |
| if (isShortener(host)) s.hasShortener = true; | |
| } | |
| // Non-GitHub binary/download links | |
| const urlRe = /https?:\/\/[^\s)\]]+/g; | |
| const urlMatches = (text.match(urlRe) || []); | |
| for (const u of urlMatches) { | |
| if (isNonGitHubBinaryUrl(u)) { | |
| s.hasNonGitHubBinary = true; | |
| } | |
| } | |
| // Duplicate-ish content detection (within items we fetched) | |
| const norm = normalizeForDup(text); | |
| if (norm) { | |
| s.dupTexts.set(norm, (s.dupTexts.get(norm) || 0) + 1); | |
| } | |
| } | |
| // PR comments + reviews (deterministic and bounded) | |
| const prItems = items.filter(i => i.is_pr).slice(0, MAX_PR); | |
| for (const it of prItems) { | |
| const login = it.author; | |
| if (login) { | |
| if (isAllowedAccount(login)) continue; | |
| await ensureUserCreatedAt(login); | |
| } | |
| let issueComments = []; | |
| try { | |
| let total = 0; | |
| issueComments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { | |
| owner, | |
| repo, | |
| issue_number: it.number, | |
| per_page: 100, | |
| }, | |
| (response, done) => { | |
| const remaining = 500 - total; | |
| if (remaining <= 0) { | |
| done(); | |
| return []; | |
| } | |
| if (total + response.data.length >= 500) { | |
| total = 500; | |
| done(); | |
| return response.data.slice(0, remaining); | |
| } | |
| total += response.data.length; | |
| return response.data; | |
| } | |
| ); | |
| } catch { | |
| // ignore | |
| } | |
| let reviewComments = []; | |
| try { | |
| let total = 0; | |
| reviewComments = await github.paginate( | |
| github.rest.pulls.listReviewComments, | |
| { | |
| owner, | |
| repo, | |
| pull_number: it.number, | |
| per_page: 100, | |
| }, | |
| (response, done) => { | |
| const remaining = 500 - total; | |
| if (remaining <= 0) { | |
| done(); | |
| return []; | |
| } | |
| if (total + response.data.length >= 500) { | |
| total = 500; | |
| done(); | |
| return response.data.slice(0, remaining); | |
| } | |
| total += response.data.length; | |
| return response.data; | |
| } | |
| ); | |
| } catch { | |
| // ignore | |
| } | |
| let reviews = []; | |
| try { | |
| let total = 0; | |
| reviews = await github.paginate( | |
| github.rest.pulls.listReviews, | |
| { | |
| owner, | |
| repo, | |
| pull_number: it.number, | |
| per_page: 100, | |
| }, | |
| (response, done) => { | |
| const remaining = 500 - total; | |
| if (remaining <= 0) { | |
| done(); | |
| return []; | |
| } | |
| if (total + response.data.length >= 500) { | |
| total = 500; | |
| done(); | |
| return response.data.slice(0, remaining); | |
| } | |
| total += response.data.length; | |
| return response.data; | |
| } | |
| ); | |
| } catch { | |
| // ignore | |
| } | |
| const commentCandidates = [...issueComments, ...reviewComments] | |
| .filter(c => c?.created_at) | |
| .filter(c => new Date(c.created_at) >= start && new Date(c.created_at) <= end) | |
| .sort((a, b) => a.created_at.localeCompare(b.created_at)); | |
| for (const c of commentCandidates) { | |
| const commenter = c.user?.login || ""; | |
| if (!commenter) continue; | |
| if (isAllowedAccount(commenter)) continue; | |
| await ensureUserCreatedAt(commenter); | |
| const s = ensureAuthor(commenter); | |
| s.commentCount += 1; | |
| if (s.examples.length < MAX_COMMENT_EXAMPLES) { | |
| s.examples.push({ url: c.html_url, is_pr: true, number: it.number }); | |
| } | |
| } | |
| const reviewCandidates = reviews | |
| .map(r => ({ | |
| user: r.user, | |
| submitted_at: r.submitted_at || r.submittedAt, | |
| url: r.html_url || `${it.url}#pullrequestreview-${r.id}`, | |
| })) | |
| .filter(r => r.submitted_at) | |
| .filter(r => new Date(r.submitted_at) >= start && new Date(r.submitted_at) <= end) | |
| .sort((a, b) => a.submitted_at.localeCompare(b.submitted_at)); | |
| for (const r of reviewCandidates) { | |
| const reviewer = r.user?.login || ""; | |
| if (!reviewer) continue; | |
| if (isAllowedAccount(reviewer)) continue; | |
| await ensureUserCreatedAt(reviewer); | |
| const s = ensureAuthor(reviewer); | |
| s.reviewCount += 1; | |
| if (s.examples.length < MAX_COMMENT_EXAMPLES) { | |
| s.examples.push({ url: r.url, is_pr: true, number: it.number }); | |
| } | |
| } | |
| } | |
| // PR file touches (sensitive paths) - deterministic and bounded | |
| for (const it of prItems) { | |
| const login = it.author; | |
| if (!login) continue; | |
| if (isAllowedAccount(login)) continue; | |
| const s = ensureAuthor(login); | |
| try { | |
| let total = 0; | |
| const files = await github.paginate( | |
| github.rest.pulls.listFiles, | |
| { | |
| owner, | |
| repo, | |
| pull_number: it.number, | |
| per_page: 100, | |
| }, | |
| (response, done) => { | |
| const remaining = 500 - total; | |
| if (remaining <= 0) { | |
| done(); | |
| return []; | |
| } | |
| if (total + response.data.length >= 500) { | |
| total = 500; | |
| done(); | |
| return response.data.slice(0, remaining); | |
| } | |
| total += response.data.length; | |
| return response.data; | |
| } | |
| ); | |
| const filenames = files.map(f => f.filename); | |
| for (const fn of filenames) { | |
| if (s.touchedFiles.size < MAX_TOUCHED_FILES) s.touchedFiles.add(fn); | |
| if (fn.startsWith(".github/workflows/") || fn.startsWith(".github/actions/")) s.touchesWorkflows = true; | |
| if (fn === "Dockerfile" || fn === "Makefile" || fn.startsWith("scripts/") || fn.startsWith("actions/")) s.touchesCI = true; | |
| if ( | |
| fn === "package.json" || | |
| fn === "package-lock.json" || | |
| fn === "pnpm-lock.yaml" || | |
| fn === "yarn.lock" || | |
| fn === "go.mod" || | |
| fn === "go.sum" || | |
| fn.startsWith("requirements") | |
| ) { | |
| s.touchesDeps = true; | |
| } | |
| } | |
| } catch (e) { | |
| // If file listing fails, do not infer. | |
| } | |
| } | |
| // Score + severity | |
| const accounts = Array.from(perAuthor.values()).map(s => { | |
| if (userCreatedAt.has(s.login)) { | |
| const createdAt = userCreatedAt.get(s.login); | |
| if (createdAt) { | |
| const now = new Date(end); | |
| s.accountAgeDays = Math.max(0, Math.floor((now - createdAt) / (24 * 60 * 60 * 1000))); | |
| } | |
| } | |
| let score = 0; | |
| const extDomains = Array.from(s.externalDomains); | |
| score += Math.min(9, extDomains.length * 3); | |
| if (s.hasShortener) score += 8; | |
| if (s.hasNonGitHubBinary) score += 10; | |
| if (s.touchesWorkflows) score += 15; | |
| if (s.touchesCI) score += 10; | |
| if (s.touchesDeps) score += 6; | |
| if (s.itemCount >= 5) score += 6; | |
| if (s.accountAgeDays !== null && s.accountAgeDays < MIN_ACCOUNT_AGE_DAYS) score += 8; | |
| let hasDup3 = false; | |
| for (const [, c] of s.dupTexts) { | |
| if (c >= 3) { | |
| hasDup3 = true; | |
| break; | |
| } | |
| } | |
| if (hasDup3) score += 8; | |
| score = Math.min(100, score); | |
| let severity = "None"; | |
| if (score >= 20) severity = "High"; | |
| else if (score >= 10) severity = "Medium"; | |
| else if (score >= 1) severity = "Low"; | |
| // Deterministic signal summary | |
| const signals = []; | |
| if (extDomains.length > 0) signals.push(`external_domains=${extDomains.length}`); | |
| if (s.hasShortener) signals.push("shortener"); | |
| if (s.hasNonGitHubBinary) signals.push("non_github_binary_link"); | |
| if (s.touchesWorkflows) signals.push("touches_workflows"); | |
| if (s.touchesCI) signals.push("touches_ci_or_scripts"); | |
| if (s.touchesDeps) signals.push("touches_dependencies"); | |
| if (s.itemCount >= 5) signals.push(`burst_items=${s.itemCount}`); | |
| if (hasDup3) signals.push("dup_text>=3"); | |
| if (s.commentCount > 0) signals.push(`comments=${s.commentCount}`); | |
| if (s.reviewCount > 0) signals.push(`reviews=${s.reviewCount}`); | |
| if (s.accountAgeDays !== null && s.accountAgeDays < MIN_ACCOUNT_AGE_DAYS) { | |
| signals.push(`new_account=${s.accountAgeDays}d`); | |
| } | |
| return { | |
| login: s.login, | |
| risk_score: score, | |
| severity, | |
| signals, | |
| external_domains: extDomains.sort((a, b) => a.localeCompare(b)), | |
| pr_count: s.prCount, | |
| issue_count: s.issueCount, | |
| comment_count: s.commentCount, | |
| review_count: s.reviewCount, | |
| example_items: s.exampleItems, | |
| touched_files: Array.from(s.touchedFiles).sort((a, b) => a.localeCompare(b)), | |
| examples: s.examples, | |
| }; | |
| }); | |
| // Stable sorting | |
| accounts.sort((a, b) => { | |
| if (b.risk_score !== a.risk_score) return b.risk_score - a.risk_score; | |
| return a.login.localeCompare(b.login); | |
| }); | |
| const domains = Array.from(domainAccounts.entries()) | |
| .map(([domain, logins]) => ({ domain, count: logins.size, accounts: Array.from(logins).sort((a, b) => a.localeCompare(b)) })) | |
| .sort((a, b) => { | |
| if (b.count !== a.count) return b.count - a.count; | |
| return a.domain.localeCompare(b.domain); | |
| }); | |
| const topSeverity = accounts.find(a => a.severity !== "None")?.severity || "None"; | |
| // Calculate metrics for observability and decision logic | |
| const highRiskAccounts = accounts.filter(a => a.risk_score >= 10).length; | |
| const multiAccountDomains = domains.filter(d => d.count >= 2).length; | |
| const hasFindings = highRiskAccounts > 0 || multiAccountDomains > 0; | |
| // Log analysis summary for observability | |
| const skippedNames = Array.from(skippedAllowedLogins).sort((a, b) => a.localeCompare(b)); | |
| const skippedLabel = skippedNames.length > 0 ? skippedNames.map(n => `@${n}`).join(", ") : "none"; | |
| const analyzedNames = accounts.slice(0, 10).map(a => `@${a.login}`).join(", ") || "none"; | |
| const domainSamples = domains.slice(0, 10).map(d => d.domain).join(", ") || "none"; | |
| core.info("Summary:"); | |
| core.info(`- Window: ${toISO(start)} -> ${toISO(end)}`); | |
| core.info(`- Items: raw=${rawItems.length}, in_window+dedup=${items.length}`); | |
| core.info(`- PRs scanned: ${prItems.length} (max ${MAX_PR})`); | |
| core.info(`- Skipped (no login): ${skippedNoLogin}`); | |
| core.info(`- Skipped (allowlisted): ${skippedAllowed} [${skippedLabel}]`); | |
| core.info(`- Accounts analyzed: ${accounts.length} [${analyzedNames}]`); | |
| core.info(`- Risk >= 10: ${highRiskAccounts}`); | |
| core.info(`- External domains: total=${domains.length}, shared>=2=${multiAccountDomains} [${domainSamples}]`); | |
| core.info(`- Decision: has_findings=${hasFindings} (will ${hasFindings ? "run" : "skip"} agent job)`); | |
| core.info("Detailed report:"); | |
| if (domains.length === 0) { | |
| core.info("- Domains: none"); | |
| } else { | |
| core.info("- Domains:"); | |
| for (const d of domains) { | |
| const logins = d.accounts.map(login => `@${login}`).join(", ") || "none"; | |
| core.info(` - ${d.domain}: accounts=${d.count} [${logins}]`); | |
| } | |
| } | |
| if (accounts.length === 0) { | |
| core.info("- Accounts: none"); | |
| } else { | |
| core.info("- Accounts:"); | |
| for (const a of accounts) { | |
| const signalsText = a.signals.join(", ") || "none"; | |
| const domainsText = (a.external_domains || []).join(", ") || "none"; | |
| const touchedText = (a.touched_files || []).join(", ") || "none"; | |
| core.info(` - @${a.login}: score=${a.risk_score}, severity=${a.severity}, signals=[${signalsText}]`); | |
| core.info(` - activity: pr=${a.pr_count || 0}, issue=${a.issue_count || 0}, comment=${a.comment_count || 0}, review=${a.review_count || 0}`); | |
| core.info(` - external_domains: ${domainsText}`); | |
| core.info(` - touched_files: ${touchedText}`); | |
| if (a.example_items && a.example_items.length > 0) { | |
| const itemLines = a.example_items | |
| .map(item => { | |
| const label = item.is_pr ? `PR #${item.number}` : `Issue #${item.number}`; | |
| const title = item.title ? ` "${item.title}"` : ""; | |
| return `${label}${title}`; | |
| }) | |
| .join("; "); | |
| core.info(` - examples: ${itemLines}`); | |
| } | |
| if (a.examples && a.examples.length > 0) { | |
| core.info(" - evidence:"); | |
| for (const ex of a.examples) { | |
| core.info(` - ${ex.url}`); | |
| } | |
| } | |
| } | |
| } | |
| // Find existing triage issue (exact title match) | |
| let existingIssueNumber = ""; | |
| try { | |
| const openIssues = await github.rest.issues.listForRepo({ | |
| owner, | |
| repo, | |
| state: "open", | |
| per_page: 100, | |
| }); | |
| const existing = (openIssues.data || []).find(i => (i.title || "") === ISSUE_TITLE); | |
| if (existing?.number) existingIssueNumber = String(existing.number); | |
| } catch (e) { | |
| // ignore | |
| } | |
| // Render deterministic markdown body | |
| function renderBody(includeMention) { | |
| const lines = []; | |
| if (includeMention) lines.push("@pelikhan", ""); | |
| lines.push( | |
| `**Window:** ${toISO(start)} → ${toISO(end)}`, | |
| `**Assessment:** ${topSeverity}`, | |
| "" | |
| ); | |
| if (!hasFindings) { | |
| lines.push("No meaningful suspicious activity detected in this window."); | |
| return lines.join("\n"); | |
| } | |
| if (domains.length > 0) { | |
| lines.push("## Domains (external)", "", "| Domain | Accounts | Logins |", "| --- | ---: | --- |"); | |
| for (const d of domains.slice(0, 20)) { | |
| const maxLogins = 5; | |
| const shown = d.accounts.slice(0, maxLogins).map(login => `@${login}`); | |
| const overflow = d.accounts.length > maxLogins ? ` +${d.accounts.length - maxLogins} more` : ""; | |
| lines.push(`| ${d.domain} | ${d.count} | ${shown.join(", ")}${overflow} |`); | |
| } | |
| lines.push(""); | |
| } | |
| const high = accounts.filter(a => a.severity === "High"); | |
| const med = accounts.filter(a => a.severity === "Medium"); | |
| const low = accounts.filter(a => a.severity === "Low"); | |
| function renderAccounts(title, arr) { | |
| if (arr.length === 0) return; | |
| lines.push(`## ${title}`, ""); | |
| for (const a of arr.slice(0, 25)) { | |
| const sig = a.signals.join(", "); | |
| lines.push(`- @${a.login} — score=${a.risk_score} — ${sig}`); | |
| const changeParts = []; | |
| if (a.example_items && a.example_items.length > 0) { | |
| const itemSamples = a.example_items.slice(0, 2).map(item => { | |
| const label = item.is_pr ? `PR #${item.number}` : `Issue #${item.number}`; | |
| const title = item.title ? ` "${item.title}"` : ""; | |
| return `${label}${title}`; | |
| }); | |
| changeParts.push(itemSamples.join("; ")); | |
| } | |
| if (a.touched_files && a.touched_files.length > 0) { | |
| const files = a.touched_files.slice(0, 6).join(", "); | |
| changeParts.push(`files: ${files}`); | |
| } | |
| if (changeParts.length > 0) { | |
| lines.push(` - Change summary: ${changeParts.join("; ")}`); | |
| } | |
| lines.push( | |
| ` - Activity summary: ${a.pr_count || 0} PR, ${a.issue_count || 0} issue, ${a.comment_count || 0} comment, ${a.review_count || 0} review` | |
| ); | |
| if (a.examples && a.examples.length > 0) { | |
| lines.push(" <details><summary>Evidence</summary>", ""); | |
| for (const ex of a.examples.slice(0, 5)) { | |
| lines.push(` - ${ex.url}`); | |
| } | |
| if (a.examples.length > 5) { | |
| lines.push(` - ... and ${a.examples.length - 5} more`); | |
| } | |
| lines.push("", " </details>"); | |
| } | |
| } | |
| lines.push(""); | |
| } | |
| renderAccounts("Accounts (High)", high); | |
| renderAccounts("Accounts (Medium)", med); | |
| renderAccounts("Accounts (Low)", low); | |
| lines.push("## Notes", "", "- This report is computed deterministically from GitHub Search + PR file listings + PR comments/reviews within the window."); | |
| return lines.join("\n"); | |
| } | |
| let action = "none"; | |
| let issueBody = ""; | |
| let issueNumber = ""; | |
| if (hasFindings) { | |
| if (existingIssueNumber) { | |
| action = "update"; | |
| issueNumber = existingIssueNumber; | |
| issueBody = renderBody(false); | |
| } else { | |
| action = "create"; | |
| issueBody = renderBody(true); | |
| } | |
| } | |
| core.setOutput("action", action); | |
| core.setOutput("issue_number", issueNumber); | |
| core.setOutput("issue_title", ISSUE_TITLE); | |
| core.setOutput("issue_body", issueBody); | |
| safe_outputs: | |
| needs: | |
| - activation | |
| - agent | |
| if: (!cancelled()) && needs.agent.result != 'skipped' | |
| runs-on: ubuntu-slim | |
| permissions: | |
| contents: read | |
| issues: write | |
| timeout-minutes: 45 | |
| env: | |
| GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} | |
| GH_AW_AIC: ${{ needs.agent.outputs.aic }} | |
| GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} | |
| GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/bot-detection" | |
| GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} | |
| GH_AW_ENGINE_ID: "copilot" | |
| GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} | |
| GH_AW_ENGINE_VERSION: "1.0.63" | |
| GH_AW_PROJECT_UTC: "-08:00" | |
| GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} | |
| GH_AW_WORKFLOW_EMOJI: "🤖" | |
| GH_AW_WORKFLOW_ID: "bot-detection" | |
| GH_AW_WORKFLOW_NAME: "Bot Detection" | |
| GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/bot-detection.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: Checkout actions folder | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| repository: github/gh-aw | |
| sparse-checkout: | | |
| actions | |
| persist-credentials: false | |
| - name: Setup Scripts | |
| id: setup | |
| uses: ./actions/setup | |
| with: | |
| destination: ${{ runner.temp }}/gh-aw/actions | |
| job-name: ${{ github.job }} | |
| trace-id: ${{ needs.activation.outputs.setup-trace-id }} | |
| parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} | |
| env: | |
| GH_AW_SETUP_WORKFLOW_NAME: "Bot Detection" | |
| GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/bot-detection.lock.yml@${{ github.ref }} | |
| GH_AW_INFO_VERSION: "1.0.63" | |
| GH_AW_INFO_AWF_VERSION: "v0.27.7" | |
| GH_AW_INFO_ENGINE_ID: "copilot" | |
| - name: Mask OTLP telemetry headers | |
| run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" | |
| - name: Download agent output artifact | |
| id: download-agent-output | |
| continue-on-error: true | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: agent | |
| path: /tmp/gh-aw/ | |
| - name: Setup agent output environment variable | |
| id: setup-agent-output-env | |
| if: steps.download-agent-output.outcome == 'success' | |
| run: | | |
| mkdir -p /tmp/gh-aw/ | |
| find "/tmp/gh-aw/" -type f -print | |
| echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" | |
| - name: Configure GH_HOST for enterprise compatibility | |
| id: ghes-host-config | |
| shell: bash | |
| run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. | |
| # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct | |
| # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. | |
| GH_HOST="${GITHUB_SERVER_URL#https://}" | |
| GH_HOST="${GH_HOST#http://}" | |
| echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" | |
| - name: Process Safe Outputs | |
| id: process_safe_outputs | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} | |
| GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} | |
| GH_AW_ALLOWED_DOMAINS: "*.grafana.net,*.sentry.io,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,www.googleapis.com" | |
| GITHUB_SERVER_URL: ${{ github.server_url }} | |
| GITHUB_API_URL: ${{ github.api_url }} | |
| GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"labels\":[\"security\",\"bot-detection\"],\"max\":1},\"create_report_incomplete_issue\":{},\"mentions\":{\"allowed\":[\"pelikhan\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"*\"}}" | |
| with: | |
| github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} | |
| script: | | |
| const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); | |
| setupGlobals(core, github, context, exec, io, getOctokit); | |
| const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); | |
| await main(); | |
| - name: Upload Safe Outputs Items | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: safe-outputs-items | |
| path: | | |
| /tmp/gh-aw/safe-output-items.jsonl | |
| /tmp/gh-aw/temporary-id-map.json | |
| if-no-files-found: ignore | |