From 87df1e1d41b2cc48b71264420eefd410e9e565f0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 01:22:40 +0000 Subject: [PATCH] chore: publish from main --- .github/plugin/marketplace.json | 27 ++ .../workflows/contributor-check-writer.yml | 217 ++++++++++ .github/workflows/contributor-check.yml | 300 ++++++++++--- ...xternal-plugin-pr-quality-gates-writer.yml | 397 ++++++++++++++++++ .../external-plugin-pr-quality-gates.yml | 300 +++---------- .github/workflows/label-pr-intent-writer.yml | 168 ++++++++ .github/workflows/label-pr-intent.yml | 62 ++- .../workflows/pr-duplicate-check-writer.yml | 257 ++++++++++++ .github/workflows/pr-duplicate-check.lock.yml | 168 ++++---- .github/workflows/pr-duplicate-check.md | 35 +- plugins/external.json | 27 ++ 11 files changed, 1528 insertions(+), 430 deletions(-) create mode 100644 .github/workflows/contributor-check-writer.yml create mode 100644 .github/workflows/external-plugin-pr-quality-gates-writer.yml create mode 100644 .github/workflows/label-pr-intent-writer.yml create mode 100644 .github/workflows/pr-duplicate-check-writer.yml diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index f073611e..055eda25 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -937,6 +937,33 @@ "sha": "c3a1606fb83ba4b89e205b1723a22e1c497f66a2" } }, + { + "name": "mobile-canvas", + "description": "View, create, boot, and interact with local iOS simulators and Android emulators from a GitHub Copilot canvas. The plugin also exposes the same device controls to agents through MCP.", + "version": "0.1.6", + "author": { + "name": "Jonathan Dick", + "url": "https://github.com/Redth" + }, + "repository": "https://github.com/Redth/mobile-canvas-ghcp", + "homepage": "https://github.com/Redth/mobile-canvas-ghcp", + "license": "MIT", + "keywords": [ + "canvas", + "mobile", + "ios", + "android", + "simulator", + "emulator", + "device-control", + "mcp" + ], + "source": { + "source": "github", + "repo": "Redth/mobile-canvas-ghcp", + "sha": "c930769d41f07852baeeb407ded76d5ba83c06d2" + } + }, { "name": "modern-web-guidance", "description": "Modern Web Guidance is an agent skill and CLI tool designed to help AI coding agents build web applications using modern, secure, and high-performance APIs rather than outdated workarounds.\nSupported by the Google Chrome team, it injects expert-curated web platform best practices directly into an agent's context window to prevent the generation of bloated, legacy code.", diff --git a/.github/workflows/contributor-check-writer.yml b/.github/workflows/contributor-check-writer.yml new file mode 100644 index 00000000..79471436 --- /dev/null +++ b/.github/workflows/contributor-check-writer.yml @@ -0,0 +1,217 @@ +name: Contributor Reputation Check Writer + +on: + workflow_run: + workflows: ["Contributor Reputation Check"] + types: [completed] + +permissions: + actions: read + issues: write + pull-requests: read + +concurrency: + group: ccw-${{ github.event.workflow_run.head_repository.id || 'unknown-repo' }}-${{ github.event.workflow_run.head_branch || 'unknown-branch' }} + cancel-in-progress: true + +jobs: + sync-pr-state: + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Download PR result artifact + id: download-result + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: contributor-check-result + path: ${{ runner.temp }}/contributor-check-result + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Sync risk labels and comment + if: steps.download-result.outcome == 'success' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + const workflowRun = context.payload.workflow_run; + const resultPath = path.join(process.env.RUNNER_TEMP, 'contributor-check-result', 'result.json'); + const raw = fs.readFileSync(resultPath, 'utf8'); + const result = JSON.parse(raw); + const allowedRisks = new Set(['HIGH', 'MEDIUM', 'LOW', 'NONE', 'UNKNOWN']); + + function fail(message) { + throw new Error(`Invalid contributor check artifact: ${message}`); + } + + if (result.schema_version !== 'contributor-check-result/v1') fail('unexpected schema_version'); + if (result.event !== 'pull_request') fail('unexpected event'); + if (!Number.isInteger(result.pr_number) || result.pr_number < 1) fail('invalid pr_number'); + if (!/^[0-9a-f]{40}$/i.test(String(result.head_sha || ''))) fail('invalid head_sha'); + if (workflowRun.event !== 'pull_request') fail('unexpected workflow_run event'); + if (String(result.run_id || '') !== String(workflowRun.id)) fail('run_id did not match workflow_run'); + if (result.head_sha !== workflowRun.head_sha) fail('head_sha did not match workflow_run'); + for (const key of ['profile_risk', 'credential_risk', 'overall_risk']) { + if (!allowedRisks.has(result[key])) fail(`invalid ${key}`); + } + + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: result.pr_number, + }); + + if (pr.state !== 'open') { + core.info(`Skipping contributor result for non-open PR #${result.pr_number}.`); + return; + } + const expectedBaseRepository = `${context.repo.owner}/${context.repo.repo}`.toLowerCase(); + const runHeadRepository = String(workflowRun.head_repository?.full_name || ''); + const runHeadRepositoryParts = runHeadRepository.split('/'); + const runHeadRef = String(workflowRun.head_branch || ''); + if (String(pr.base?.repo?.full_name || '').toLowerCase() !== expectedBaseRepository) { + fail(`PR #${result.pr_number} does not target this repository`); + } + if (pr.head.sha !== workflowRun.head_sha) { + core.warning(`Skipping stale contributor result for PR #${result.pr_number}: artifact head ${result.head_sha}, current head ${pr.head.sha}`); + return; + } + if ( + runHeadRepositoryParts.length !== 2 || + !runHeadRepositoryParts[0] || + !runHeadRepositoryParts[1] || + !runHeadRef || + String(pr.head?.repo?.full_name || '').toLowerCase() !== runHeadRepository.toLowerCase() || + String(pr.head?.ref || '') !== runHeadRef + ) { + fail(`PR #${result.pr_number} head did not match workflow_run`); + } + + const workflowRunPullRequests = Array.isArray(workflowRun.pull_requests) ? workflowRun.pull_requests : []; + if (workflowRunPullRequests.length > 0) { + if (!workflowRunPullRequests.some((pullRequest) => pullRequest.number === result.pr_number)) { + fail(`PR #${result.pr_number} was not present in workflow_run.pull_requests`); + } + } else { + const candidatePullRequests = await github.paginate(github.rest.pulls.list, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${runHeadRepositoryParts[0]}:${runHeadRef}`, + per_page: 100, + }); + const trustedMatches = candidatePullRequests.filter((candidate) => + candidate.head?.sha === workflowRun.head_sha && + String(candidate.head?.ref || '') === runHeadRef && + String(candidate.head?.repo?.full_name || '').toLowerCase() === runHeadRepository.toLowerCase() && + String(candidate.base?.repo?.full_name || '').toLowerCase() === expectedBaseRepository + ); + if (trustedMatches.length !== 1 || trustedMatches[0].number !== result.pr_number) { + fail(`PR #${result.pr_number} could not be uniquely associated with workflow_run`); + } + } + + const issueNumber = pr.number; + const risk = result.overall_risk; + const marker = ''; + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + per_page: 100, + }); + const matchingComments = comments.filter((comment) => + comment.user?.login === 'github-actions[bot]' && String(comment.body || '').includes(marker) + ); + + if (risk !== 'MEDIUM' && risk !== 'HIGH') { + for (const comment of matchingComments) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id, + }).catch((error) => core.warning(`Could not delete comment ${comment.id}: ${error.message}`)); + } + } else { + const icon = risk === 'HIGH' ? '๐Ÿ”ด' : '๐ŸŸก'; + const runUrl = context.payload.workflow_run.html_url; + const body = [ + marker, + `${icon} **Contributor Reputation Check: ${risk} risk**`, + '', + '| Check | Risk |', + '|-------|------|', + `| Profile | ${result.profile_risk} |`, + `| Credential audit | ${result.credential_risk} |`, + '', + 'Maintainers: please review this contributor before merging.', + `See the [workflow run](${runUrl}) for full details.`, + '*Automated check powered by [AGT](https://github.com/microsoft/agent-governance-toolkit).*', + ].join('\n'); + + const [canonical, ...duplicates] = matchingComments; + if (canonical) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: canonical.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body, + }); + } + + for (const duplicate of duplicates) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: duplicate.id, + }).catch(() => {}); + } + } + + for (const label of ['needs-review:MEDIUM', 'needs-review:HIGH']) { + if (label !== `needs-review:${risk}`) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + name: label, + }).catch(() => {}); + } + } + + if (risk === 'MEDIUM' || risk === 'HIGH') { + const label = `needs-review:${risk}`; + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label, + }).catch(async () => { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label, + description: `Contributor reputation check flagged ${risk} risk`, + color: 'FFA500', + }); + }); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: [label], + }); + } + + - name: Note missing artifact + if: steps.download-result.outcome != 'success' + run: echo "No contributor-check-result artifact was available; nothing to synchronize." diff --git a/.github/workflows/contributor-check.yml b/.github/workflows/contributor-check.yml index 8823f90b..c1028a7e 100644 --- a/.github/workflows/contributor-check.yml +++ b/.github/workflows/contributor-check.yml @@ -1,29 +1,26 @@ name: Contributor Reputation Check on: - pull_request_target: + pull_request: types: [opened, synchronize, reopened, edited, ready_for_review] issues: types: [opened, reopened, edited] permissions: contents: read - issues: write - pull-requests: write jobs: - check: + issue-check: runs-on: ubuntu-latest if: >- + github.event_name == 'issues' && github.actor != 'dependabot[bot]' && github.actor != 'github-actions[bot]' && github.actor != 'copilot-swe-agent[bot]' + permissions: + contents: read + issues: write steps: - - name: Checkout code - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 0 - - name: Setup Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: @@ -33,58 +30,48 @@ jobs: env: AGT_REF: v4.1.0 run: | - mkdir -p /tmp/agt + mkdir -p "$RUNNER_TEMP/agt" curl -fsSL "https://raw.githubusercontent.com/microsoft/agent-governance-toolkit/${AGT_REF}/scripts/contributor_check.py" \ - -o /tmp/agt/contributor_check.py + -o "$RUNNER_TEMP/agt/contributor_check.py" curl -fsSL "https://raw.githubusercontent.com/microsoft/agent-governance-toolkit/${AGT_REF}/scripts/credential_audit.py" \ - -o /tmp/agt/credential_audit.py - - - name: Determine author - id: author - run: | - if [ "${{ github.event_name }}" = "pull_request_target" ]; then - echo "username=${{ github.event.pull_request.user.login }}" >> "$GITHUB_OUTPUT" - echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" - echo "type=pr" >> "$GITHUB_OUTPUT" - else - echo "username=${{ github.event.issue.user.login }}" >> "$GITHUB_OUTPUT" - echo "number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" - echo "type=issue" >> "$GITHUB_OUTPUT" - fi + -o "$RUNNER_TEMP/agt/credential_audit.py" - name: Run profile check env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} + USERNAME: ${{ github.event.issue.user.login }} run: | + mkdir -p "$RUNNER_TEMP/contributor-check" set +e - python3 /tmp/agt/contributor_check.py \ - --username "${{ steps.author.outputs.username }}" \ + python3 "$RUNNER_TEMP/agt/contributor_check.py" \ + --username "$USERNAME" \ --repo "${{ github.repository }}" \ - --json > /tmp/profile.json 2>/tmp/profile.log + --json > "$RUNNER_TEMP/contributor-check/profile.json" 2>"$RUNNER_TEMP/contributor-check/profile.log" status=$? set -e - if [ "$status" -ne 0 ] && [ ! -s /tmp/profile.json ]; then + if [ "$status" -ne 0 ] && [ ! -s "$RUNNER_TEMP/contributor-check/profile.json" ]; then echo "::warning::Profile check failed" - if [ -s /tmp/profile.log ]; then - sed -n '1,120p' /tmp/profile.log + if [ -s "$RUNNER_TEMP/contributor-check/profile.log" ]; then + sed -n '1,120p' "$RUNNER_TEMP/contributor-check/profile.log" fi fi - name: Run credential audit env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} + USERNAME: ${{ github.event.issue.user.login }} run: | set +e - python3 /tmp/agt/credential_audit.py \ - --username "${{ steps.author.outputs.username }}" \ + python3 "$RUNNER_TEMP/agt/credential_audit.py" \ + --username "$USERNAME" \ --repo "${{ github.repository }}" \ - --json > /tmp/cred.json 2>/tmp/cred.log + --json > "$RUNNER_TEMP/contributor-check/cred.json" 2>"$RUNNER_TEMP/contributor-check/cred.log" status=$? set -e - if [ "$status" -ne 0 ] && [ ! -s /tmp/cred.json ]; then + if [ "$status" -ne 0 ] && [ ! -s "$RUNNER_TEMP/contributor-check/cred.json" ]; then echo "::warning::Credential audit failed" - if [ -s /tmp/cred.log ]; then - sed -n '1,120p' /tmp/cred.log + if [ -s "$RUNNER_TEMP/contributor-check/cred.log" ]; then + sed -n '1,120p' "$RUNNER_TEMP/contributor-check/cred.log" fi fi @@ -115,8 +102,8 @@ jobs: fi } - dump_json "Profile check" /tmp/profile.json /tmp/profile.log - dump_json "Credential audit" /tmp/cred.json /tmp/cred.log + dump_json "Profile check" "$RUNNER_TEMP/contributor-check/profile.json" "$RUNNER_TEMP/contributor-check/profile.log" + dump_json "Credential audit" "$RUNNER_TEMP/contributor-check/cred.json" "$RUNNER_TEMP/contributor-check/cred.log" - name: Resolve check risks id: results @@ -154,8 +141,8 @@ jobs: esac } - profile_risk=$(extract_risk /tmp/profile.json UNKNOWN) - credential_risk=$(extract_risk /tmp/cred.json UNKNOWN) + profile_risk=$(extract_risk "$RUNNER_TEMP/contributor-check/profile.json" UNKNOWN) + credential_risk=$(extract_risk "$RUNNER_TEMP/contributor-check/cred.json" UNKNOWN) echo "profile=$profile_risk" >> "$GITHUB_OUTPUT" echo "credential=$credential_risk" >> "$GITHUB_OUTPUT" @@ -185,23 +172,21 @@ jobs: - name: Sync risk comment env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} + NUMBER: ${{ github.event.issue.number }} + RISK: ${{ steps.overall.outputs.risk }} + PROFILE_RISK: ${{ steps.results.outputs.profile }} + CREDENTIAL_RISK: ${{ steps.results.outputs.credential }} run: | - number="${{ steps.author.outputs.number }}" - risk="${{ steps.overall.outputs.risk }}" - profile="${{ steps.results.outputs.profile }}" - cred="${{ steps.results.outputs.credential }}" marker="" comment_ids=$( - gh api "repos/${{ github.repository }}/issues/$number/comments" --paginate \ + gh api "repos/${{ github.repository }}/issues/$NUMBER/comments" --paginate \ | jq -r --arg marker "$marker" '.[] | select((.user.login // "") == "github-actions[bot]" and ((.body // "") | contains($marker))) | .id' ) comment_id=$(printf "%s\n" "$comment_ids" | sed -n '1p') - if [ "$risk" != "MEDIUM" ] && [ "$risk" != "HIGH" ]; then + if [ "$RISK" != "MEDIUM" ] && [ "$RISK" != "HIGH" ]; then if [ -n "$comment_id" ]; then - # Keep one canonical comment thread by removing all matching comments - # when risk drops below MEDIUM. while IFS= read -r id; do [ -z "$id" ] && continue gh api --method DELETE "repos/${{ github.repository }}/issues/comments/$id" \ @@ -211,16 +196,16 @@ jobs: exit 0 fi - if [ "$risk" = "HIGH" ]; then icon="๐Ÿ”ด"; else icon="๐ŸŸก"; fi + if [ "$RISK" = "HIGH" ]; then icon="๐Ÿ”ด"; else icon="๐ŸŸก"; fi body=$(cat </dev/null 2>&1 || true done else - gh api --method POST "repos/${{ github.repository }}/issues/$number/comments" -f body="$body" + gh api --method POST "repos/${{ github.repository }}/issues/$NUMBER/comments" -f body="$body" fi - name: Sync risk label env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} + NUMBER: ${{ github.event.issue.number }} + RISK: ${{ steps.overall.outputs.risk }} run: | - number="${{ steps.author.outputs.number }}" - risk="${{ steps.overall.outputs.risk }}" - for label in needs-review:MEDIUM needs-review:HIGH; do - if [ "$label" != "needs-review:$risk" ]; then - gh api --method DELETE "repos/${{ github.repository }}/issues/$number/labels/$label" >/dev/null 2>&1 || true + if [ "$label" != "needs-review:$RISK" ]; then + gh api --method DELETE "repos/${{ github.repository }}/issues/$NUMBER/labels/$label" >/dev/null 2>&1 || true fi done - if [ "$risk" != "MEDIUM" ] && [ "$risk" != "HIGH" ]; then + if [ "$RISK" != "MEDIUM" ] && [ "$RISK" != "HIGH" ]; then exit 0 fi - gh label create "needs-review:$risk" \ - --description "Contributor reputation check flagged $risk risk" \ + gh label create "needs-review:$RISK" \ + --description "Contributor reputation check flagged $RISK risk" \ --color "FFA500" --force 2>/dev/null || true - gh api --method POST "repos/${{ github.repository }}/issues/$number/labels" \ - -f labels[]="needs-review:$risk" >/dev/null + gh api --method POST "repos/${{ github.repository }}/issues/$NUMBER/labels" \ + -f labels[]="needs-review:$RISK" >/dev/null - name: Job summary if: always() @@ -269,10 +252,187 @@ jobs: risk="${{ steps.overall.outputs.risk }}" case "$risk" in HIGH) icon="๐Ÿ”ด" ;; MEDIUM) icon="๐ŸŸก" ;; LOW) icon="โœ…" ;; *) icon="โ“" ;; esac { - echo "## $icon Contributor Check: \`${{ steps.author.outputs.username }}\`" + echo "## $icon Contributor Check: \`${{ github.event.issue.user.login }}\`" echo "| Check | Risk |" echo "|-------|------|" echo "| Profile | ${{ steps.results.outputs.profile }} |" echo "| Credential | ${{ steps.results.outputs.credential }} |" echo "| **Overall** | **$risk** |" } >> "$GITHUB_STEP_SUMMARY" + + pr-check: + runs-on: ubuntu-latest + if: >- + github.event_name == 'pull_request' && + github.actor != 'dependabot[bot]' && + github.actor != 'github-actions[bot]' && + github.actor != 'copilot-swe-agent[bot]' + permissions: + contents: read + pull-requests: read + steps: + - name: Setup Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Fetch AGT check scripts + env: + AGT_REF: v4.1.0 + run: | + mkdir -p "$RUNNER_TEMP/agt" + curl -fsSL "https://raw.githubusercontent.com/microsoft/agent-governance-toolkit/${AGT_REF}/scripts/contributor_check.py" \ + -o "$RUNNER_TEMP/agt/contributor_check.py" + curl -fsSL "https://raw.githubusercontent.com/microsoft/agent-governance-toolkit/${AGT_REF}/scripts/credential_audit.py" \ + -o "$RUNNER_TEMP/agt/credential_audit.py" + + - name: Run profile check + env: + GITHUB_TOKEN: ${{ github.token }} + USERNAME: ${{ github.event.pull_request.user.login }} + run: | + mkdir -p "$RUNNER_TEMP/contributor-check" + set +e + python3 "$RUNNER_TEMP/agt/contributor_check.py" \ + --username "$USERNAME" \ + --repo "${{ github.repository }}" \ + --json > "$RUNNER_TEMP/contributor-check/profile.json" 2>"$RUNNER_TEMP/contributor-check/profile.log" + status=$? + set -e + if [ "$status" -ne 0 ] && [ ! -s "$RUNNER_TEMP/contributor-check/profile.json" ]; then + echo "::warning::Profile check failed" + if [ -s "$RUNNER_TEMP/contributor-check/profile.log" ]; then + sed -n '1,120p' "$RUNNER_TEMP/contributor-check/profile.log" + fi + fi + + - name: Run credential audit + env: + GITHUB_TOKEN: ${{ github.token }} + USERNAME: ${{ github.event.pull_request.user.login }} + run: | + set +e + python3 "$RUNNER_TEMP/agt/credential_audit.py" \ + --username "$USERNAME" \ + --repo "${{ github.repository }}" \ + --json > "$RUNNER_TEMP/contributor-check/cred.json" 2>"$RUNNER_TEMP/contributor-check/cred.log" + status=$? + set -e + if [ "$status" -ne 0 ] && [ ! -s "$RUNNER_TEMP/contributor-check/cred.json" ]; then + echo "::warning::Credential audit failed" + if [ -s "$RUNNER_TEMP/contributor-check/cred.log" ]; then + sed -n '1,120p' "$RUNNER_TEMP/contributor-check/cred.log" + fi + fi + + - name: Resolve check risks + id: results + run: | + extract_risk() { + file="$1" + fallback="$2" + + if [ ! -s "$file" ]; then + echo "$fallback" + return + fi + + risk=$( + jq -r ' + [ + .risk, + .overall_risk, + .overallRisk, + .result.risk, + .result.overall_risk, + .result.overallRisk + ] + | map(select(. != null and . != "")) + | .[0] // empty + ' "$file" 2>/dev/null \ + | tr "[:lower:]" "[:upper:]" \ + | tr -d "\r" + ) + + case "$risk" in + HIGH|MEDIUM|LOW|NONE|UNKNOWN) echo "$risk" ;; + "") echo "$fallback" ;; + *) echo "$fallback" ;; + esac + } + + profile_risk=$(extract_risk "$RUNNER_TEMP/contributor-check/profile.json" UNKNOWN) + credential_risk=$(extract_risk "$RUNNER_TEMP/contributor-check/cred.json" UNKNOWN) + + echo "profile=$profile_risk" >> "$GITHUB_OUTPUT" + echo "credential=$credential_risk" >> "$GITHUB_OUTPUT" + + - name: Compute overall risk + id: overall + run: | + risk_to_num() { + case "$1" in + HIGH) echo 3 ;; + MEDIUM) echo 2 ;; + LOW|NONE) echo 1 ;; + UNKNOWN|"") echo 0 ;; + *) echo 0 ;; + esac + } + p=$(risk_to_num "${{ steps.results.outputs.profile }}") + c=$(risk_to_num "${{ steps.results.outputs.credential }}") + max=$p; [ "$c" -gt "$max" ] && max=$c + case "$max" in + 3) r="HIGH" ;; + 2) r="MEDIUM" ;; + 1) r="LOW" ;; + *) r="UNKNOWN" ;; + esac + echo "risk=$r" >> "$GITHUB_OUTPUT" + + - name: Write PR result artifact + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + USERNAME: ${{ github.event.pull_request.user.login }} + PROFILE_RISK: ${{ steps.results.outputs.profile }} + CREDENTIAL_RISK: ${{ steps.results.outputs.credential }} + OVERALL_RISK: ${{ steps.overall.outputs.risk }} + run: | + mkdir -p "$RUNNER_TEMP/contributor-check-result" + jq -n \ + --arg schema_version "contributor-check-result/v1" \ + --arg event "pull_request" \ + --argjson pr_number "$PR_NUMBER" \ + --arg head_sha "$PR_HEAD_SHA" \ + --arg username "$USERNAME" \ + --arg profile_risk "$PROFILE_RISK" \ + --arg credential_risk "$CREDENTIAL_RISK" \ + --arg overall_risk "$OVERALL_RISK" \ + --arg run_id "$GITHUB_RUN_ID" \ + '{schema_version:$schema_version,event:$event,pr_number:$pr_number,head_sha:$head_sha,username:$username,profile_risk:$profile_risk,credential_risk:$credential_risk,overall_risk:$overall_risk,run_id:$run_id}' \ + > "$RUNNER_TEMP/contributor-check-result/result.json" + + - name: Upload PR result artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: contributor-check-result + path: ${{ runner.temp }}/contributor-check-result/result.json + if-no-files-found: error + retention-days: 3 + + - name: Job summary + if: always() + run: | + risk="${{ steps.overall.outputs.risk }}" + case "$risk" in HIGH) icon="๐Ÿ”ด" ;; MEDIUM) icon="๐ŸŸก" ;; LOW) icon="โœ…" ;; *) icon="โ“" ;; esac + { + echo "## $icon Contributor Check: \`${{ github.event.pull_request.user.login }}\`" + echo "| Check | Risk |" + echo "|-------|------|" + echo "| Profile | ${{ steps.results.outputs.profile }} |" + echo "| Credential | ${{ steps.results.outputs.credential }} |" + echo "| **Overall** | **$risk** |" + echo "" + echo "PR label/comment synchronization is handled by the workflow_run writer after PR state is re-fetched." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/external-plugin-pr-quality-gates-writer.yml b/.github/workflows/external-plugin-pr-quality-gates-writer.yml new file mode 100644 index 00000000..d4d20b88 --- /dev/null +++ b/.github/workflows/external-plugin-pr-quality-gates-writer.yml @@ -0,0 +1,397 @@ +name: External Plugin PR Quality Gates Writer + +on: + workflow_run: + workflows: ["External Plugin PR Quality Gates"] + types: [completed] + +permissions: + actions: read + contents: read + issues: write + pull-requests: read + +concurrency: + group: epqw-${{ github.event.workflow_run.head_repository.id || 'unknown-repo' }}-${{ github.event.workflow_run.head_branch || 'unknown-branch' }} + cancel-in-progress: true + +jobs: + sync-pr-state: + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Checkout main branch + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: main + persist-credentials: false + submodules: false + + - name: Download quality result artifact + id: download-result + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: external-plugin-pr-quality-result + path: ${{ runner.temp }}/external-plugin-pr-quality-result + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Sync labels and PR status comment + if: steps.download-result.outcome == 'success' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + const fs = require('fs'); + const path = require('path'); + const { pathToFileURL } = require('url'); + + const workflowRun = context.payload.workflow_run; + const artifactPath = path.join(process.env.RUNNER_TEMP, 'external-plugin-pr-quality-result', 'result.json'); + const payload = JSON.parse(fs.readFileSync(artifactPath, 'utf8')); + const allowedJobResults = new Set(['success', 'failure', 'cancelled', 'skipped']); + + function fail(message) { + throw new Error(`Invalid external plugin quality artifact: ${message}`); + } + + if (payload.schema_version !== 'external-plugin-pr-quality-result/v1') fail('unexpected schema_version'); + if (payload.event !== 'pull_request') fail('unexpected event'); + if (!Number.isInteger(payload.pr_number) || payload.pr_number < 1) fail('invalid pr_number'); + if (!/^[0-9a-f]{40}$/i.test(String(payload.head_sha || ''))) fail('invalid head_sha'); + if (!/^[0-9a-f]{40}$/i.test(String(payload.base_sha || ''))) fail('invalid base_sha'); + if (payload.base_ref !== 'main') fail('unexpected base_ref'); + if (workflowRun.event !== 'pull_request') fail('unexpected workflow_run event'); + if (String(payload.run_id || '') !== String(workflowRun.id)) fail('run_id did not match workflow_run'); + if (payload.head_sha !== workflowRun.head_sha) fail('head_sha did not match workflow_run'); + if (!allowedJobResults.has(payload.detect_job_result)) fail('invalid detect_job_result'); + if (!allowedJobResults.has(payload.quality_job_result)) fail('invalid quality_job_result'); + if (!Number.isInteger(payload.changed_count) || payload.changed_count < 0 || payload.changed_count > 1000) fail('invalid changed_count'); + if (typeof payload.should_run !== 'boolean') fail('invalid should_run'); + if (typeof payload.quality_result_json !== 'string' || payload.quality_result_json.length > 250000) fail('invalid quality_result_json'); + + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: payload.pr_number, + }); + + if (pr.state !== 'open') { + core.info(`Skipping non-open PR #${payload.pr_number}.`); + return; + } + const expectedBaseRepository = `${context.repo.owner}/${context.repo.repo}`.toLowerCase(); + const runHeadRepository = String(workflowRun.head_repository?.full_name || ''); + const runHeadRepositoryParts = runHeadRepository.split('/'); + const runHeadRef = String(workflowRun.head_branch || ''); + if (String(pr.base?.repo?.full_name || '').toLowerCase() !== expectedBaseRepository) { + fail(`PR #${payload.pr_number} does not target this repository`); + } + if (pr.head.sha !== workflowRun.head_sha) { + core.warning(`Skipping stale external plugin result for PR #${payload.pr_number}: artifact head ${payload.head_sha}, current head ${pr.head.sha}`); + return; + } + if ( + runHeadRepositoryParts.length !== 2 || + !runHeadRepositoryParts[0] || + !runHeadRepositoryParts[1] || + !runHeadRef || + String(pr.head?.repo?.full_name || '').toLowerCase() !== runHeadRepository.toLowerCase() || + String(pr.head?.ref || '') !== runHeadRef + ) { + fail(`PR #${payload.pr_number} head did not match workflow_run`); + } + + const workflowRunPullRequests = Array.isArray(workflowRun.pull_requests) ? workflowRun.pull_requests : []; + if (workflowRunPullRequests.length > 0) { + if (!workflowRunPullRequests.some((pullRequest) => pullRequest.number === payload.pr_number)) { + fail(`PR #${payload.pr_number} was not present in workflow_run.pull_requests`); + } + } else { + const candidatePullRequests = await github.paginate(github.rest.pulls.list, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${runHeadRepositoryParts[0]}:${runHeadRef}`, + per_page: 100, + }); + const trustedMatches = candidatePullRequests.filter((candidate) => + candidate.head?.sha === workflowRun.head_sha && + String(candidate.head?.ref || '') === runHeadRef && + String(candidate.head?.repo?.full_name || '').toLowerCase() === runHeadRepository.toLowerCase() && + String(candidate.base?.repo?.full_name || '').toLowerCase() === expectedBaseRepository + ); + if (trustedMatches.length !== 1 || trustedMatches[0].number !== payload.pr_number) { + fail(`PR #${payload.pr_number} could not be uniquely associated with workflow_run`); + } + } + if (pr.base.ref !== 'main' || pr.base.sha !== payload.base_sha) { + core.warning(`Skipping external plugin result for PR #${payload.pr_number}: base branch/ref changed since the read-only run.`); + return; + } + + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: payload.pr_number, + per_page: 100, + }); + if (!files.some((file) => file.filename === 'plugins/external.json')) { + core.warning(`Skipping external plugin result for PR #${payload.pr_number}: plugins/external.json is no longer in the PR file list.`); + return; + } + + const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href); + const marker = ''; + const detectJobResult = payload.detect_job_result; + const shouldRun = payload.should_run; + const changedCount = payload.changed_count; + const qualityJobResult = payload.quality_job_result; + + let qualityResult = { + overall_status: 'not_run', + spec_compliance_status: 'not_run', + failure_class: 'none', + checked_plugins: [], + summary: 'No changed external plugin entries were detected in this PR.', + }; + + if (detectJobResult === 'failure' || detectJobResult === 'cancelled') { + qualityResult = { + overall_status: 'infra_error', + spec_compliance_status: 'not_run', + failure_class: 'infra', + checked_plugins: [], + version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', + summary: 'External plugin PR change detection failed unexpectedly. Re-run this workflow.', + }; + } else if (shouldRun) { + if (qualityJobResult === 'failure' || qualityJobResult === 'cancelled') { + qualityResult = { + overall_status: 'infra_error', + spec_compliance_status: 'not_run', + failure_class: 'infra', + checked_plugins: [], + version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', + summary: 'External plugin PR quality checks failed unexpectedly. Re-run this workflow.', + }; + } else if (payload.quality_result_json) { + qualityResult = JSON.parse(payload.quality_result_json); + if (!qualityResult || typeof qualityResult !== 'object' || Array.isArray(qualityResult)) { + fail('quality_result_json did not parse to an object'); + } + } else { + qualityResult = { + overall_status: 'infra_error', + spec_compliance_status: 'not_run', + failure_class: 'infra', + checked_plugins: [], + version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', + summary: 'External plugin PR quality checks did not return a result payload.', + }; + } + } + + const stateLabel = qualityResult.failure_class === 'submitter_fixes' + ? 'requires-submitter-fixes' + : qualityResult.overall_status === 'pass' || !shouldRun + ? 'ready-for-review' + : 'awaiting-review'; + + const desiredLabels = new Set(['external-plugin', stateLabel]); + await intakeState.syncExternalPluginIntakeLabels({ + github, + owner: context.repo.owner, + repo: context.repo.repo, + issueNumber: payload.pr_number, + desiredLabels, + }); + + const checkedPlugins = Array.isArray(qualityResult.checked_plugins) ? qualityResult.checked_plugins.slice(0, 50) : []; + const hasSpecWarnings = checkedPlugins.some((entry) => String(entry?.quality?.spec_compliance_status || '') === 'warning'); + const header = qualityResult.failure_class === 'submitter_fixes' + ? '## ๐Ÿ›‘ External plugin PR checks failed (submitter fixes required)' + : qualityResult.overall_status === 'infra_error' + ? '## ๐Ÿ›‘ External plugin PR checks failed (maintainer follow-up)' + : hasSpecWarnings + ? '## โš ๏ธ External plugin PR checks passed with spec warnings' + : qualityResult.overall_status === 'pass' || !shouldRun + ? '## โœ… External plugin PR checks passed' + : '## โš ๏ธ External plugin PR checks need maintainer follow-up'; + const formatStatus = (rawStatus, gateName) => { + const status = String(rawStatus || 'not_run'); + if (status === 'pass') { + return 'โœ… pass'; + } + if (status === 'warning' || (gateName === 'spec compliance' && status === 'fail')) { + return 'โš ๏ธ warning'; + } + if (status === 'fail' || status === 'infra_error') { + return '๐Ÿ›‘ fail'; + } + return 'โšช not_run'; + }; + const MAX_GATE_OUTPUT_CHARS = 2000; + const escapeHtml = (value) => + String(value || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + const escapeMarkdownTableCell = (value) => + String(value ?? '') + .replace(/\r\n?|\n/g, '\n') + .split('\n') + .map((line) => + Array.from(line, (character) => + /^[A-Za-z0-9 .-]$/.test(character) + ? character + : `&#${character.codePointAt(0)};` + ).join('') + ) + .join('
'); + const normalizeGitHubUrl = (value) => { + const raw = String(value || '').trim(); + if (!raw) { + return ''; + } + + try { + const parsed = new URL(raw); + if (parsed.protocol !== 'https:' || parsed.hostname !== 'github.com') { + return ''; + } + return parsed.toString(); + } catch { + return ''; + } + }; + const TRUNCATED_OUTPUT_MARKER = '\n...output truncated...'; + const truncateGateOutput = (rawOutput) => { + const normalized = escapeHtml(String(rawOutput || '').trim()); + if (!normalized) { + return '_No output captured._'; + } + if (normalized.length <= MAX_GATE_OUTPUT_CHARS) { + return normalized; + } + return `${normalized.slice(0, Math.max(0, MAX_GATE_OUTPUT_CHARS - TRUNCATED_OUTPUT_MARKER.length))}${TRUNCATED_OUTPUT_MARKER}`; + }; + const formatGateOutput = (pluginName, gateName, gateStatus, rawOutput) => { + const summaryPluginName = escapeHtml(String(pluginName || 'unknown')); + const summaryGateName = escapeHtml(String(gateName || 'gate')); + const summaryGateStatus = escapeHtml(String(gateStatus || 'not_run')); + const output = truncateGateOutput(rawOutput); + return [ + '
', + `${summaryPluginName} - ${summaryGateName} (${summaryGateStatus})`, + '', + '
',
+                output,
+                '
', + '
', + ].join('\n'); + }; + + const rows = checkedPlugins.length > 0 + ? checkedPlugins.map((entry) => { + const name = escapeMarkdownTableCell(entry?.name || 'unknown'); + const quality = entry?.quality || {}; + const sourceUrl = normalizeGitHubUrl(entry?.source_tree_url); + const locator = escapeMarkdownTableCell(entry?.source?.sha || entry?.source?.ref || 'repository'); + const sourceCell = sourceUrl ? `[${locator}](${sourceUrl})` : locator; + return `| ${name} | ${formatStatus(quality.spec_compliance_status, 'spec compliance')} | ${formatStatus(quality.vally_lint_status, 'vally lint')} | ${formatStatus(quality.smoke_status, 'install smoke test')} | ${formatStatus(quality.version_match_status, 'version match')} | ${formatStatus(quality.ref_sha_consistency_status, 'ref/sha consistency')} | ${formatStatus(quality.canvas_structure_status, 'canvas structure')} | ${formatStatus(quality.overall_status, 'overall')} | ${sourceCell} |`; + }) + : ['| _none_ | โšช not_run | โšช not_run | โšช not_run | โšช not_run | โšช not_run | โšช not_run | โšช not_run | _n/a_ |']; + const failureDetails = checkedPlugins.flatMap((entry) => { + const name = String(entry?.name || 'unknown'); + const quality = entry?.quality || {}; + const shouldShowSpec = quality.spec_compliance_status === 'warning' || String(quality.spec_compliance_output || '').trim().length > 0; + const shouldShowVally = quality.vally_lint_status === 'fail' || quality.vally_lint_status === 'infra_error' || String(quality.vally_lint_output || '').trim().length > 0; + const shouldShowSmoke = quality.smoke_status === 'fail' || quality.smoke_status === 'infra_error' || String(quality.smoke_output || '').trim().length > 0; + const shouldShowVersionMatch = quality.version_match_status === 'fail' || quality.version_match_status === 'infra_error' || String(quality.version_match_output || '').trim().length > 0; + const shouldShowRefShaConsistency = quality.ref_sha_consistency_status === 'fail' || quality.ref_sha_consistency_status === 'infra_error' || String(quality.ref_sha_consistency_output || '').trim().length > 0; + const shouldShowCanvasStructure = quality.canvas_structure_status === 'fail' || quality.canvas_structure_status === 'infra_error' || String(quality.canvas_structure_output || '').trim().length > 0; + + const details = []; + if (shouldShowSpec) { + details.push(formatGateOutput(name, 'spec compliance', formatStatus(quality.spec_compliance_status, 'spec compliance'), quality.spec_compliance_output)); + } + if (shouldShowVally) { + details.push(formatGateOutput(name, 'vally lint', formatStatus(quality.vally_lint_status, 'vally lint'), quality.vally_lint_output)); + } + if (shouldShowSmoke) { + details.push(formatGateOutput(name, 'install smoke test', formatStatus(quality.smoke_status, 'install smoke test'), quality.smoke_output)); + } + if (shouldShowVersionMatch) { + details.push(formatGateOutput(name, 'version match', quality.version_match_status, quality.version_match_output)); + } + if (shouldShowRefShaConsistency) { + details.push(formatGateOutput(name, 'ref/sha consistency', quality.ref_sha_consistency_status, quality.ref_sha_consistency_output)); + } + if (shouldShowCanvasStructure) { + details.push(formatGateOutput(name, 'canvas structure', quality.canvas_structure_status, quality.canvas_structure_output)); + } + return details; + }); + + const body = [ + marker, + header, + '', + `- **Changed entries detected:** ${changedCount}`, + `- **Workflow state label:** \`${stateLabel}\``, + '- **Status legend:** โœ… pass ยท โš ๏ธ warning ยท ๐Ÿ›‘ fail', + '', + '### Per-plugin quality summary', + '', + '| Plugin | spec compliance (non-blocking) | vally lint | install smoke test | version match | ref/sha consistency | canvas structure | overall | source tree |', + '|---|---|---|---|---|---|---|---|---|', + ...rows, + '', + ...(failureDetails.length > 0 + ? [ + '### Gate output details', + '', + ...failureDetails, + '', + ] + : []), + String(qualityResult.summary || '').trim() + ? `
${escapeHtml(String(qualityResult.summary).trim())}
` + : '_No summary provided._', + ].join('\n'); + const MAX_COMMENT_BODY_BYTES = 60000; + const truncationNotice = '\n\n_Additional gate output was truncated to fit GitHub comment limits._'; + const truncateUtf8 = (value, maxBytes) => { + const encoded = Buffer.from(value, 'utf8'); + if (encoded.length <= maxBytes) { + return value; + } + + let end = maxBytes; + while (end > 0 && (encoded[end] & 0xc0) === 0x80) { + end -= 1; + } + return encoded.subarray(0, end).toString('utf8'); + }; + const boundedBody = Buffer.byteLength(body, 'utf8') <= MAX_COMMENT_BODY_BYTES + ? body + : `${truncateUtf8(body, MAX_COMMENT_BODY_BYTES - Buffer.byteLength(truncationNotice, 'utf8'))}${truncationNotice}`; + + await intakeState.upsertExternalPluginIntakeComment({ + github, + owner: context.repo.owner, + repo: context.repo.repo, + issueNumber: payload.pr_number, + marker, + body: boundedBody, + }); + + - name: Note missing artifact + if: steps.download-result.outcome != 'success' + run: echo "No external-plugin-pr-quality-result artifact was available; nothing to synchronize." diff --git a/.github/workflows/external-plugin-pr-quality-gates.yml b/.github/workflows/external-plugin-pr-quality-gates.yml index 89c79d79..f92f4358 100644 --- a/.github/workflows/external-plugin-pr-quality-gates.yml +++ b/.github/workflows/external-plugin-pr-quality-gates.yml @@ -1,7 +1,7 @@ name: External Plugin PR Quality Gates on: - pull_request_target: + pull_request: branches: [main] paths: - "plugins/external.json" @@ -13,6 +13,7 @@ concurrency: permissions: contents: read + pull-requests: read jobs: detect-changed-plugins: @@ -28,8 +29,9 @@ jobs: with: script: | const filePath = 'plugins/external.json'; - const baseRef = context.payload.pull_request.base.sha; - const headRef = context.payload.pull_request.head.sha; + const pull = context.payload.pull_request; + const baseRef = pull.base.sha; + const headRef = pull.head.sha; function normalizePath(value) { if (!value || value === '/') { @@ -46,21 +48,33 @@ jobs: ].join('|'); } - async function readExternalJson(ref) { + async function readExternalJson({ owner, repo, ref }) { const response = await github.rest.repos.getContent({ - owner: context.repo.owner, - repo: context.repo.repo, + owner, + repo, path: filePath, ref, }); + if (Array.isArray(response.data) || response.data.type !== 'file') { + throw new Error(`${filePath} at ${owner}/${repo}@${ref} is not a file`); + } + const encoded = response.data?.content ?? ''; const decoded = Buffer.from(encoded, 'base64').toString('utf8'); return JSON.parse(decoded); } - const basePlugins = await readExternalJson(baseRef); - const headPlugins = await readExternalJson(headRef); + const basePlugins = await readExternalJson({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: baseRef, + }); + const headPlugins = await readExternalJson({ + owner: pull.head.repo.owner.login, + repo: pull.head.repo.name, + ref: headRef, + }); const baseByIdentity = new Map(basePlugins.map((plugin) => [toIdentity(plugin), plugin])); const changedPlugins = headPlugins.filter((plugin) => { @@ -110,21 +124,14 @@ jobs: echo 'EOF' } >> "$GITHUB_OUTPUT" - sync-pr-state: + publish-quality-result: runs-on: ubuntu-latest needs: [detect-changed-plugins, run-quality-gates] if: always() permissions: contents: read - issues: write - pull-requests: write steps: - - name: Checkout main branch - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - ref: main - - - name: Sync labels and PR status comment + - name: Write quality result artifact uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 env: DETECT_JOB_RESULT: ${{ needs.detect-changed-plugins.result }} @@ -132,239 +139,36 @@ jobs: CHANGED_COUNT: ${{ needs.detect-changed-plugins.outputs.changed-count }} QUALITY_RESULT_JSON: ${{ needs.run-quality-gates.outputs.quality-result }} QUALITY_JOB_RESULT: ${{ needs.run-quality-gates.result }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} with: script: | + const fs = require('fs'); const path = require('path'); - const { pathToFileURL } = require('url'); - - const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href); - const marker = ''; - - const detectJobResult = process.env.DETECT_JOB_RESULT; - const shouldRun = process.env.SHOULD_RUN === 'true'; - const changedCount = Number.parseInt(process.env.CHANGED_COUNT || '0', 10); - const qualityJobResult = process.env.QUALITY_JOB_RESULT; - - let qualityResult = { - overall_status: 'not_run', - spec_compliance_status: 'not_run', - failure_class: 'none', - checked_plugins: [], - summary: 'No changed external plugin entries were detected in this PR.', + const outDir = path.join(process.env.RUNNER_TEMP, 'external-plugin-pr-quality-result'); + fs.mkdirSync(outDir, { recursive: true }); + const payload = { + schema_version: 'external-plugin-pr-quality-result/v1', + event: 'pull_request', + pr_number: Number.parseInt(process.env.PR_NUMBER, 10), + head_sha: process.env.PR_HEAD_SHA, + base_sha: process.env.PR_BASE_SHA, + base_ref: process.env.PR_BASE_REF, + detect_job_result: process.env.DETECT_JOB_RESULT || '', + should_run: process.env.SHOULD_RUN === 'true', + changed_count: Number.parseInt(process.env.CHANGED_COUNT || '0', 10) || 0, + quality_job_result: process.env.QUALITY_JOB_RESULT || '', + quality_result_json: process.env.QUALITY_RESULT_JSON || '', + run_id: process.env.GITHUB_RUN_ID, }; + fs.writeFileSync(path.join(outDir, 'result.json'), `${JSON.stringify(payload, null, 2)}\n`); - if (detectJobResult === 'failure' || detectJobResult === 'cancelled') { - qualityResult = { - overall_status: 'infra_error', - spec_compliance_status: 'not_run', - failure_class: 'infra', - checked_plugins: [], - version_match_status: 'infra_error', - canvas_structure_status: 'infra_error', - summary: 'External plugin PR change detection failed unexpectedly. Re-run this workflow.', - }; - } else if (shouldRun) { - if (qualityJobResult === 'failure' || qualityJobResult === 'cancelled') { - qualityResult = { - overall_status: 'infra_error', - spec_compliance_status: 'not_run', - failure_class: 'infra', - checked_plugins: [], - version_match_status: 'infra_error', - canvas_structure_status: 'infra_error', - summary: 'External plugin PR quality checks failed unexpectedly. Re-run this workflow.', - }; - } else if (process.env.QUALITY_RESULT_JSON) { - qualityResult = JSON.parse(process.env.QUALITY_RESULT_JSON); - } else { - qualityResult = { - overall_status: 'infra_error', - spec_compliance_status: 'not_run', - failure_class: 'infra', - checked_plugins: [], - version_match_status: 'infra_error', - canvas_structure_status: 'infra_error', - summary: 'External plugin PR quality checks did not return a result payload.', - }; - } - } - - const stateLabel = qualityResult.failure_class === 'submitter_fixes' - ? 'requires-submitter-fixes' - : qualityResult.overall_status === 'pass' || !shouldRun - ? 'ready-for-review' - : 'awaiting-review'; - - const desiredLabels = new Set(['external-plugin', stateLabel]); - await intakeState.syncExternalPluginIntakeLabels({ - github, - owner: context.repo.owner, - repo: context.repo.repo, - issueNumber: context.issue.number, - desiredLabels, - }); - - const checkedPlugins = Array.isArray(qualityResult.checked_plugins) ? qualityResult.checked_plugins : []; - const hasSpecWarnings = checkedPlugins.some((entry) => String(entry?.quality?.spec_compliance_status || '') === 'warning'); - const header = qualityResult.failure_class === 'submitter_fixes' - ? '## ๐Ÿ›‘ External plugin PR checks failed (submitter fixes required)' - : qualityResult.overall_status === 'infra_error' - ? '## ๐Ÿ›‘ External plugin PR checks failed (maintainer follow-up)' - : hasSpecWarnings - ? '## โš ๏ธ External plugin PR checks passed with spec warnings' - : qualityResult.overall_status === 'pass' || !shouldRun - ? '## โœ… External plugin PR checks passed' - : '## โš ๏ธ External plugin PR checks need maintainer follow-up'; - const formatStatus = (rawStatus, gateName) => { - const status = String(rawStatus || 'not_run'); - if (status === 'pass') { - return 'โœ… pass'; - } - if (status === 'warning' || (gateName === 'spec compliance' && status === 'fail')) { - return 'โš ๏ธ warning'; - } - if (status === 'fail' || status === 'infra_error') { - return '๐Ÿ›‘ fail'; - } - return 'โšช not_run'; - }; - const MAX_GATE_OUTPUT_CHARS = 2000; - const escapeHtml = (value) => - String(value || '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - const escapeMarkdownTableCell = (value) => - String(value ?? '') - .replace(/\r\n?|\n/g, '\n') - .split('\n') - .map((line) => - Array.from(line, (character) => - /^[A-Za-z0-9 .-]$/.test(character) - ? character - : `&#${character.codePointAt(0)};` - ).join('') - ) - .join('
'); - const normalizeGitHubUrl = (value) => { - const raw = String(value || '').trim(); - if (!raw) { - return ''; - } - - try { - const parsed = new URL(raw); - if (parsed.protocol !== 'https:' || parsed.hostname !== 'github.com') { - return ''; - } - return parsed.toString(); - } catch { - return ''; - } - }; - const TRUNCATED_OUTPUT_MARKER = '\n...output truncated...'; - const truncateGateOutput = (rawOutput) => { - const normalized = escapeHtml(String(rawOutput || '').trim()); - if (!normalized) { - return '_No output captured._'; - } - if (normalized.length <= MAX_GATE_OUTPUT_CHARS) { - return normalized; - } - return `${normalized.slice(0, Math.max(0, MAX_GATE_OUTPUT_CHARS - TRUNCATED_OUTPUT_MARKER.length))}${TRUNCATED_OUTPUT_MARKER}`; - }; - const formatGateOutput = (pluginName, gateName, gateStatus, rawOutput) => { - const summaryPluginName = escapeHtml(String(pluginName || 'unknown')); - const summaryGateName = escapeHtml(String(gateName || 'gate')); - const summaryGateStatus = escapeHtml(String(gateStatus || 'not_run')); - const output = truncateGateOutput(rawOutput); - return [ - '
', - `${summaryPluginName} - ${summaryGateName} (${summaryGateStatus})`, - '', - '
',
-                output,
-                '
', - '
', - ].join('\n'); - }; - - const rows = checkedPlugins.length > 0 - ? checkedPlugins.map((entry) => { - const name = escapeMarkdownTableCell(entry?.name || 'unknown'); - const quality = entry?.quality || {}; - const sourceUrl = normalizeGitHubUrl(entry?.source_tree_url); - const locator = escapeMarkdownTableCell(entry?.source?.sha || entry?.source?.ref || 'repository'); - const sourceCell = sourceUrl ? `[${locator}](${sourceUrl})` : locator; - return `| ${name} | ${formatStatus(quality.spec_compliance_status, 'spec compliance')} | ${formatStatus(quality.vally_lint_status, 'vally lint')} | ${formatStatus(quality.smoke_status, 'install smoke test')} | ${formatStatus(quality.version_match_status, 'version match')} | ${formatStatus(quality.ref_sha_consistency_status, 'ref/sha consistency')} | ${formatStatus(quality.canvas_structure_status, 'canvas structure')} | ${formatStatus(quality.overall_status, 'overall')} | ${sourceCell} |`; - }) - : ['| _none_ | โšช not_run | โšช not_run | โšช not_run | โšช not_run | โšช not_run | โšช not_run | โšช not_run | _n/a_ |']; - const failureDetails = checkedPlugins.flatMap((entry) => { - const name = String(entry?.name || 'unknown'); - const quality = entry?.quality || {}; - const shouldShowSpec = quality.spec_compliance_status === 'warning' || String(quality.spec_compliance_output || '').trim().length > 0; - const shouldShowVally = quality.vally_lint_status === 'fail' || quality.vally_lint_status === 'infra_error' || String(quality.vally_lint_output || '').trim().length > 0; - const shouldShowSmoke = quality.smoke_status === 'fail' || quality.smoke_status === 'infra_error' || String(quality.smoke_output || '').trim().length > 0; - const shouldShowVersionMatch = quality.version_match_status === 'fail' || quality.version_match_status === 'infra_error' || String(quality.version_match_output || '').trim().length > 0; - const shouldShowRefShaConsistency = quality.ref_sha_consistency_status === 'fail' || quality.ref_sha_consistency_status === 'infra_error' || String(quality.ref_sha_consistency_output || '').trim().length > 0; - const shouldShowCanvasStructure = quality.canvas_structure_status === 'fail' || quality.canvas_structure_status === 'infra_error' || String(quality.canvas_structure_output || '').trim().length > 0; - - const details = []; - if (shouldShowSpec) { - details.push(formatGateOutput(name, 'spec compliance', formatStatus(quality.spec_compliance_status, 'spec compliance'), quality.spec_compliance_output)); - } - if (shouldShowVally) { - details.push(formatGateOutput(name, 'vally lint', formatStatus(quality.vally_lint_status, 'vally lint'), quality.vally_lint_output)); - } - if (shouldShowSmoke) { - details.push(formatGateOutput(name, 'install smoke test', formatStatus(quality.smoke_status, 'install smoke test'), quality.smoke_output)); - } - if (shouldShowVersionMatch) { - details.push(formatGateOutput(name, 'version match', quality.version_match_status, quality.version_match_output)); - } - if (shouldShowRefShaConsistency) { - details.push(formatGateOutput(name, 'ref/sha consistency', quality.ref_sha_consistency_status, quality.ref_sha_consistency_output)); - } - if (shouldShowCanvasStructure) { - details.push(formatGateOutput(name, 'canvas structure', quality.canvas_structure_status, quality.canvas_structure_output)); - } - return details; - }); - - const body = [ - marker, - header, - '', - `- **Changed entries detected:** ${changedCount}`, - `- **Workflow state label:** \`${stateLabel}\``, - '- **Status legend:** โœ… pass ยท โš ๏ธ warning ยท ๐Ÿ›‘ fail', - '', - '### Per-plugin quality summary', - '', - '| Plugin | spec compliance (non-blocking) | vally lint | install smoke test | version match | ref/sha consistency | canvas structure | overall | source tree |', - '|---|---|---|---|---|---|---|---|---|', - ...rows, - '', - ...(failureDetails.length > 0 - ? [ - '### Gate output details', - '', - ...failureDetails, - '', - ] - : []), - String(qualityResult.summary || '').trim() - ? `
${escapeHtml(String(qualityResult.summary).trim())}
` - : '_No summary provided._', - ].join('\n'); - - await intakeState.upsertExternalPluginIntakeComment({ - github, - owner: context.repo.owner, - repo: context.repo.repo, - issueNumber: context.issue.number, - marker, - body, - }); + - name: Upload quality result artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: external-plugin-pr-quality-result + path: ${{ runner.temp }}/external-plugin-pr-quality-result/result.json + if-no-files-found: error + retention-days: 3 diff --git a/.github/workflows/label-pr-intent-writer.yml b/.github/workflows/label-pr-intent-writer.yml new file mode 100644 index 00000000..f1cf43f7 --- /dev/null +++ b/.github/workflows/label-pr-intent-writer.yml @@ -0,0 +1,168 @@ +name: Label PR Intent Writer + +on: + workflow_run: + workflows: ["Label PR Intent"] + types: [completed] + +permissions: + actions: read + issues: write + pull-requests: read + +concurrency: + group: lpiw-${{ github.event.workflow_run.head_repository.id || 'unknown-repo' }}-${{ github.event.workflow_run.head_branch || 'unknown-branch' }} + cancel-in-progress: true + +jobs: + apply-labels: + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Download desired label artifact + id: download-result + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: label-pr-intent-result + path: ${{ runner.temp }}/label-pr-intent-result + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Apply intent labels + if: steps.download-result.outcome == 'success' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + const workflowRun = context.payload.workflow_run; + const resultPath = path.join(process.env.RUNNER_TEMP, 'label-pr-intent-result', 'result.json'); + const result = JSON.parse(fs.readFileSync(resultPath, 'utf8')); + const managedLabels = new Set([ + 'skills', + 'plugin', + 'agent', + 'instructions', + 'new-submission', + 'website-update', + 'external-plugin', + 'hooks', + 'workflow', + 'canvas-extension', + ]); + + function fail(message) { + throw new Error(`Invalid label intent artifact: ${message}`); + } + + if (result.schema_version !== 'label-pr-intent-result/v1') fail('unexpected schema_version'); + if (result.event !== 'pull_request') fail('unexpected event'); + if (!Number.isInteger(result.pr_number) || result.pr_number < 1) fail('invalid pr_number'); + if (!/^[0-9a-f]{40}$/i.test(String(result.head_sha || ''))) fail('invalid head_sha'); + if (workflowRun.event !== 'pull_request') fail('unexpected workflow_run event'); + if (String(result.run_id || '') !== String(workflowRun.id)) fail('run_id did not match workflow_run'); + if (result.head_sha !== workflowRun.head_sha) fail('head_sha did not match workflow_run'); + if (!Array.isArray(result.desired_labels) || result.desired_labels.length > managedLabels.size) fail('invalid desired_labels'); + if (!Array.isArray(result.managed_labels)) fail('invalid managed_labels'); + for (const label of result.managed_labels) { + if (!managedLabels.has(label)) fail(`unexpected managed label ${label}`); + } + for (const label of result.desired_labels) { + if (!managedLabels.has(label)) fail(`unexpected desired label ${label}`); + } + + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: result.pr_number, + }); + if (pr.state !== 'open') { + core.info(`Skipping non-open PR #${result.pr_number}.`); + return; + } + const expectedBaseRepository = `${context.repo.owner}/${context.repo.repo}`.toLowerCase(); + const runHeadRepository = String(workflowRun.head_repository?.full_name || ''); + const runHeadRepositoryParts = runHeadRepository.split('/'); + const runHeadRef = String(workflowRun.head_branch || ''); + if (String(pr.base?.repo?.full_name || '').toLowerCase() !== expectedBaseRepository) { + fail(`PR #${result.pr_number} does not target this repository`); + } + if (pr.head.sha !== workflowRun.head_sha) { + core.warning(`Skipping stale label intent result for PR #${result.pr_number}: artifact head ${result.head_sha}, current head ${pr.head.sha}`); + return; + } + if ( + runHeadRepositoryParts.length !== 2 || + !runHeadRepositoryParts[0] || + !runHeadRepositoryParts[1] || + !runHeadRef || + String(pr.head?.repo?.full_name || '').toLowerCase() !== runHeadRepository.toLowerCase() || + String(pr.head?.ref || '') !== runHeadRef + ) { + fail(`PR #${result.pr_number} head did not match workflow_run`); + } + + const workflowRunPullRequests = Array.isArray(workflowRun.pull_requests) ? workflowRun.pull_requests : []; + if (workflowRunPullRequests.length > 0) { + if (!workflowRunPullRequests.some((pullRequest) => pullRequest.number === result.pr_number)) { + fail(`PR #${result.pr_number} was not present in workflow_run.pull_requests`); + } + } else { + const candidatePullRequests = await github.paginate(github.rest.pulls.list, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${runHeadRepositoryParts[0]}:${runHeadRef}`, + per_page: 100, + }); + const trustedMatches = candidatePullRequests.filter((candidate) => + candidate.head?.sha === workflowRun.head_sha && + String(candidate.head?.ref || '') === runHeadRef && + String(candidate.head?.repo?.full_name || '').toLowerCase() === runHeadRepository.toLowerCase() && + String(candidate.base?.repo?.full_name || '').toLowerCase() === expectedBaseRepository + ); + if (trustedMatches.length !== 1 || trustedMatches[0].number !== result.pr_number) { + fail(`PR #${result.pr_number} could not be uniquely associated with workflow_run`); + } + } + + const desiredLabels = new Set(result.desired_labels); + const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: result.pr_number, + per_page: 100, + }); + + const currentManagedLabels = currentLabels + .map((label) => label.name) + .filter((name) => managedLabels.has(name)); + + const labelsToAdd = [...desiredLabels].filter((name) => !currentManagedLabels.includes(name)); + const labelsToRemove = currentManagedLabels.filter((name) => !desiredLabels.has(name)); + + if (labelsToAdd.length > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: result.pr_number, + labels: labelsToAdd, + }); + } + + for (const name of labelsToRemove) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: result.pr_number, + name, + }); + } + + core.info(`Managed labels: ${[...desiredLabels].sort().join(', ') || 'none'}`); + + - name: Note missing artifact + if: steps.download-result.outcome != 'success' + run: echo "No label-pr-intent-result artifact was available; nothing to synchronize." diff --git a/.github/workflows/label-pr-intent.yml b/.github/workflows/label-pr-intent.yml index a449cfcc..1fe88f46 100644 --- a/.github/workflows/label-pr-intent.yml +++ b/.github/workflows/label-pr-intent.yml @@ -1,24 +1,26 @@ name: Label PR Intent on: - pull_request_target: + pull_request: types: [opened, synchronize, reopened, edited, ready_for_review] permissions: - issues: write - pull-requests: write + pull-requests: read jobs: - label-pr: + compute-labels: runs-on: ubuntu-latest if: >- github.actor != 'dependabot[bot]' && github.actor != 'github-actions[bot]' steps: - - name: Apply intent labels + - name: Compute desired intent labels uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: script: | + const fs = require('fs'); + const path = require('path'); + const managedLabels = { 'skills': true, 'plugin': true, @@ -148,36 +150,24 @@ jobs: desiredLabels.add('new-submission'); } - const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - per_page: 100 - }); + const outDir = path.join(process.env.RUNNER_TEMP, 'label-pr-intent-result'); + fs.mkdirSync(outDir, { recursive: true }); + fs.writeFileSync(path.join(outDir, 'result.json'), `${JSON.stringify({ + schema_version: 'label-pr-intent-result/v1', + event: 'pull_request', + pr_number: context.payload.pull_request.number, + head_sha: context.payload.pull_request.head.sha, + managed_labels: Object.keys(managedLabels).sort(), + desired_labels: [...desiredLabels].sort(), + run_id: process.env.GITHUB_RUN_ID, + }, null, 2)}\n`); - const currentManagedLabels = currentLabels - .map((label) => label.name) - .filter((name) => Object.prototype.hasOwnProperty.call(managedLabels, name)); + core.info(`Desired managed labels: ${[...desiredLabels].sort().join(', ') || 'none'}`); - const labelsToAdd = [...desiredLabels].filter((name) => !currentManagedLabels.includes(name)); - const labelsToRemove = currentManagedLabels.filter((name) => !desiredLabels.has(name)); - - if (labelsToAdd.length > 0) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels: labelsToAdd - }); - } - - for (const name of labelsToRemove) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - name - }); - } - - core.info(`Managed labels: ${[...desiredLabels].sort().join(', ') || 'none'}`); + - name: Upload desired label artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: label-pr-intent-result + path: ${{ runner.temp }}/label-pr-intent-result/result.json + if-no-files-found: error + retention-days: 3 diff --git a/.github/workflows/pr-duplicate-check-writer.yml b/.github/workflows/pr-duplicate-check-writer.yml new file mode 100644 index 00000000..5ce9ed3e --- /dev/null +++ b/.github/workflows/pr-duplicate-check-writer.yml @@ -0,0 +1,257 @@ +name: PR Duplicate Check Writer + +on: + workflow_run: + workflows: ["PR Duplicate Check"] + types: [completed] + +permissions: + actions: read + issues: write + pull-requests: read + +concurrency: + group: pdcw-${{ github.event.workflow_run.head_repository.id || 'unknown-repo' }}-${{ github.event.workflow_run.head_branch || 'unknown-branch' }} + cancel-in-progress: true + +jobs: + process-safe-output: + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Download agent artifact + id: download-agent + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: ${{ runner.temp }}/pr-duplicate-agent + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Download PR context artifact + id: download-context + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pr-duplicate-check-context + path: ${{ runner.temp }}/pr-duplicate-context + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Validate and publish safe comment output + if: steps.download-agent.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + const workflowRun = context.payload.workflow_run; + const artifactRoot = path.join(process.env.RUNNER_TEMP, 'pr-duplicate-agent'); + const contextRoot = path.join(process.env.RUNNER_TEMP, 'pr-duplicate-context'); + + function readJsonIfExists(filePath) { + if (!fs.existsSync(filePath)) return null; + const stat = fs.statSync(filePath); + if (stat.size > 1024 * 1024) throw new Error(`${filePath} exceeds 1 MiB`); + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } + + const prContext = + readJsonIfExists(path.join(contextRoot, 'pr-context.json')) || + readJsonIfExists(path.join(artifactRoot, 'pr-context.json')); + if (!prContext || prContext.schema_version !== 'pr-duplicate-check-context/v1') { + throw new Error('Missing or invalid pr-context.json artifact.'); + } + if (!Number.isInteger(prContext.pr_number) || prContext.pr_number < 1) { + throw new Error('Invalid PR context pr_number.'); + } + if (!/^[0-9a-f]{40}$/i.test(String(prContext.head_sha || ''))) { + throw new Error('Invalid PR context head_sha.'); + } + if (workflowRun.event !== 'pull_request') { + throw new Error('Invalid workflow_run event.'); + } + if (String(prContext.run_id || '') !== String(workflowRun.id)) { + throw new Error('PR context run_id did not match workflow_run.'); + } + if (prContext.head_sha !== workflowRun.head_sha) { + throw new Error('PR context head_sha did not match workflow_run.'); + } + + const prNumber = prContext.pr_number; + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + if (pr.state !== 'open') { + core.info(`Skipping non-open PR #${prNumber}.`); + return; + } + const expectedBaseRepository = `${context.repo.owner}/${context.repo.repo}`.toLowerCase(); + const runHeadRepository = String(workflowRun.head_repository?.full_name || ''); + const runHeadRepositoryParts = runHeadRepository.split('/'); + const runHeadRef = String(workflowRun.head_branch || ''); + if (String(pr.base?.repo?.full_name || '').toLowerCase() !== expectedBaseRepository) { + throw new Error(`PR #${prNumber} base repository ${pr.base?.repo?.full_name || ''} is not this repository.`); + } + if (pr.head.sha !== workflowRun.head_sha) { + core.warning(`Skipping stale PR duplicate output for PR #${prNumber}: artifact head ${prContext.head_sha}, current head ${pr.head.sha}`); + return; + } + if ( + runHeadRepositoryParts.length !== 2 || + !runHeadRepositoryParts[0] || + !runHeadRepositoryParts[1] || + !runHeadRef || + String(pr.head?.repo?.full_name || '').toLowerCase() !== runHeadRepository.toLowerCase() || + String(pr.head?.ref || '') !== runHeadRef + ) { + throw new Error(`PR #${prNumber} head did not match workflow_run.`); + } + + const workflowRunPullRequests = Array.isArray(workflowRun.pull_requests) ? workflowRun.pull_requests : []; + if (workflowRunPullRequests.length > 0) { + if (!workflowRunPullRequests.some((pullRequest) => pullRequest.number === prNumber)) { + throw new Error(`PR context number ${prNumber} was not present in workflow_run.pull_requests.`); + } + } else { + const candidatePullRequests = await github.paginate(github.rest.pulls.list, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${runHeadRepositoryParts[0]}:${runHeadRef}`, + per_page: 100, + }); + const trustedMatches = candidatePullRequests.filter((candidate) => + candidate.head?.sha === workflowRun.head_sha && + String(candidate.head?.ref || '') === runHeadRef && + String(candidate.head?.repo?.full_name || '').toLowerCase() === runHeadRepository.toLowerCase() && + String(candidate.base?.repo?.full_name || '').toLowerCase() === expectedBaseRepository + ); + if (trustedMatches.length !== 1 || trustedMatches[0].number !== prNumber) { + throw new Error(`PR context number ${prNumber} could not be uniquely associated with workflow_run.`); + } + } + + function collectCandidate(value, targetCandidates) { + if (!value || typeof value !== 'object') return; + const tool = value.tool || value.name || value.tool_name || value.type || value.action; + const args = value.arguments || value.args || value.input || value.params || value.data || value; + if (tool === 'add_comment') { + targetCandidates.push(args); + } + } + + function collectCandidatesFromJson(value) { + const collected = []; + if (value) { + if (Array.isArray(value.items)) { + for (const item of value.items) collectCandidate(item, collected); + } else if (Array.isArray(value)) { + for (const item of value) collectCandidate(item, collected); + } else { + collectCandidate(value, collected); + } + } + return collected; + } + + const agentOutput = readJsonIfExists(path.join(artifactRoot, 'agent_output.json')); + let candidates = collectCandidatesFromJson(agentOutput); + + if (candidates.length === 0) { + const safeOutputsPath = path.join(artifactRoot, 'safeoutputs.jsonl'); + if (fs.existsSync(safeOutputsPath)) { + const stat = fs.statSync(safeOutputsPath); + if (stat.size > 1024 * 1024) throw new Error('safeoutputs.jsonl exceeds 1 MiB'); + const lines = fs.readFileSync(safeOutputsPath, 'utf8').split(/\r?\n/).filter(Boolean).slice(0, 100); + const safeOutputCandidates = []; + for (const line of lines) collectCandidate(JSON.parse(line), safeOutputCandidates); + candidates = safeOutputCandidates; + } + } + + const stripControlCharacters = (value) => + String(value).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, ''); + const neutralizeMentions = (value) => + value.replace(/@([A-Za-z0-9][A-Za-z0-9-]{0,38})/g, '@\u200B$1'); + const sanitizeCommentBody = (value, maxLength) => { + const sanitized = neutralizeMentions(stripControlCharacters(value)); + return sanitized.length > maxLength ? sanitized.slice(0, maxLength) : sanitized; + }; + + const validComments = []; + for (const candidate of candidates) { + if (!candidate || typeof candidate !== 'object') continue; + const body = candidate.body; + if (typeof body !== 'string' || body.trim() === '' || body.length > 65000) continue; + if (candidate.item_number !== undefined && Number(candidate.item_number) !== prNumber) continue; + if (candidate.repo !== undefined && String(candidate.repo) !== `${context.repo.owner}/${context.repo.repo}`) continue; + validComments.push(body); + } + + if (validComments.length === 0) { + core.info('No valid add_comment safe output was found.'); + return; + } + if (validComments.length > 1) { + throw new Error(`Expected at most one add_comment safe output, found ${validComments.length}.`); + } + + const marker = ''; + const runUrl = workflowRun.html_url; + const footer = ``; + const maxSafeOutputBodyLength = 65000 - marker.length - footer.length - 4; + const safeOutputBody = sanitizeCommentBody(validComments[0], Math.max(0, maxSafeOutputBodyLength)); + if (safeOutputBody.trim() === '') { + core.info('No non-empty safe comment body remained after sanitization.'); + return; + } + const body = [ + marker, + safeOutputBody, + '', + footer, + ].join('\n'); + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + }); + const matchingComments = comments.filter((comment) => + comment.user?.login === 'github-actions[bot]' && String(comment.body || '').includes(marker) + ); + + const [canonical, ...duplicates] = matchingComments; + if (canonical) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: canonical.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + } + + for (const duplicate of duplicates) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: duplicate.id, + }).catch(() => {}); + } + + - name: Note missing artifact + if: steps.download-agent.outcome != 'success' + run: echo "No PR Duplicate Check agent artifact was available; nothing to synchronize." diff --git a/.github/workflows/pr-duplicate-check.lock.yml b/.github/workflows/pr-duplicate-check.lock.yml index 89a804c8..516c669f 100644 --- a/.github/workflows/pr-duplicate-check.lock.yml +++ b/.github/workflows/pr-duplicate-check.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c1877b8a55ae9cad25f53d73dfdd810184817e8118fa5e28ca318ba34b059044","body_hash":"3cd4ec993ffb688af3d1649a5d4f904f15618da1399e9a2ad903925da60c4468","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","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":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}],"has_pull_request_target":true} -# This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c4bf579582c4086263bafa20678ed2875220919d340720656d7a7511f0ea0078","body_hash":"3cd4ec993ffb688af3d1649a5d4f904f15618da1399e9a2ad903925da60c4468","compiler_version":"v0.85.4","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.78"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","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":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"2709137ea6c5b0e19aa621454dc643ea8dc526b1","version":"v0.85.4"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.8","digest":"sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}],"has_pull_request":true} +# This file was automatically generated by gh-aw (v0.85.4). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -40,19 +40,20 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@c863074b673419603d146aab585e2986ef08deec # v0.84.3 +# - github/gh-aw-actions/setup@2709137ea6c5b0e19aa621454dc643ea8dc526b1 # v0.85.4 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1 -# - ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d -# - ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00 +# - ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627 +# - ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8 # - ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196 # - ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520 name: "PR Duplicate Check" on: - pull_request_target: + pull_request: + # forks: "*" # Fork filtering applied via job conditions types: - opened - synchronize @@ -69,7 +70,9 @@ run-name: "PR Duplicate Check" jobs: activation: needs: pre_activation - if: needs.pre_activation.outputs.activated == 'true' + if: > + needs.pre_activation.outputs.activated == 'true' && ((github.event_name != 'pull_request' && github.event_name != 'pull_request_review') || + github.event.pull_request.stack == null || github.event.pull_request.stack.position == github.event.pull_request.stack.size) runs-on: ubuntu-slim permissions: actions: read @@ -82,6 +85,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} 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 }} @@ -97,7 +101,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c863074b673419603d146aab585e2986ef08deec # v0.84.3 + uses: github/gh-aw-actions/setup@2709137ea6c5b0e19aa621454dc643ea8dc526b1 # v0.85.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -107,8 +111,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "PR Duplicate Check" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/pr-duplicate-check.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.77" - GH_AW_INFO_AWF_VERSION: "v0.27.43" + GH_AW_INFO_VERSION: "1.0.78" + GH_AW_INFO_AWF_VERSION: "v0.27.44" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -116,16 +120,16 @@ jobs: 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 || 'auto' }} - GH_AW_INFO_VERSION: "1.0.77" - GH_AW_INFO_AGENT_VERSION: "1.0.77" - GH_AW_INFO_CLI_VERSION: "v0.84.3" + GH_AW_INFO_VERSION: "1.0.78" + GH_AW_INFO_AGENT_VERSION: "1.0.78" + GH_AW_INFO_CLI_VERSION: "v0.85.4" GH_AW_INFO_WORKFLOW_NAME: "PR Duplicate Check" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" - GH_AW_INFO_STAGED: "false" + GH_AW_INFO_STAGED: "true" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.43" + GH_AW_INFO_AWF_VERSION: "v0.27.44" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -194,18 +198,16 @@ jobs: sparse-checkout: | .github .agents - .antigravity .claude .codex .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 .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" # poutine:ignore untrusted_checkout_exec run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file @@ -223,7 +225,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.84.3" + GH_AW_COMPILED_VERSION: "v0.85.4" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -431,7 +433,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c863074b673419603d146aab585e2986ef08deec # v0.84.3 + uses: github/gh-aw-actions/setup@2709137ea6c5b0e19aa621454dc643ea8dc526b1 # v0.85.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -440,8 +442,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "PR Duplicate Check" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/pr-duplicate-check.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.77" - GH_AW_INFO_AWF_VERSION: "v0.27.43" + GH_AW_INFO_VERSION: "1.0.78" + GH_AW_INFO_AWF_VERSION: "v0.27.44" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -466,9 +468,9 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - GH_AW_COMPILED_VERSION: v0.84.3 + GH_AW_COMPILED_VERSION: v0.85.4 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.43 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.44 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -489,15 +491,15 @@ jobs: 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.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1 ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00 ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196 ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7 ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627 ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8 ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196 ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520 - 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_1ac138f5d8a92364_EOF' - {"add_comment":{"hide_older_comments":true,"max":1},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_1ac138f5d8a92364_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_e2155cb95a85d17e_EOF' + {"add_comment":{"hide_older_comments":true,"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_e2155cb95a85d17e_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -645,7 +647,7 @@ jobs: 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 GH_AW_SINK_VISIBILITY -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.7' + 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 GH_AW_SINK_VISIBILITY -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.8' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) @@ -751,7 +753,7 @@ jobs: 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.43/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.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*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*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"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\":[\"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*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"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\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"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-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"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.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.44/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.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*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*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"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\":[\"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*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"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\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"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-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"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.44,squid=sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627,agent=sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4,api-proxy=sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7,cli-proxy=sha256:c064d15974f7c933ec7d3f7b4038f4fd203547b3154bdc821afd379144887eff\"},\"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="" @@ -769,7 +771,7 @@ jobs: 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 \ + 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 ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --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 @@ -783,8 +785,9 @@ jobs: 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_SAFE_OUTPUTS_STAGED: true GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.84.3 + GH_AW_VERSION: v0.85.4 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -912,6 +915,22 @@ jobs: if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + - env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + if: always() + name: Write PR context artifact + run: "mkdir -p /tmp/gh-aw\njq -n \\\n --arg schema_version \"pr-duplicate-check-context/v1\" \\\n --argjson pr_number \"$PR_NUMBER\" \\\n --arg head_sha \"$HEAD_SHA\" \\\n --arg base_ref \"$BASE_REF\" \\\n --arg run_id \"$GITHUB_RUN_ID\" \\\n '{schema_version:$schema_version,pr_number:$pr_number,head_sha:$head_sha,base_ref:$base_ref,run_id:$run_id}' \\\n > /tmp/gh-aw/pr-context.json\n" + - if: always() + name: Upload PR context artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + if-no-files-found: error + name: pr-duplicate-check-context + path: /tmp/gh-aw/pr-context.json + retention-days: 7 + - name: Upload agent artifacts if: always() continue-on-error: true @@ -950,8 +969,7 @@ jobs: needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - issues: write - pull-requests: write + actions: write concurrency: group: "gh-aw-conclusion-pr-duplicate-check" cancel-in-progress: false @@ -966,7 +984,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c863074b673419603d146aab585e2986ef08deec # v0.84.3 + uses: github/gh-aw-actions/setup@2709137ea6c5b0e19aa621454dc643ea8dc526b1 # v0.85.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -975,8 +993,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "PR Duplicate Check" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/pr-duplicate-check.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.77" - GH_AW_INFO_AWF_VERSION: "v0.27.43" + GH_AW_INFO_VERSION: "1.0.78" + GH_AW_INFO_AWF_VERSION: "v0.27.44" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -992,7 +1010,7 @@ jobs: 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 + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true @@ -1105,7 +1123,8 @@ jobs: 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_MISSING_TOOL_CREATE_ISSUE: "false" + GH_AW_MISSING_TOOL_TITLE_PREFIX: "[missing tool]" GH_AW_WORKFLOW_NAME: "PR Duplicate Check" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/pr-duplicate-check.md" with: @@ -1120,7 +1139,8 @@ jobs: 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_REPORT_INCOMPLETE_CREATE_ISSUE: "false" + GH_AW_REPORT_INCOMPLETE_TITLE_PREFIX: "[incomplete]" GH_AW_WORKFLOW_NAME: "PR Duplicate Check" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/pr-duplicate-check.md" with: @@ -1165,7 +1185,7 @@ jobs: 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_FAILURE_REPORT_AS_ISSUE: "false" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "20" @@ -1196,7 +1216,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c863074b673419603d146aab585e2986ef08deec # v0.84.3 + uses: github/gh-aw-actions/setup@2709137ea6c5b0e19aa621454dc643ea8dc526b1 # v0.85.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1205,8 +1225,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "PR Duplicate Check" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/pr-duplicate-check.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.77" - GH_AW_INFO_AWF_VERSION: "v0.27.43" + GH_AW_INFO_VERSION: "1.0.78" + GH_AW_INFO_AWF_VERSION: "v0.27.44" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1233,7 +1253,7 @@ jobs: 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.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1 ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7 ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627 - name: Check if detection needed id: detection_guard if: always() @@ -1295,6 +1315,8 @@ jobs: run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log + rm -f /tmp/gh-aw/threat-detection/step-summary.md + touch /tmp/gh-aw/threat-detection/step-summary.md - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: @@ -1304,9 +1326,9 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - GH_AW_COMPILED_VERSION: v0.84.3 + GH_AW_COMPILED_VERSION: v0.85.4 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.43 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.44 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1326,7 +1348,7 @@ jobs: 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.43/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,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*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*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"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\":[\"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*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"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\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"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-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"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.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.44/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,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*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*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"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\":[\"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*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"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\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"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-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"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.44,squid=sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627,agent=sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4,api-proxy=sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7,cli-proxy=sha256:c064d15974f7c933ec7d3f7b4038f4fd203547b3154bdc821afd379144887eff\"},\"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="" @@ -1346,7 +1368,7 @@ jobs: 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 --log-level info --skip-pull \ + 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 ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --log-level info --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 @@ -1360,7 +1382,7 @@ jobs: GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.84.3 + GH_AW_VERSION: v0.85.4 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1430,6 +1452,10 @@ jobs: } pre_activation: + if: > + (github.event_name != 'pull_request' && github.event_name != 'pull_request_review') || + github.event.pull_request.stack == null || + github.event.pull_request.stack.position == github.event.pull_request.stack.size runs-on: ubuntu-slim env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} @@ -1442,15 +1468,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c863074b673419603d146aab585e2986ef08deec # v0.84.3 + uses: github/gh-aw-actions/setup@2709137ea6c5b0e19aa621454dc643ea8dc526b1 # v0.85.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: GH_AW_SETUP_WORKFLOW_NAME: "PR Duplicate Check" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/pr-duplicate-check.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.77" - GH_AW_INFO_AWF_VERSION: "v0.27.43" + GH_AW_INFO_VERSION: "1.0.78" + GH_AW_INFO_AWF_VERSION: "v0.27.44" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for workflow id: check_membership @@ -1472,9 +1498,7 @@ jobs: - detection if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim - permissions: - issues: write - pull-requests: write + permissions: {} timeout-minutes: 45 env: GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} @@ -1488,6 +1512,7 @@ jobs: GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + GH_AW_SAFE_OUTPUTS_STAGED: "true" GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "pr-duplicate-check" GH_AW_WORKFLOW_NAME: "PR Duplicate Check" @@ -1499,12 +1524,15 @@ jobs: 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_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} 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@c863074b673419603d146aab585e2986ef08deec # v0.84.3 + uses: github/gh-aw-actions/setup@2709137ea6c5b0e19aa621454dc643ea8dc526b1 # v0.85.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1513,8 +1541,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "PR Duplicate Check" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/pr-duplicate-check.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.77" - GH_AW_INFO_AWF_VERSION: "v0.27.43" + GH_AW_INFO_VERSION: "1.0.78" + GH_AW_INFO_AWF_VERSION: "v0.27.44" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1548,7 +1576,8 @@ jobs: 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\":{\"hide_older_comments\":true,\"max\":1},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_STAGED: "true" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1556,14 +1585,3 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/process_safe_outputs.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 - /tmp/gh-aw/process-safe-outputs.stdout.log - /tmp/gh-aw/process-safe-outputs.stderr.log - if-no-files-found: ignore diff --git a/.github/workflows/pr-duplicate-check.md b/.github/workflows/pr-duplicate-check.md index d84ca479..3e010603 100644 --- a/.github/workflows/pr-duplicate-check.md +++ b/.github/workflows/pr-duplicate-check.md @@ -1,8 +1,9 @@ --- description: 'Checks PRs for potential duplicate agents, instructions, skills, and workflows already in the repository' on: - pull_request_target: + pull_request: types: [opened, synchronize, reopened] + forks: "*" checkout: false permissions: contents: read @@ -11,10 +12,42 @@ permissions: tools: github: toolsets: [repos, pull_requests] +post-steps: + - name: Write PR context artifact + if: always() + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + mkdir -p /tmp/gh-aw + jq -n \ + --arg schema_version "pr-duplicate-check-context/v1" \ + --argjson pr_number "$PR_NUMBER" \ + --arg head_sha "$HEAD_SHA" \ + --arg base_ref "$BASE_REF" \ + --arg run_id "$GITHUB_RUN_ID" \ + '{schema_version:$schema_version,pr_number:$pr_number,head_sha:$head_sha,base_ref:$base_ref,run_id:$run_id}' \ + > /tmp/gh-aw/pr-context.json + - name: Upload PR context artifact + if: always() + uses: actions/upload-artifact@v7.0.1 + with: + name: pr-duplicate-check-context + path: /tmp/gh-aw/pr-context.json + if-no-files-found: error + retention-days: 7 safe-outputs: + staged: true + report-failure-as-issue: false + report-failed-jobs: false add-comment: max: 1 hide-older-comments: true + missing-tool: + create-issue: false + report-incomplete: + create-issue: false noop: report-as-issue: false --- diff --git a/plugins/external.json b/plugins/external.json index 2f0264f5..d520161c 100644 --- a/plugins/external.json +++ b/plugins/external.json @@ -663,6 +663,33 @@ "sha": "c3a1606fb83ba4b89e205b1723a22e1c497f66a2" } }, + { + "name": "mobile-canvas", + "description": "View, create, boot, and interact with local iOS simulators and Android emulators from a GitHub Copilot canvas. The plugin also exposes the same device controls to agents through MCP.", + "version": "0.1.6", + "author": { + "name": "Jonathan Dick", + "url": "https://github.com/Redth" + }, + "repository": "https://github.com/Redth/mobile-canvas-ghcp", + "homepage": "https://github.com/Redth/mobile-canvas-ghcp", + "license": "MIT", + "keywords": [ + "canvas", + "mobile", + "ios", + "android", + "simulator", + "emulator", + "device-control", + "mcp" + ], + "source": { + "source": "github", + "repo": "Redth/mobile-canvas-ghcp", + "sha": "c930769d41f07852baeeb407ded76d5ba83c06d2" + } + }, { "name": "modern-web-guidance", "description": "Modern Web Guidance is an agent skill and CLI tool designed to help AI coding agents build web applications using modern, secure, and high-performance APIs rather than outdated workarounds.\nSupported by the Google Chrome team, it injects expert-curated web platform best practices directly into an agent's context window to prevent the generation of bloated, legacy code.",