From 1b0a8f752c84146eed7eef0b366a35894bc8fd92 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 22 Jul 2026 09:09:28 +0200 Subject: [PATCH 1/3] Fold Close Stale PRs into deterministic pr-triage stale sweep Replace the agentic close-stale-prs.agent.md with a deterministic stale-PR sweep hosted in pr-triage-batch.yml. The new stale-sweep job runs .github/scripts/pr-stale-sweep.sh weekly (cron 17 4 * * 1) and on manual dispatch (stale_sweep=true), warning about and closing PRs with no non-bot activity for 30/37 days. Preserves the original policy (no-stale and maestro exemptions, drafts included, non-bot activity timer) without model calls or token cost. Deletes close-stale-prs.agent.md and its compiled lock file, and updates docs/design/pr-triage-workflows.md. Fixes #915 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/scripts/pr-stale-sweep.sh | 210 ++ .../workflows/close-stale-prs.agent.lock.yml | 1726 ----------------- .github/workflows/close-stale-prs.agent.md | 96 - .github/workflows/pr-triage-batch.yml | 54 +- docs/design/pr-triage-workflows.md | 31 +- 5 files changed, 290 insertions(+), 1827 deletions(-) create mode 100644 .github/scripts/pr-stale-sweep.sh delete mode 100644 .github/workflows/close-stale-prs.agent.lock.yml delete mode 100644 .github/workflows/close-stale-prs.agent.md diff --git a/.github/scripts/pr-stale-sweep.sh b/.github/scripts/pr-stale-sweep.sh new file mode 100644 index 0000000000..c5adceb71f --- /dev/null +++ b/.github/scripts/pr-stale-sweep.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +# pr-stale-sweep.sh +# +# Deterministic stale-PR sweep. Replaces the former agentic +# `close-stale-prs.agent.md`: no model calls, no tokens — just the GitHub API. +# +# Policy (unchanged from the agentic version): +# * Consider every OPEN pull request, including drafts. +# * "Last activity" is the most recent NON-bot comment or review; if there is +# none, it falls back to the PR's created_at. We deliberately ignore +# `updated_at` and all `[bot]` activity so the bot's own stale-warning +# comment never resets the inactivity timer. +# * created <= 30 days ago -> skip (too new) +# * 30 days < inactivity <= 37 days -> post a stale WARNING +# (once; marker-guarded) +# * inactivity > 37 days -> CLOSE the PR +# * label `no-stale` -> exempt (skip) +# * author dotnet-maestro[bot] / dotnet-maestro -> exempt (skip) +# +# Required env: +# GH_TOKEN — token with pull-requests:write, issues:write +# GITHUB_REPOSITORY — owner/repo (set by Actions) +# +# Optional env: +# DRY_RUN — "true" to log intended actions without any writes +# STALE_MAX — hard cap on warn+close writes per run (default 25) +# WARN_DAYS — inactivity threshold for a warning (default 30) +# CLOSE_DAYS — inactivity threshold for a close (default 37) +# +# Exits 0 on success (including no-op). Non-zero only on hard failures. + +set -euo pipefail + +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" + +DRY_RUN="${DRY_RUN:-false}" +STALE_MAX="${STALE_MAX:-25}" +WARN_DAYS="${WARN_DAYS:-30}" +CLOSE_DAYS="${CLOSE_DAYS:-37}" + +REPO="$GITHUB_REPOSITORY" +OWNER="${REPO%/*}" +NAME="${REPO#*/}" + +# Idempotency marker embedded in every warning comment. Its presence on a PR +# means "already warned" regardless of how the visible text may change. +WARN_MARKER="" + +NOW_SECS=$(date -u +%s) +WARN_CUTOFF_SECS=$(( WARN_DAYS * 86400 )) +CLOSE_CUTOFF_SECS=$(( CLOSE_DAYS * 86400 )) + +log() { printf '[stale-sweep] %s\n' "$*" >&2; } +summary() { + if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + printf '%s\n' "$*" >> "$GITHUB_STEP_SUMMARY" + fi +} + +# Parse an ISO-8601 timestamp to epoch seconds (GNU date on the runner; BSD fallback). +to_epoch() { + local ts="$1" + date -u -d "$ts" +%s 2>/dev/null || date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +%s +} + +WARNING_BODY() { + cat </dev/null || true + gh api --paginate "repos/$REPO/pulls/$pr/reviews" \ + --jq '.[] | select(.user != null) | select((.user.login | endswith("[bot]")) | not) | .submitted_at' 2>/dev/null || true + } | grep -v '^null$' | sort | tail -n 1 + ) + + if [ -z "$newest_ts" ]; then + echo "$created_epoch" + return + fi + to_epoch "$newest_ts" +} + +already_warned() { + local pr="$1" hit + hit=$(gh api --paginate "repos/$REPO/issues/$pr/comments" \ + --jq ".[] | select(.body | contains(\"$WARN_MARKER\")) | .id" 2>/dev/null | head -n 1) + [ -n "$hit" ] +} + +log "repo=$REPO dry_run=$DRY_RUN warn>${WARN_DAYS}d close>${CLOSE_DAYS}d max=$STALE_MAX" + +summary "## Stale PR sweep" +summary "" +summary "Repo \`$REPO\` · dry_run=\`$DRY_RUN\` · warn>\`${WARN_DAYS}d\` · close>\`${CLOSE_DAYS}d\` · max=\`$STALE_MAX\`" +summary "" +summary "| PR | author | created | inactivity(d) | decision |" +summary "|---:|---|---|---:|---|" + +# Enumerate all open PRs (drafts included). --limit caps the working set; PRs +# newer than WARN_DAYS are filtered out per-PR below. +PRS_JSON=$(gh pr list --repo "$REPO" --state open --limit 500 \ + --json number,createdAt,isDraft,author,labels) + +COUNT=$(jq 'length' <<<"$PRS_JSON") +log "open PRs fetched: $COUNT" + +ACTIONS=0 +processed=0 +while IFS= read -r row; do + PR=$(jq -r '.number' <<<"$row") + CREATED_AT=$(jq -r '.createdAt' <<<"$row") + AUTHOR=$(jq -r '.author.login // ""' <<<"$row") + LABELS=$(jq -r '[.labels[].name] | join(",")' <<<"$row") + + # Exemptions ------------------------------------------------------------ + if [[ ",$LABELS," == *",no-stale,"* ]]; then + log "PR #$PR: no-stale label — exempt" + continue + fi + case "$AUTHOR" in + "dotnet-maestro[bot]"|"dotnet-maestro") + log "PR #$PR: maestro-authored — exempt" + continue ;; + esac + + CREATED_EPOCH=$(to_epoch "$CREATED_AT") + AGE_SECS=$(( NOW_SECS - CREATED_EPOCH )) + # Too new: opened within WARN_DAYS. + if [ "$AGE_SECS" -le "$WARN_CUTOFF_SECS" ]; then + continue + fi + + LAST_EPOCH=$(last_non_bot_activity_epoch "$PR" "$CREATED_EPOCH") + INACTIVE_SECS=$(( NOW_SECS - LAST_EPOCH )) + INACTIVE_DAYS=$(( INACTIVE_SECS / 86400 )) + + DECISION="skip(active)" + if [ "$INACTIVE_SECS" -gt "$CLOSE_CUTOFF_SECS" ]; then + DECISION="close" + elif [ "$INACTIVE_SECS" -gt "$WARN_CUTOFF_SECS" ]; then + if already_warned "$PR"; then + DECISION="skip(already-warned)" + else + DECISION="warn" + fi + fi + + if [ "$DECISION" = "skip(active)" ] || [ "$DECISION" = "skip(already-warned)" ]; then + log "PR #$PR: inactivity=${INACTIVE_DAYS}d -> $DECISION" + continue + fi + + summary "| #$PR | $AUTHOR | ${CREATED_AT%%T*} | $INACTIVE_DAYS | $DECISION |" + + if [ "$ACTIONS" -ge "$STALE_MAX" ]; then + log "PR #$PR: reached STALE_MAX=$STALE_MAX — skipping remaining writes" + continue + fi + + case "$DECISION" in + close) + if [ "$DRY_RUN" = "true" ]; then + log "PR #$PR: [DRY_RUN] would close (inactivity=${INACTIVE_DAYS}d)" + else + gh pr comment "$PR" --repo "$REPO" --body "$(CLOSING_BODY)" >/dev/null + gh pr close "$PR" --repo "$REPO" >/dev/null + log "PR #$PR: closed (inactivity=${INACTIVE_DAYS}d)" + fi + ACTIONS=$(( ACTIONS + 1 )) + ;; + warn) + if [ "$DRY_RUN" = "true" ]; then + log "PR #$PR: [DRY_RUN] would post stale warning (inactivity=${INACTIVE_DAYS}d)" + else + gh pr comment "$PR" --repo "$REPO" --body "$(WARNING_BODY)" >/dev/null + log "PR #$PR: warned (inactivity=${INACTIVE_DAYS}d)" + fi + ACTIONS=$(( ACTIONS + 1 )) + ;; + esac + processed=$(( processed + 1 )) +done < <(jq -c '.[]' <<<"$PRS_JSON") + +summary "" +summary "**Actions taken (warn+close): $ACTIONS** (dry_run=\`$DRY_RUN\`)" +log "done — actions=$ACTIONS" diff --git a/.github/workflows/close-stale-prs.agent.lock.yml b/.github/workflows/close-stale-prs.agent.lock.yml deleted file mode 100644 index 2b49fdd715..0000000000 --- a/.github/workflows/close-stale-prs.agent.lock.yml +++ /dev/null @@ -1,1726 +0,0 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"300b9fe706e9f8d8c2191777c99cee2be23faf2ab073c04c4746fae387b603d2","body_hash":"88b13a361dde2f1d90841208e25c17307a0d8195b7005ceb75a265c03b0cd334","compiler_version":"v0.82.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.68"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"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/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"99d9d888952ee25fce70c6b3120ca490d7d8da95","version":"v0.82.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.29","digest":"sha256:debc0b18ef8ea3a64585c4d1eea1099f0d9fa76b53d34a1f3c53b3225fe158fe","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.29@sha256:debc0b18ef8ea3a64585c4d1eea1099f0d9fa76b53d34a1f3c53b3225fe158fe"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.29","digest":"sha256:c7754df3f06f346c817db0525ba523cbdaf5349239fd7f37897c4250a8fc7bde","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.29@sha256:c7754df3f06f346c817db0525ba523cbdaf5349239fd7f37897c4250a8fc7bde"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.29","digest":"sha256:7bfa0742f9a5bd6309507caaa80a8b6cf3e05bd95a1429affbf64cc94cfbd34f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.29@sha256:7bfa0742f9a5bd6309507caaa80a8b6cf3e05bd95a1429affbf64cc94cfbd34f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} -# This file was automatically generated by gh-aw (v0.82.8). 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/ -# -# Automatically warn about and close pull requests that have been open for more than 30 days with no recent activity. -# -# Resolved workflow manifest: -# Imports: -# - shared/pat_pool.md -# -# Secrets used: -# - COPILOT_PAT_0 -# - COPILOT_PAT_1 -# - COPILOT_PAT_2 -# - COPILOT_PAT_3 -# - COPILOT_PAT_4 -# - COPILOT_PAT_5 -# - COPILOT_PAT_6 -# - COPILOT_PAT_7 -# - COPILOT_PAT_8 -# - COPILOT_PAT_9 -# - GH_AW_GITHUB_MCP_SERVER_TOKEN -# - GH_AW_GITHUB_TOKEN -# - GITHUB_TOKEN -# -# Custom actions used: -# - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - 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/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 -# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@99d9d888952ee25fce70c6b3120ca490d7d8da95 # v0.82.8 -# -# Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.29@sha256:debc0b18ef8ea3a64585c4d1eea1099f0d9fa76b53d34a1f3c53b3225fe158fe -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.29@sha256:c7754df3f06f346c817db0525ba523cbdaf5349239fd7f37897c4250a8fc7bde -# - ghcr.io/github/gh-aw-firewall/squid:0.27.29@sha256:7bfa0742f9a5bd6309507caaa80a8b6cf3e05bd95a1429affbf64cc94cfbd34f -# - ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 - -name: "Close Stale Pull Requests" -on: - # permissions: {} # Permissions applied to pre-activation job - schedule: - - cron: "6 22 * * 1" # Friendly format: weekly on monday (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: "Close Stale Pull Requests" - -jobs: - activation: - needs: - - pat_pool - - pre_activation - if: > - needs.pre_activation.outputs.activated == 'true' && ((!(github.event_name == 'schedule' && github.event.repository.fork))) - runs-on: ubuntu-slim - permissions: - actions: read - contents: read - env: - GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} - 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 }} - oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} - 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: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@99d9d888952ee25fce70c6b3120ca490d7d8da95 # v0.82.8 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} - parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} - safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.68" - GH_AW_INFO_AWF_VERSION: "v0.27.29" - GH_AW_INFO_ENGINE_ID: "copilot" - - 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.68" - GH_AW_INFO_AGENT_VERSION: "1.0.68" - GH_AW_INFO_CLI_VERSION: "v0.82.8" - GH_AW_INFO_WORKFLOW_NAME: "Close Stale Pull Requests" - GH_AW_INFO_EXPERIMENTAL: "false" - GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" - GH_AW_INFO_STAGED: "false" - GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' - GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.29" - GH_AW_INFO_AWMG_VERSION: "" - GH_AW_INFO_FIREWALL_TYPE: "squid" - 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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - key: agentic-workflow-usage-closestaleprs.agent-${{ github.run_id }} - restore-keys: agentic-workflow-usage-closestaleprs.agent- - 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: "Close Stale Pull Requests" - GH_AW_WORKFLOW_ID: "close-stale-prs.agent" - 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: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} - 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: Check for OAuth tokens - id: check-oauth-tokens - run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" - env: - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, 'NO COPILOT PAT AVAILABLE') }} - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - sparse-checkout: | - .github - .agents - .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: "close-stale-prs.agent.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: Check compile-agentic version - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_COMPILED_VERSION: "v0.82.8" - 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_version_updates.cjs'); - await main(); - - name: Log runtime features - if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} - run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - - 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 }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_fb209a28380afd03_EOF' - - GH_AW_PROMPT_fb209a28380afd03_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_fb209a28380afd03_EOF' - - Tools: add_comment(max:30), close_pull_request(max:25), missing_tool, missing_data, noop - - GH_AW_PROMPT_fb209a28380afd03_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_fb209a28380afd03_EOF' - - 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}} - - - GH_AW_PROMPT_fb209a28380afd03_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_fb209a28380afd03_EOF' - - {{#runtime-import .github/workflows/close-stale-prs.agent.md}} - GH_AW_PROMPT_fb209a28380afd03_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" - 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_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} - 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_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED - } - }); - - name: Validate prompt placeholders - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # 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 - - pat_pool - if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' - runs-on: ubuntu-latest - environment: copilot-pat-pool - permissions: - contents: 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_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} - GH_AW_WORKFLOW_ID_SANITIZED: closestaleprs.agent - 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 }} - http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} - 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: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@99d9d888952ee25fce70c6b3120ca490d7d8da95 # v0.82.8 - 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: "Close Stale Pull Requests" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.68" - GH_AW_INFO_AWF_VERSION: "v0.27.29" - 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: 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: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: activation - path: /tmp/gh-aw - - 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.68 - env: - GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.29 --rootless - - 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: 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.29@sha256:debc0b18ef8ea3a64585c4d1eea1099f0d9fa76b53d34a1f3c53b3225fe158fe ghcr.io/github/gh-aw-firewall/api-proxy:0.27.29@sha256:c7754df3f06f346c817db0525ba523cbdaf5349239fd7f37897c4250a8fc7bde ghcr.io/github/gh-aw-firewall/squid:0.27.29@sha256:7bfa0742f9a5bd6309507caaa80a8b6cf3e05bd95a1429affbf64cc94cfbd34f ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 - - 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_42f3b4436ff28f61_EOF' - {"add_comment":{"max":30},"close_pull_request":{"max":25},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_42f3b4436ff28f61_EOF - - name: Generate Safe Outputs Tools - env: - GH_AW_TOOLS_META_JSON: | - { - "description_suffixes": { - "add_comment": " CONSTRAINTS: Maximum 30 comment(s) can be added. Supports reply_to_id for discussion threading.", - "close_pull_request": " CONSTRAINTS: Maximum 25 pull request(s) can be closed." - }, - "repo_params": {}, - "dynamic_tools": [] - } - GH_AW_VALIDATION_JSON: | - { - "add_comment": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "item_number": { - "issueOrPRNumber": true - }, - "reply_to_id": { - "type": "string", - "maxLength": 256 - }, - "repo": { - "type": "string", - "maxLength": 256 - } - } - }, - "close_pull_request": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "pull_request_number": { - "optionalPositiveInteger": true - }, - "repo": { - "type": "string", - "maxLength": 256 - } - } - }, - "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 - } - } - } - } - 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_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} - 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="awmg-mcpg" - 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" - MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') - MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_POLICY_ALLOW_CREATE_PULL_REQUEST -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 -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.4.1' - - 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_d97c92af15acf38e_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.5.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}", - "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", - "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", - "GITHUB_SHA": "\${GITHUB_SHA}", - "GITHUB_TOKEN": "\${GITHUB_TOKEN}", - "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", - "RUNNER_TEMP": "\${RUNNER_TEMP}" - }, - "guard-policies": { - "write-sink": { - "accept": [ - "*" - ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} - } - } - } - }, - "gateway": { - "port": $MCP_GATEWAY_PORT, - "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", - "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" - } - } - GH_AW_MCP_CONFIG_d97c92af15acf38e_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: 20 - 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.29/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"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\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"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*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"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\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.29,squid=sha256:7bfa0742f9a5bd6309507caaa80a8b6cf3e05bd95a1429affbf64cc94cfbd34f,agent=sha256:debc0b18ef8ea3a64585c4d1eea1099f0d9fa76b53d34a1f3c53b3225fe158fe,api-proxy=sha256:c7754df3f06f346c817db0525ba523cbdaf5349239fd7f37897c4250a8fc7bde,cli-proxy=sha256:bfc90b1f10d2ff61dbbbf0d57600001f85bf5f6444ed78bc05d9f6677327e4b8\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${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 - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" - 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,SC2016,SC2086 - 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"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --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; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, 'NO COPILOT PAT AVAILABLE') }} - 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: 20 - GH_AW_VERSION: v0.82.8 - 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_PAT_0,COPILOT_PAT_1,COPILOT_PAT_2,COPILOT_PAT_3,COPILOT_PAT_4,COPILOT_PAT_5,COPILOT_PAT_6,COPILOT_PAT_7,COPILOT_PAT_8,COPILOT_PAT_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} - SECRET_COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} - SECRET_COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} - SECRET_COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} - SECRET_COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} - SECRET_COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} - SECRET_COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} - SECRET_COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} - SECRET_COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} - SECRET_COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} - 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: "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/ - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - 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: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - - 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: 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/safeoutputs.jsonl - /tmp/gh-aw/agent_output.json - /tmp/gh-aw/aw-*.patch - /tmp/gh-aw/aw-*.bundle - /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 - - detection - - pat_pool - - safe_outputs - if: > - always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') - runs-on: ubuntu-slim - environment: copilot-pat-pool - permissions: - contents: read - issues: write - pull-requests: write - concurrency: - group: "gh-aw-conclusion-close-stale-prs.agent" - 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: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@99d9d888952ee25fce70c6b3120ca490d7d8da95 # v0.82.8 - 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: "Close Stale Pull Requests" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.68" - GH_AW_INFO_AWF_VERSION: "v0.27.29" - 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: Download safe outputs items manifest - id: download-safe-outputs-manifest - if: always() - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: safe-outputs-items - path: /tmp/gh-aw/ - - 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 - [ -s /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 - [ -s /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 - [ -s /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 - [ -s /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 - [ -s /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 - [ -s /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/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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - key: agentic-workflow-usage-closestaleprs.agent-${{ github.run_id }} - restore-keys: agentic-workflow-usage-closestaleprs.agent- - 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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - key: agentic-workflow-usage-closestaleprs.agent-${{ 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: "Close Stale Pull Requests" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/close-stale-prs.agent.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: "false" - GH_AW_AIC: ${{ needs.agent.outputs.aic }} - GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} - GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} - GH_AW_WORKFLOW_ID: "close-stale-prs.agent" - 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: Log detection run - id: detection_runs - 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: "Close Stale Pull Requests" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/close-stale-prs.agent.md" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} - GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} - 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_detection_runs.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: "Close Stale Pull Requests" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/close-stale-prs.agent.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: "Close Stale Pull Requests" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/close-stale-prs.agent.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: "Close Stale Pull Requests" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/close-stale-prs.agent.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: "close-stale-prs.agent" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" - GH_AW_ENGINE_ID: "copilot" - 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_THREAT_DETECTION_AIC: ${{ needs.detection.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_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_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_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_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: "20" - 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(); - - detection: - needs: - - activation - - agent - - pat_pool - if: always() && needs.agent.result != 'skipped' - runs-on: ubuntu-latest - environment: copilot-pat-pool - permissions: - contents: read - env: - GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} - outputs: - aic: ${{ steps.parse_detection_token_usage.outputs.aic }} - detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} - detection_reason: ${{ steps.detection_conclusion.outputs.reason }} - detection_success: ${{ steps.detection_conclusion.outputs.success }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@99d9d888952ee25fce70c6b3120ca490d7d8da95 # v0.82.8 - 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: "Close Stale Pull Requests" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.68" - GH_AW_INFO_AWF_VERSION: "v0.27.29" - 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: Checkout repository for patch context - if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - # --- Threat Detection --- - - name: Clean stale firewall files from agent artifact - run: | - rm -rf /tmp/gh-aw/sandbox/firewall/logs - rm -rf /tmp/gh-aw/sandbox/firewall/audit - - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.29@sha256:debc0b18ef8ea3a64585c4d1eea1099f0d9fa76b53d34a1f3c53b3225fe158fe ghcr.io/github/gh-aw-firewall/api-proxy:0.27.29@sha256:c7754df3f06f346c817db0525ba523cbdaf5349239fd7f37897c4250a8fc7bde ghcr.io/github/gh-aw-firewall/squid:0.27.29@sha256:7bfa0742f9a5bd6309507caaa80a8b6cf3e05bd95a1429affbf64cc94cfbd34f - - name: Check if detection needed - id: detection_guard - if: always() - env: - OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - run: | - if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then - echo "run_detection=true" >> "$GITHUB_OUTPUT" - echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" - else - echo "run_detection=false" >> "$GITHUB_OUTPUT" - echo "Detection skipped: no agent outputs or patches to analyze" - fi - - name: Clear MCP Config for detection - if: always() && steps.detection_guard.outputs.run_detection == 'true' - run: | - rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f "$HOME/.copilot/mcp-config.json" - rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - - name: Prepare threat detection files - if: always() && steps.detection_guard.outputs.run_detection == 'true' - run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - for f in /tmp/gh-aw/aw-*.bundle; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true - - name: Setup threat detection - if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - WORKFLOW_NAME: "Close Stale Pull Requests" - WORKFLOW_DESCRIPTION: "Automatically warn about and close pull requests that have been open for more than 30 days with no recent activity." - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - 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/setup_threat_detection.cjs'); - await main(); - - name: Ensure threat-detection directory and log - if: always() && steps.detection_guard.outputs.run_detection == 'true' - run: | - mkdir -p /tmp/gh-aw/threat-detection - touch /tmp/gh-aw/threat-detection/detection.log - - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: '24' - package-manager-cache: false - - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.68 - env: - GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.29 - - name: Execute GitHub Copilot CLI - if: always() && steps.detection_guard.outputs.run_detection == 'true' - continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 - 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" - 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/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.29/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5},\"container\":{\"imageTag\":\"0.27.29,squid=sha256:7bfa0742f9a5bd6309507caaa80a8b6cf3e05bd95a1429affbf64cc94cfbd34f,agent=sha256:debc0b18ef8ea3a64585c4d1eea1099f0d9fa76b53d34a1f3c53b3225fe158fe,api-proxy=sha256:c7754df3f06f346c817db0525ba523cbdaf5349239fd7f37897c4250a8fc7bde,cli-proxy=sha256:bfc90b1f10d2ff61dbbbf0d57600001f85bf5f6444ed78bc05d9f6677327e4b8\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${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 - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - 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,SC2016,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"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${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; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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 --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log - env: - AWF_REFLECT_ENABLED: 1 - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, 'NO COPILOT PAT AVAILABLE') }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_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_DETECTION_MAX_AI_CREDITS || '400' }} - GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - GH_AW_PHASE: detection - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.82.8 - GITHUB_API_URL: ${{ github.api_url }} - GITHUB_AW: true - GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md - GITHUB_WORKSPACE: ${{ github.workspace }} - 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: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() - continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage - 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: Upload threat detection log - if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: detection - path: /tmp/gh-aw/threat-detection/detection.log - if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion - if: always() - continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} - DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} - GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - 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_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } - - pat_pool: - needs: pre_activation - runs-on: ubuntu-slim - environment: copilot-pat-pool - outputs: - pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} - 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: Select Copilot token from pool - id: select-pat-number - run: | - # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_9. - PAT_NUMBERS=() - POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) - - for i in $(seq 0 9); do - var="COPILOT_PAT_${i}" - val="${!var}" - if [ -n "$val" ]; then - PAT_NUMBERS+=(${i}) - POOL_INDICATORS[${i}]="🟪" - fi - done - - # If none of the entries in the pool have values, emit a warning - # and do not set an output value. The consumer can fall back to - # using COPILOT_GITHUB_TOKEN. - if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then - warning_message="::warning::None of the PAT pool entries had values " - warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_9)" - echo "$warning_message" - exit 0 - fi - - # Select a random index using the seed if specified - if [ -n "$RANDOM_SEED" ]; then - RANDOM=$RANDOM_SEED - fi - - PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) - PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" - POOL_INDICATORS[${PAT_NUMBER}]="✅" - - echo "Pool size: ${#PAT_NUMBERS[@]}" - echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" - - # Emit a markdown table of the pool entries to the step summary - echo "|0|1|2|3|4|5|6|7|8|9|" >> "$GITHUB_STEP_SUMMARY" - echo "|-|-|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" - (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" - - # Set the PAT number as the output - echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" - env: - COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} - COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} - COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} - COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} - COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} - COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} - COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} - COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} - COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} - COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} - RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} - shell: bash - - pre_activation: - if: (!(github.event_name == 'schedule' && github.event.repository.fork)) - runs-on: ubuntu-slim - environment: copilot-pat-pool - env: - GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} - outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - matched_command: '' - 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 }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@99d9d888952ee25fce70c6b3120ca490d7d8da95 # v0.82.8 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.68" - GH_AW_INFO_AWF_VERSION: "v0.27.29" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Check team membership for workflow - id: check_membership - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_REQUIRED_ROLES: "admin,maintainer,write" - 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_membership.cjs'); - await main(); - - safe_outputs: - needs: - - activation - - agent - - detection - if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' - runs-on: ubuntu-slim - environment: copilot-pat-pool - permissions: - contents: read - issues: write - pull-requests: 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 }}/close-stale-prs.agent" - GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} - GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} - 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.68" - GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} - GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} - GH_AW_WORKFLOW_ID: "close-stale-prs.agent" - GH_AW_WORKFLOW_NAME: "Close Stale Pull Requests" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/close-stale-prs.agent.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 }} - comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} - comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} - create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} - create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} - process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} - process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@99d9d888952ee25fce70c6b3120ca490d7d8da95 # v0.82.8 - 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: "Close Stale Pull Requests" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.68" - GH_AW_INFO_AWF_VERSION: "v0.27.29" - 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: 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: "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: "{\"add_comment\":{\"max\":30},\"close_pull_request\":{\"max\":25},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" - 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 diff --git a/.github/workflows/close-stale-prs.agent.md b/.github/workflows/close-stale-prs.agent.md deleted file mode 100644 index 6c586471e8..0000000000 --- a/.github/workflows/close-stale-prs.agent.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -name: "Close Stale Pull Requests" -description: "Automatically warn about and close pull requests that have been open for more than 30 days with no recent activity." -on: - permissions: {} - schedule: weekly on monday - workflow_dispatch: # Allow manual triggering - -# Don't run scheduled triggers on forked repositories — forks lack the -# secrets and context required, and scheduled runs would consume the -# fork owner's minutes. -if: ${{ (!(github.event_name == 'schedule' && github.event.repository.fork)) }} -safe-outputs: - close-pull-request: - max: 25 - add-comment: - max: 30 - noop: - report-as-issue: false - -# ############################################################### -# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. -# Run agentic jobs in an isolated `copilot-pat-pool` environment. -# -# When org-level billing is available, this will be removed. -# See `shared/pat_pool.README.md` for more information. -# ############################################################### -imports: - - uses: shared/pat_pool.md - with: - environment: copilot-pat-pool - -environment: copilot-pat-pool - -engine: - id: copilot - env: - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, 'NO COPILOT PAT AVAILABLE') }} ---- - -# Close Stale Pull Requests - -You are an automated repository maintenance agent for the current repository. - -## Task - -Find pull requests that have been open for more than **30 days** and have had no recent activity. Depending on how long they have been inactive, either **warn** the author or **close** the pull request. - -## Definitions - -- **Created date**: The date the pull request was originally opened. -- **Last activity date**: The date of the most recent **non-bot** activity on the pull request. To determine this, list the PR's comments and reviews and find the most recent one **not** authored by a bot (i.e., ignore comments from users whose login ends with `[bot]`). If there are no non-bot comments or reviews, fall back to the PR's `created_at` date. Do **not** rely on `updated_at` alone, because the bot's own stale-warning comment updates `updated_at` and would reset the inactivity timer. -- **Stale (warning)**: A PR is eligible for a stale warning if it was created more than 30 days ago **and** its last non-bot activity was more than 30 days ago but no more than 37 days ago (i.e., 30 < days_since_last_non_bot_activity ≤ 37), **and** the PR does not already have a stale warning comment from this bot. -- **Stale (close)**: A PR is eligible for closure if it was created more than 30 days ago **and** its last non-bot activity was more than 37 days ago. - -## Instructions - -1. Calculate the date 30 days before the current time. -2. Retrieve the complete set of open pull requests created before that date, including both draft and non-draft pull requests: - a. Prefer a repository-scoped pull request search for `is:pr is:open created:<=YYYY-MM-DD` to avoid fetching newer pull requests without excluding PRs from the cutoff date. - b. Request 100 results per page, starting with page 1, and increment the page number until a page returns fewer than 100 results. - c. Do not treat the first page as the complete result set and do not conclude that no older pull requests exist until every page has been processed. -3. For each open pull request: - a. Skip it if it has the label `no-stale` — these are exempt from this policy. - b. Skip it if it was authored by `dotnet-maestro[bot]` or `dotnet-maestro` — dependency update PRs are managed separately. - c. Skip it if it was created **fewer than 30 days ago**. - d. Determine the **last non-bot activity date**: fetch the PR's comments and reviews, find the most recent entry not authored by a bot (login ending in `[bot]`), and use its date. If none exist, use the PR's `created_at` date. -4. Sort eligible pull requests by last non-bot activity date, oldest first, so closures are processed before warnings. -5. For each eligible pull request (created more than 30 days ago): - - If last non-bot activity was **more than 37 days ago**: **Close** the pull request using the `close_pull_request` tool (with `pull_request_number` set to the PR number) and the closing comment below. - - If last non-bot activity was **more than 30 days ago but 37 or fewer days ago** and the PR does not already have a stale warning comment from this bot: **Post a stale warning comment** using the `add_comment` tool (with `item_number` set to the PR number) and the warning comment below. - - Otherwise (non-bot activity within the last 30 days): Skip it — it is not stale. -6. If no pull request requires a warning or closure, call the `noop` tool with a brief explanation. - -## Important - -- You **must** use the `close_pull_request` tool to close pull requests. Always provide the `pull_request_number` parameter with the PR number — this workflow runs on a schedule, not on a PR event, so the tool cannot auto-detect the target PR. -- You **must** use the `add_comment` tool to post stale warning comments. Always provide the `item_number` parameter with the PR number. -- Draft pull requests are subject to the same stale policy as non-draft pull requests and must not be filtered out. -- You **must** paginate through the complete candidate set. A full page of 100 results means another page may exist. -- When determining staleness, ignore all bot activity (comments/reviews from users with logins ending in `[bot]`). Only human activity resets the inactivity timer. -- Process all eligible pull requests, up to the tool limits. - -## Stale Warning Comment Template - -Use the following comment when warning about a stale pull request (using `add_comment`): - -> This PR has been automatically marked as stale because it has no activity for 30 days. It will be closed if no further activity occurs within another 7 days of this comment. If it is closed, you may reopen it anytime when you're ready again. - -## Closing Comment Template - -Use the following comment when closing a stale pull request (as the `body` of `close_pull_request`): - -> This pull request has been automatically closed because it has been open for more than 30 days with no recent activity. -> -> If you believe this work is still relevant, please feel free to reopen or create a new pull request. Thank you for your contribution! diff --git a/.github/workflows/pr-triage-batch.yml b/.github/workflows/pr-triage-batch.yml index ab32018ec9..dd722959f2 100644 --- a/.github/workflows/pr-triage-batch.yml +++ b/.github/workflows/pr-triage-batch.yml @@ -10,15 +10,22 @@ name: "PR Triage — Batch" # triggering the scanner workflow. That marker is the source of truth that # survives any scanner-side failure mode (PAT outage, integrity block, # dropped HTML marker by the agent). +# +# This workflow also hosts the deterministic weekly stale-PR sweep (the +# `stale-sweep` job, cron `17 4 * * 1`), which replaces the former agentic +# `close-stale-prs.agent.md`. It warns about and closes PRs that have had no +# non-bot activity for 30 / 37 days. See .github/scripts/pr-stale-sweep.sh. on: schedule: # Off-the-hour to avoid colliding with evaluation.yml's daily 00:00 UTC cron. - cron: "17 * * * *" + # Weekly (Mon 04:17 UTC) deterministic stale-PR sweep — see the stale-sweep job. + - cron: "17 4 * * 1" workflow_dispatch: inputs: dry_run: - description: "If 'true', list states without dispatching worker runs." + description: "If 'true', list states / stale decisions without making writes." required: false type: string default: "false" @@ -31,6 +38,16 @@ on: required: false type: string default: "30" + stale_sweep: + description: "If 'true', run the deterministic stale-PR sweep instead of the triage dispatch." + required: false + type: string + default: "false" + stale_max: + description: "Hard cap on stale warn+close writes per sweep. Default 25." + required: false + type: string + default: "25" permissions: pull-requests: read @@ -43,11 +60,13 @@ concurrency: group: pr-triage-batch cancel-in-progress: false -run-name: "PR triage batch${{ inputs.pr_number && format(' (PR #{0})', inputs.pr_number) || '' }}${{ inputs.dry_run == 'true' && ' [dry-run]' || '' }}" +run-name: "PR triage batch${{ inputs.stale_sweep == 'true' && ' — stale sweep' || '' }}${{ inputs.pr_number && format(' (PR #{0})', inputs.pr_number) || '' }}${{ inputs.dry_run == 'true' && ' [dry-run]' || '' }}" jobs: dispatch: - if: ${{ !github.event.repository.fork }} + # Runs on the hourly cron and on manual dispatches (unless the dispatch asked + # for a stale sweep). Skipped on the weekly stale-sweep cron. + if: ${{ !github.event.repository.fork && (github.event_name != 'schedule' || github.event.schedule == '17 * * * *') && inputs.stale_sweep != 'true' }} runs-on: ubuntu-latest steps: - name: Enumerate open PRs and dispatch workers @@ -221,3 +240,32 @@ jobs: done echo "Dispatched $DISPATCHED worker run(s)." + + # Deterministic stale-PR sweep. Runs on the weekly cron and on manual + # dispatches that pass stale_sweep=true. Warns about / closes PRs with no + # non-bot activity for 30 / 37 days. Replaces close-stale-prs.agent.md. + stale-sweep: + if: ${{ !github.event.repository.fork && ((github.event_name == 'schedule' && github.event.schedule == '17 4 * * 1') || (github.event_name == 'workflow_dispatch' && inputs.stale_sweep == 'true')) }} + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + steps: + - name: Checkout stale-sweep script + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6 + with: + persist-credentials: false + fetch-depth: 1 + sparse-checkout: | + .github/scripts + sparse-checkout-cone-mode: false + + - name: Run pr-stale-sweep.sh + env: + GH_TOKEN: ${{ github.token }} + DRY_RUN: ${{ inputs.dry_run || 'false' }} + STALE_MAX: ${{ inputs.stale_max || '25' }} + run: | + chmod +x .github/scripts/pr-stale-sweep.sh + ./.github/scripts/pr-stale-sweep.sh diff --git a/docs/design/pr-triage-workflows.md b/docs/design/pr-triage-workflows.md index 4be66c9bbb..2051b38dff 100644 --- a/docs/design/pr-triage-workflows.md +++ b/docs/design/pr-triage-workflows.md @@ -5,7 +5,8 @@ Three GitHub Actions workflows keep open PRs moving without manual nudging: - **`pr-triage-batch.yml`** — hourly orchestrator (cron `17 * * * *`). Enumerates open non-draft PRs, computes a deterministic state for each, and dispatches the per-PR worker (or the malicious-code scanner). No comments, no labels, no model - calls. + calls. Also hosts the deterministic weekly **stale-PR sweep** (`stale-sweep` + job, cron `17 4 * * 1`) — see [Stale-PR sweep](#stale-pr-sweep). - **`pr-triage.yml`** — per-PR worker (`workflow_dispatch`). Re-validates the PR's state, reconciles a single `pr-state/*` label, and performs at most one of: trigger evaluation (by dispatching `evaluation.yml`), ping the author, or @@ -19,12 +20,14 @@ Three GitHub Actions workflows keep open PRs moving without manual nudging: ```mermaid flowchart TD Cron["cron: every hour"] --> Batch["pr-triage-batch.yml
(orchestrator)"] + WCron["cron: weekly (Mon)"] --> Sweep["pr-triage-batch.yml
(stale-sweep job)"] Batch -->|workflow_dispatch| Worker["pr-triage.yml
(per-PR worker)"] Batch -->|workflow_dispatch| Scan["pr-malicious-scan.agent.lock.yml
(per-PR scanner)"] Worker -->|workflow_dispatch: pr_number| Eval["evaluation.yml
(existing)"] Worker -->|adds pr-state/* label| PR[("PR")] Worker -->|posts ping comment| PR Scan -->|code-scanning alert + comment| PR + Sweep -->|warn / close stale| PR PR -.->|human adds label: evaluate-now| Eval ``` @@ -91,4 +94,28 @@ a duplicate `pr-state/*` name: Triggers and opt-outs: - `evaluate-now` — applied to fire evaluation; removed by the gate after consumption. -- `no-stale` — opt-out of stale-PR closure (consumed by `close-stale-prs`). +- `no-stale` — opt-out of stale-PR closure (honored by the `stale-sweep` job) and + of author/maintainer pings in the worker. + +## Stale-PR sweep + +`pr-triage-batch.yml` includes a deterministic `stale-sweep` job that replaces the +former agentic `close-stale-prs.agent.md`. It runs weekly (cron `17 4 * * 1`) and +on manual `workflow_dispatch` with `stale_sweep=true`, and executes +[`.github/scripts/pr-stale-sweep.sh`](../../.github/scripts/pr-stale-sweep.sh) — no +model calls, no tokens. + +Policy (unchanged from the agentic version): + +- Considers every **open** PR, **including drafts**. +- "Last activity" is the most recent **non-bot** comment or review; if there is + none, it falls back to the PR's `created_at`. `updated_at` and all `[bot]` + activity are ignored so the bot's own warning never resets the timer. +- created ≤ 30 days ago → skip (too new). +- 30 days < inactivity ≤ 37 days → post a stale **warning** (once; guarded by a + `` marker). +- inactivity > 37 days → **close** the PR with a closing comment. +- Exempt: the `no-stale` label; authors `dotnet-maestro[bot]` / `dotnet-maestro`. + +Inputs (via `workflow_dispatch`): `stale_sweep` (run the sweep), `dry_run` (log +decisions without writing), `stale_max` (hard cap on warn+close writes, default 25). From 9f8127631926855d97c34616bb8b329b890369aa Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 22 Jul 2026 19:31:33 +0200 Subject: [PATCH 2/3] Harden stale-sweep: fail-safe reads, sort-before-cap, full pagination, review comments Address code review on PR #928: - Never decide on partial data: if any activity source read fails, skip the PR instead of swallowing the error and risking a wrongful close. - Apply STALE_MAX only after sorting eligible PRs by inactivity (stalest first), so the cap can't starve an old closure behind a newer warning. - Enumerate open PRs with full pagination (gh api --paginate) instead of a fixed --limit. - Include inline review comments as a human-activity source. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/scripts/pr-stale-sweep.sh | 206 ++++++++++++++++++++---------- 1 file changed, 136 insertions(+), 70 deletions(-) diff --git a/.github/scripts/pr-stale-sweep.sh b/.github/scripts/pr-stale-sweep.sh index c5adceb71f..8ffbd61844 100644 --- a/.github/scripts/pr-stale-sweep.sh +++ b/.github/scripts/pr-stale-sweep.sh @@ -5,11 +5,12 @@ # `close-stale-prs.agent.md`: no model calls, no tokens — just the GitHub API. # # Policy (unchanged from the agentic version): -# * Consider every OPEN pull request, including drafts. -# * "Last activity" is the most recent NON-bot comment or review; if there is -# none, it falls back to the PR's created_at. We deliberately ignore -# `updated_at` and all `[bot]` activity so the bot's own stale-warning -# comment never resets the inactivity timer. +# * Consider every OPEN pull request, including drafts. The open-PR set is +# fetched with full pagination (no upper bound). +# * "Last activity" is the most recent NON-bot issue comment, review, or +# inline review comment; if there is none, it falls back to the PR's +# created_at. We deliberately ignore `updated_at` and all `[bot]` activity +# so the bot's own stale-warning comment never resets the inactivity timer. # * created <= 30 days ago -> skip (too new) # * 30 days < inactivity <= 37 days -> post a stale WARNING # (once; marker-guarded) @@ -17,6 +18,14 @@ # * label `no-stale` -> exempt (skip) # * author dotnet-maestro[bot] / dotnet-maestro -> exempt (skip) # +# Safety: +# * Fail-safe reads: if any activity/comment source cannot be read for a PR, +# that PR is SKIPPED (never decided on partial data), so a transient API +# failure can never cause a wrongful close. +# * STALE_MAX is applied only AFTER sorting eligible PRs by inactivity +# (stalest first), so the cap can never starve an old closure behind a +# newer warning. +# # Required env: # GH_TOKEN — token with pull-requests:write, issues:write # GITHUB_REPOSITORY — owner/repo (set by Actions) @@ -82,59 +91,78 @@ EOF # Most recent NON-bot activity epoch for a PR. Considers issue comments and # reviews; ignores any author whose login ends in "[bot]". Falls back to the # supplied created_at epoch when there is no human activity. +# +# Returns non-zero if ANY activity source cannot be read. Callers MUST treat a +# non-zero return as "unknown activity" and skip the PR — silently omitting a +# source could drop the only recent human touch and wrongly close a live PR. last_non_bot_activity_epoch() { local pr="$1" created_epoch="$2" - local newest_ts - - # Emit one timestamp per non-bot comment/review, then pick the max. --paginate - # runs --jq per page, so we must aggregate in the shell, not inside jq. - newest_ts=$( - { - gh api --paginate "repos/$REPO/issues/$pr/comments" \ - --jq '.[] | select((.user.login | endswith("[bot]")) | not) | .created_at' 2>/dev/null || true - gh api --paginate "repos/$REPO/pulls/$pr/reviews" \ - --jq '.[] | select(.user != null) | select((.user.login | endswith("[bot]")) | not) | .submitted_at' 2>/dev/null || true - } | grep -v '^null$' | sort | tail -n 1 - ) + local issue_comments reviews review_comments newest_ts + + # Three human-activity sources. Each --paginate runs --jq per page, so we + # aggregate timestamps in the shell. Do NOT swallow gh failures: if a read + # fails, propagate it so the caller skips the PR rather than deciding on + # partial data. + # 1. issue comments (repos/{}/issues/{pr}/comments) -> created_at + # 2. reviews (repos/{}/pulls/{pr}/reviews) -> submitted_at + # 3. inline review comments(repos/{}/pulls/{pr}/comments) -> created_at + if ! issue_comments=$(gh api --paginate "repos/$REPO/issues/$pr/comments" \ + --jq '.[] | select(.user != null) | select((.user.login | endswith("[bot]")) | not) | .created_at'); then + return 1 + fi + if ! reviews=$(gh api --paginate "repos/$REPO/pulls/$pr/reviews" \ + --jq '.[] | select(.user != null) | select((.user.login | endswith("[bot]")) | not) | .submitted_at'); then + return 1 + fi + if ! review_comments=$(gh api --paginate "repos/$REPO/pulls/$pr/comments" \ + --jq '.[] | select(.user != null) | select((.user.login | endswith("[bot]")) | not) | .created_at'); then + return 1 + fi + + # `|| true` here only guards grep returning 1 on no matches (a pure text + # pipeline); the API reads above already succeeded, so no error is hidden. + newest_ts=$(printf '%s\n%s\n%s\n' "$issue_comments" "$reviews" "$review_comments" \ + | grep -v -e '^null$' -e '^$' | sort | tail -n 1 || true) if [ -z "$newest_ts" ]; then echo "$created_epoch" - return + return 0 fi to_epoch "$newest_ts" } +# 0 = already warned, 1 = not warned, 2 = read error (caller should skip). already_warned() { local pr="$1" hit - hit=$(gh api --paginate "repos/$REPO/issues/$pr/comments" \ - --jq ".[] | select(.body | contains(\"$WARN_MARKER\")) | .id" 2>/dev/null | head -n 1) - [ -n "$hit" ] + if ! hit=$(gh api --paginate "repos/$REPO/issues/$pr/comments" \ + --jq ".[] | select(.body != null) | select(.body | contains(\"$WARN_MARKER\")) | .id"); then + return 2 + fi + [ -n "$(printf '%s' "$hit" | head -n 1)" ] } log "repo=$REPO dry_run=$DRY_RUN warn>${WARN_DAYS}d close>${CLOSE_DAYS}d max=$STALE_MAX" -summary "## Stale PR sweep" -summary "" -summary "Repo \`$REPO\` · dry_run=\`$DRY_RUN\` · warn>\`${WARN_DAYS}d\` · close>\`${CLOSE_DAYS}d\` · max=\`$STALE_MAX\`" -summary "" -summary "| PR | author | created | inactivity(d) | decision |" -summary "|---:|---|---|---:|---|" - -# Enumerate all open PRs (drafts included). --limit caps the working set; PRs -# newer than WARN_DAYS are filtered out per-PR below. -PRS_JSON=$(gh pr list --repo "$REPO" --state open --limit 500 \ - --json number,createdAt,isDraft,author,labels) +# ---------------------------------------------------------------------- +# Phase 1 — enumerate every open PR and compute a decision for each. +# ---------------------------------------------------------------------- +# Fully paginated (no upper bound) so the sweep really considers EVERY open PR, +# drafts included. --paginate walks all pages; @tsv emits one line per PR. +if ! PR_ROWS=$(gh api --paginate "repos/$REPO/pulls?state=open&per_page=100" \ + --jq '.[] | [.number, .created_at, .draft, (.user.login // ""), ([.labels[].name] | join(","))] | @tsv'); then + echo "::error::failed to enumerate open PRs — aborting sweep" >&2 + exit 1 +fi -COUNT=$(jq 'length' <<<"$PRS_JSON") -log "open PRs fetched: $COUNT" +# Candidate records for writes, one per line: +# INACTIVE_SECS|PR|AUTHOR|CREATED_DATE|INACTIVE_DAYS|DECISION +CANDIDATES="" +FETCHED=0 +SKIPPED_READ_ERR=0 -ACTIONS=0 -processed=0 -while IFS= read -r row; do - PR=$(jq -r '.number' <<<"$row") - CREATED_AT=$(jq -r '.createdAt' <<<"$row") - AUTHOR=$(jq -r '.author.login // ""' <<<"$row") - LABELS=$(jq -r '[.labels[].name] | join(",")' <<<"$row") +while IFS=$'\t' read -r PR CREATED_AT IS_DRAFT AUTHOR LABELS; do + [ -z "${PR:-}" ] && continue + FETCHED=$(( FETCHED + 1 )) # Exemptions ------------------------------------------------------------ if [[ ",$LABELS," == *",no-stale,"* ]]; then @@ -154,7 +182,13 @@ while IFS= read -r row; do continue fi - LAST_EPOCH=$(last_non_bot_activity_epoch "$PR" "$CREATED_EPOCH") + # Fail safe: if any activity source can't be read, skip this PR rather than + # risk closing a PR that actually had recent human activity. + if ! LAST_EPOCH=$(last_non_bot_activity_epoch "$PR" "$CREATED_EPOCH"); then + log "::warning::PR #$PR: activity read failed — skipping (won't decide on partial data)" + SKIPPED_READ_ERR=$(( SKIPPED_READ_ERR + 1 )) + continue + fi INACTIVE_SECS=$(( NOW_SECS - LAST_EPOCH )) INACTIVE_DAYS=$(( INACTIVE_SECS / 86400 )) @@ -165,6 +199,12 @@ while IFS= read -r row; do if already_warned "$PR"; then DECISION="skip(already-warned)" else + rc=$? + if [ "$rc" = "2" ]; then + log "::warning::PR #$PR: comment read failed — skipping" + SKIPPED_READ_ERR=$(( SKIPPED_READ_ERR + 1 )) + continue + fi DECISION="warn" fi fi @@ -174,37 +214,63 @@ while IFS= read -r row; do continue fi - summary "| #$PR | $AUTHOR | ${CREATED_AT%%T*} | $INACTIVE_DAYS | $DECISION |" + CANDIDATES+="${INACTIVE_SECS}|${PR}|${AUTHOR}|${CREATED_AT%%T*}|${INACTIVE_DAYS}|${DECISION}"$'\n' +done <<<"$PR_ROWS" - if [ "$ACTIONS" -ge "$STALE_MAX" ]; then - log "PR #$PR: reached STALE_MAX=$STALE_MAX — skipping remaining writes" - continue - fi +log "open PRs fetched: $FETCHED (read-error skips: $SKIPPED_READ_ERR)" - case "$DECISION" in - close) - if [ "$DRY_RUN" = "true" ]; then - log "PR #$PR: [DRY_RUN] would close (inactivity=${INACTIVE_DAYS}d)" - else - gh pr comment "$PR" --repo "$REPO" --body "$(CLOSING_BODY)" >/dev/null - gh pr close "$PR" --repo "$REPO" >/dev/null - log "PR #$PR: closed (inactivity=${INACTIVE_DAYS}d)" - fi - ACTIONS=$(( ACTIONS + 1 )) - ;; - warn) - if [ "$DRY_RUN" = "true" ]; then - log "PR #$PR: [DRY_RUN] would post stale warning (inactivity=${INACTIVE_DAYS}d)" - else - gh pr comment "$PR" --repo "$REPO" --body "$(WARNING_BODY)" >/dev/null - log "PR #$PR: warned (inactivity=${INACTIVE_DAYS}d)" - fi - ACTIONS=$(( ACTIONS + 1 )) - ;; - esac - processed=$(( processed + 1 )) -done < <(jq -c '.[]' <<<"$PRS_JSON") +# ---------------------------------------------------------------------- +# Phase 2 — sort eligible PRs by inactivity (oldest activity first) and apply +# STALE_MAX only after sorting, so the stalest closures are never starved by +# newer warnings when the cap is hit. +# ---------------------------------------------------------------------- +summary "## Stale PR sweep" +summary "" +summary "Repo \`$REPO\` · dry_run=\`$DRY_RUN\` · warn>\`${WARN_DAYS}d\` · close>\`${CLOSE_DAYS}d\` · max=\`$STALE_MAX\`" +summary "" +summary "| PR | author | created | inactivity(d) | decision |" +summary "|---:|---|---|---:|---|" + +ACTIONS=0 +if [ -n "$CANDIDATES" ]; then + # -rn on the leading INACTIVE_SECS field: largest inactivity (closures) first. + SORTED=$(printf '%s' "$CANDIDATES" | grep -v '^$' | sort -t'|' -k1,1 -rn) + while IFS='|' read -r _SECS PR AUTHOR CREATED_DATE INACTIVE_DAYS DECISION; do + [ -z "${PR:-}" ] && continue + + if [ "$ACTIONS" -ge "$STALE_MAX" ]; then + log "PR #$PR: reached STALE_MAX=$STALE_MAX — skipping remaining writes" + summary "| #$PR | $AUTHOR | $CREATED_DATE | $INACTIVE_DAYS | ${DECISION} (capped) |" + continue + fi + + summary "| #$PR | $AUTHOR | $CREATED_DATE | $INACTIVE_DAYS | $DECISION |" + + case "$DECISION" in + close) + if [ "$DRY_RUN" = "true" ]; then + log "PR #$PR: [DRY_RUN] would close (inactivity=${INACTIVE_DAYS}d)" + else + gh pr comment "$PR" --repo "$REPO" --body "$(CLOSING_BODY)" >/dev/null + gh pr close "$PR" --repo "$REPO" >/dev/null + log "PR #$PR: closed (inactivity=${INACTIVE_DAYS}d)" + fi + ACTIONS=$(( ACTIONS + 1 )) + ;; + warn) + if [ "$DRY_RUN" = "true" ]; then + log "PR #$PR: [DRY_RUN] would post stale warning (inactivity=${INACTIVE_DAYS}d)" + else + gh pr comment "$PR" --repo "$REPO" --body "$(WARNING_BODY)" >/dev/null + log "PR #$PR: warned (inactivity=${INACTIVE_DAYS}d)" + fi + ACTIONS=$(( ACTIONS + 1 )) + ;; + esac + done <<<"$SORTED" +fi summary "" summary "**Actions taken (warn+close): $ACTIONS** (dry_run=\`$DRY_RUN\`)" +[ "$SKIPPED_READ_ERR" -gt 0 ] && summary "_Skipped $SKIPPED_READ_ERR PR(s) due to activity read errors._" log "done — actions=$ACTIONS" From 3a3c9b9cfb39242effcdec2162419a19365658a3 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Thu, 23 Jul 2026 11:55:59 +0200 Subject: [PATCH 3/3] Address re-review: validate numeric inputs, split concurrency, fix doc Address Copilot re-review on PR #928: - Validate STALE_MAX/WARN_DAYS/CLOSE_DAYS are integers and CLOSE_DAYS > WARN_DAYS, so a bad manual dispatch fails fast with a clear message instead of an opaque bash arithmetic error. - Give the weekly stale sweep its own concurrency group so it and the hourly triage orchestrator don't block each other when their crons overlap (Mon 04:17). - Correct the design doc: the orchestrator posts a one-time idempotency marker comment when it dispatches the malicious-code scanner (it is not comment-free). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/scripts/pr-stale-sweep.sh | 15 +++++++++++++++ .github/workflows/pr-triage-batch.yml | 5 ++++- docs/design/pr-triage-workflows.md | 5 +++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/scripts/pr-stale-sweep.sh b/.github/scripts/pr-stale-sweep.sh index 8ffbd61844..aa962ffc6e 100644 --- a/.github/scripts/pr-stale-sweep.sh +++ b/.github/scripts/pr-stale-sweep.sh @@ -48,6 +48,21 @@ STALE_MAX="${STALE_MAX:-25}" WARN_DAYS="${WARN_DAYS:-30}" CLOSE_DAYS="${CLOSE_DAYS:-37}" +# Validate numeric inputs up front. They arrive as strings from env/dispatch +# inputs but are used in arithmetic and comparisons, so a non-integer (e.g. a +# typo on a manual dispatch) would otherwise blow up with an opaque bash +# arithmetic error mid-sweep. Fail fast with a clear message instead. +for _var in STALE_MAX WARN_DAYS CLOSE_DAYS; do + if ! [[ "${!_var}" =~ ^[0-9]+$ ]]; then + echo "::error::$_var must be a non-negative integer (got '${!_var}')" >&2 + exit 2 + fi +done +if [ "$CLOSE_DAYS" -le "$WARN_DAYS" ]; then + echo "::error::CLOSE_DAYS ($CLOSE_DAYS) must be greater than WARN_DAYS ($WARN_DAYS)" >&2 + exit 2 +fi + REPO="$GITHUB_REPOSITORY" OWNER="${REPO%/*}" NAME="${REPO#*/}" diff --git a/.github/workflows/pr-triage-batch.yml b/.github/workflows/pr-triage-batch.yml index dd722959f2..75d0835490 100644 --- a/.github/workflows/pr-triage-batch.yml +++ b/.github/workflows/pr-triage-batch.yml @@ -57,7 +57,10 @@ permissions: contents: read concurrency: - group: pr-triage-batch + # Keep the hourly triage orchestrator and the weekly stale sweep in separate + # concurrency groups so that when their crons overlap (Mon 04:17) neither run + # blocks or queues the other. Runs within each group still serialize. + group: "${{ (github.event.schedule == '17 4 * * 1' || inputs.stale_sweep == 'true') && 'pr-triage-batch-stale' || 'pr-triage-batch' }}" cancel-in-progress: false run-name: "PR triage batch${{ inputs.stale_sweep == 'true' && ' — stale sweep' || '' }}${{ inputs.pr_number && format(' (PR #{0})', inputs.pr_number) || '' }}${{ inputs.dry_run == 'true' && ' [dry-run]' || '' }}" diff --git a/docs/design/pr-triage-workflows.md b/docs/design/pr-triage-workflows.md index 2051b38dff..4878fed3d5 100644 --- a/docs/design/pr-triage-workflows.md +++ b/docs/design/pr-triage-workflows.md @@ -4,8 +4,9 @@ Three GitHub Actions workflows keep open PRs moving without manual nudging: - **`pr-triage-batch.yml`** — hourly orchestrator (cron `17 * * * *`). Enumerates open non-draft PRs, computes a deterministic state for each, and dispatches the - per-PR worker (or the malicious-code scanner). No comments, no labels, no model - calls. Also hosts the deterministic weekly **stale-PR sweep** (`stale-sweep` + per-PR worker (or the malicious-code scanner). No labels, no model calls; the + only comment it posts is a one-time idempotency marker when it dispatches the + malicious-code scanner. Also hosts the deterministic weekly **stale-PR sweep** (`stale-sweep` job, cron `17 4 * * 1`) — see [Stale-PR sweep](#stale-pr-sweep). - **`pr-triage.yml`** — per-PR worker (`workflow_dispatch`). Re-validates the PR's state, reconciles a single `pr-state/*` label, and performs at most one