From 84f2c43e89fc0baeefc1b2ad0b69cfe555de02f4 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 3 Aug 2026 12:25:07 +0200 Subject: [PATCH 1/3] devops: extract reusable PR CI triage workflow Pull the Copilot CI-failure triage into a workflow_call so other repos can reuse it. create_test_report behaviour is unchanged. --- .github/workflows/create_test_report.yml | 108 +----------------- .github/workflows/pr-ci-triage.yml | 134 +++++++++++++++++++++++ 2 files changed, 139 insertions(+), 103 deletions(-) create mode 100644 .github/workflows/pr-ci-triage.yml diff --git a/.github/workflows/create_test_report.yml b/.github/workflows/create_test_report.yml index 0bc76438dac5d..f6aed35520254 100644 --- a/.github/workflows/create_test_report.yml +++ b/.github/workflows/create_test_report.yml @@ -116,109 +116,11 @@ jobs: triage: needs: merge-reports if: ${{ needs.merge-reports.outputs.has_failures == 'true' && needs.merge-reports.outputs.pr_number && needs.merge-reports.outputs.triage_allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 20 permissions: + contents: read + actions: read + pull-requests: write copilot-requests: write - outputs: - has_draft: ${{ steps.triage.outputs.has_draft }} + uses: ./.github/workflows/pr-ci-triage.yml + with: pr_number: ${{ needs.merge-reports.outputs.pr_number }} - env: - PR_NUMBER: ${{ needs.merge-reports.outputs.pr_number }} - GH_TOKEN: ${{ github.token }} - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: "24" - - - name: Install Copilot CLI - run: npm install -g @github/copilot - - - name: Triage failures with Copilot CLI - id: triage - env: - COPILOT_GITHUB_TOKEN: ${{ github.token }} - run: | - mkdir -p output - PROMPT=$(cat <> "$GITHUB_OUTPUT" - else - echo "has_draft=false" >> "$GITHUB_OUTPUT" - fi - - - name: Add session transcript to job summary - if: ${{ always() }} - run: | - { - echo "## CI triage session transcript (PR #$PR_NUMBER)" - echo '' - cat "output/copilot-session.md" 2>/dev/null || echo "(no transcript)" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Upload output - if: ${{ always() }} - uses: actions/upload-artifact@v4 - with: - name: ci-triage-${{ needs.merge-reports.outputs.pr_number }} - path: output/triage.md - if-no-files-found: warn - - post: - needs: triage - if: needs.triage.outputs.has_draft == 'true' - runs-on: ubuntu-latest - permissions: - pull-requests: write - env: - PR_NUMBER: ${{ needs.triage.outputs.pr_number }} - GH_TOKEN: ${{ github.token }} - WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Download triage output - uses: actions/download-artifact@v4 - with: - name: ci-triage-${{ needs.triage.outputs.pr_number }} - path: output - - - name: Post triage comment - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - const { collapsePreviousComments } = require('./tests/config/postReportComment'); - const sentinel = ``; - const prNumber = +process.env.PR_NUMBER; - await collapsePreviousComments(github, context, prNumber, sentinel); - - const triage = fs.readFileSync('output/triage.md', 'utf8'); - const body = `${triage}\n\nTriaged by the Playwright bot - [agent run](${process.env.WORKFLOW_URL})\n${sentinel}\n`; - await github.rest.issues.createComment({ - ...context.repo, - issue_number: prNumber, - body, - }); diff --git a/.github/workflows/pr-ci-triage.yml b/.github/workflows/pr-ci-triage.yml new file mode 100644 index 0000000000000..07c9c7b32a180 --- /dev/null +++ b/.github/workflows/pr-ci-triage.yml @@ -0,0 +1,134 @@ +# Reusable CI triage for PR test failures. Callable from other repos +# (e.g. microsoft/playwright-browsers) via workflow_call. +name: PR CI Triage + +on: + workflow_call: + inputs: + pr_number: + description: 'PR number to triage' + required: true + type: string + +permissions: {} + +jobs: + triage: + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + actions: read + pull-requests: read + copilot-requests: write + outputs: + has_draft: ${{ steps.triage.outputs.has_draft }} + env: + PR_NUMBER: ${{ inputs.pr_number }} + TARGET_REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + repository: microsoft/playwright + ref: main + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + + - name: Install Copilot CLI + run: npm install -g @github/copilot + + - name: Triage failures with Copilot CLI + id: triage + env: + COPILOT_GITHUB_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + mkdir -p output + PROMPT=$(cat <> "$GITHUB_OUTPUT" + else + echo "has_draft=false" >> "$GITHUB_OUTPUT" + fi + + - name: Add session transcript to job summary + if: ${{ always() }} + run: | + { + echo "## CI triage session transcript (PR #$PR_NUMBER on $TARGET_REPO)" + echo '' + cat output/copilot-session.md 2>/dev/null || echo "(no transcript)" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload triage draft + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: ci-triage-${{ inputs.pr_number }} + path: output/triage.md + if-no-files-found: warn + + post: + needs: triage + if: ${{ needs.triage.outputs.has_draft == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + env: + PR_NUMBER: ${{ inputs.pr_number }} + GH_TOKEN: ${{ github.token }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + REPORT_NAME: ${{ github.event.workflow_run.name || github.workflow }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + repository: microsoft/playwright + ref: main + + - name: Download triage draft + uses: actions/download-artifact@v4 + with: + name: ci-triage-${{ inputs.pr_number }} + path: output + + - name: Post triage comment + uses: actions/github-script@v9 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const { collapsePreviousComments } = require('./tests/config/postReportComment'); + const sentinel = ``; + const prNumber = +process.env.PR_NUMBER; + await collapsePreviousComments(github, context, prNumber, sentinel); + const triage = fs.readFileSync('output/triage.md', 'utf8'); + const body = `${triage}\n\nTriaged by the Playwright bot - [agent run](${process.env.WORKFLOW_URL})\n${sentinel}\n`; + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body, + }); From 59002f3beb46de2fedd767476f909a48128520c9 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 3 Aug 2026 12:29:43 +0200 Subject: [PATCH 2/3] devops: keep pull-requests:write off the triage agent Reusable workflow only drafts the comment. Callers post it in a separate job that holds pull-requests: write. --- .github/workflows/create_test_report.yml | 41 +++++++++++++++++++- .github/workflows/pr-ci-triage.yml | 48 +++--------------------- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/.github/workflows/create_test_report.yml b/.github/workflows/create_test_report.yml index f6aed35520254..cbf9b7bb1f9b6 100644 --- a/.github/workflows/create_test_report.yml +++ b/.github/workflows/create_test_report.yml @@ -119,8 +119,47 @@ jobs: permissions: contents: read actions: read - pull-requests: write + pull-requests: read copilot-requests: write uses: ./.github/workflows/pr-ci-triage.yml with: pr_number: ${{ needs.merge-reports.outputs.pr_number }} + + post: + needs: [merge-reports, triage] + if: needs.triage.outputs.has_draft == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + env: + PR_NUMBER: ${{ needs.merge-reports.outputs.pr_number }} + GH_TOKEN: ${{ github.token }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + REPORT_NAME: ${{ github.event.workflow_run.name }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Download triage draft + uses: actions/download-artifact@v4 + with: + name: ci-triage-${{ needs.merge-reports.outputs.pr_number }} + path: output + + - name: Post triage comment + uses: actions/github-script@v9 + with: + script: | + const fs = require('fs'); + const { collapsePreviousComments } = require('./tests/config/postReportComment'); + const sentinel = ``; + const prNumber = +process.env.PR_NUMBER; + await collapsePreviousComments(github, context, prNumber, sentinel); + const triage = fs.readFileSync('output/triage.md', 'utf8'); + const body = `${triage}\n\nTriaged by the Playwright bot - [agent run](${process.env.WORKFLOW_URL})\n${sentinel}\n`; + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body, + }); diff --git a/.github/workflows/pr-ci-triage.yml b/.github/workflows/pr-ci-triage.yml index 07c9c7b32a180..dfce038e80af6 100644 --- a/.github/workflows/pr-ci-triage.yml +++ b/.github/workflows/pr-ci-triage.yml @@ -1,5 +1,6 @@ # Reusable CI triage for PR test failures. Callable from other repos # (e.g. microsoft/playwright-browsers) via workflow_call. +# Drafts only - the caller posts the comment with pull-requests: write. name: PR CI Triage on: @@ -9,6 +10,10 @@ on: description: 'PR number to triage' required: true type: string + outputs: + has_draft: + description: 'Whether output/triage.md was produced' + value: ${{ jobs.triage.outputs.has_draft }} permissions: {} @@ -89,46 +94,3 @@ jobs: name: ci-triage-${{ inputs.pr_number }} path: output/triage.md if-no-files-found: warn - - post: - needs: triage - if: ${{ needs.triage.outputs.has_draft == 'true' }} - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - env: - PR_NUMBER: ${{ inputs.pr_number }} - GH_TOKEN: ${{ github.token }} - WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - REPORT_NAME: ${{ github.event.workflow_run.name || github.workflow }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - repository: microsoft/playwright - ref: main - - - name: Download triage draft - uses: actions/download-artifact@v4 - with: - name: ci-triage-${{ inputs.pr_number }} - path: output - - - name: Post triage comment - uses: actions/github-script@v9 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - const { collapsePreviousComments } = require('./tests/config/postReportComment'); - const sentinel = ``; - const prNumber = +process.env.PR_NUMBER; - await collapsePreviousComments(github, context, prNumber, sentinel); - const triage = fs.readFileSync('output/triage.md', 'utf8'); - const body = `${triage}\n\nTriaged by the Playwright bot - [agent run](${process.env.WORKFLOW_URL})\n${sentinel}\n`; - await github.rest.issues.createComment({ - ...context.repo, - issue_number: prNumber, - body, - }); From 2d1e5132468cd09f98aad415a3e3348c2c258394 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 3 Aug 2026 17:21:13 +0200 Subject: [PATCH 3/3] devops: post triage comments inside the reusable workflow Keep pull-requests: write on a separate post job; the agent job stays read-only. Callers only grant the permission for the nested post. --- .github/workflows/create_test_report.yml | 41 +------------------- .github/workflows/pr-ci-triage.yml | 48 +++++++++++++++++++++--- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/.github/workflows/create_test_report.yml b/.github/workflows/create_test_report.yml index cbf9b7bb1f9b6..f6aed35520254 100644 --- a/.github/workflows/create_test_report.yml +++ b/.github/workflows/create_test_report.yml @@ -119,47 +119,8 @@ jobs: permissions: contents: read actions: read - pull-requests: read + pull-requests: write copilot-requests: write uses: ./.github/workflows/pr-ci-triage.yml with: pr_number: ${{ needs.merge-reports.outputs.pr_number }} - - post: - needs: [merge-reports, triage] - if: needs.triage.outputs.has_draft == 'true' - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - env: - PR_NUMBER: ${{ needs.merge-reports.outputs.pr_number }} - GH_TOKEN: ${{ github.token }} - WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - REPORT_NAME: ${{ github.event.workflow_run.name }} - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Download triage draft - uses: actions/download-artifact@v4 - with: - name: ci-triage-${{ needs.merge-reports.outputs.pr_number }} - path: output - - - name: Post triage comment - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - const { collapsePreviousComments } = require('./tests/config/postReportComment'); - const sentinel = ``; - const prNumber = +process.env.PR_NUMBER; - await collapsePreviousComments(github, context, prNumber, sentinel); - const triage = fs.readFileSync('output/triage.md', 'utf8'); - const body = `${triage}\n\nTriaged by the Playwright bot - [agent run](${process.env.WORKFLOW_URL})\n${sentinel}\n`; - await github.rest.issues.createComment({ - ...context.repo, - issue_number: prNumber, - body, - }); diff --git a/.github/workflows/pr-ci-triage.yml b/.github/workflows/pr-ci-triage.yml index dfce038e80af6..07c9c7b32a180 100644 --- a/.github/workflows/pr-ci-triage.yml +++ b/.github/workflows/pr-ci-triage.yml @@ -1,6 +1,5 @@ # Reusable CI triage for PR test failures. Callable from other repos # (e.g. microsoft/playwright-browsers) via workflow_call. -# Drafts only - the caller posts the comment with pull-requests: write. name: PR CI Triage on: @@ -10,10 +9,6 @@ on: description: 'PR number to triage' required: true type: string - outputs: - has_draft: - description: 'Whether output/triage.md was produced' - value: ${{ jobs.triage.outputs.has_draft }} permissions: {} @@ -94,3 +89,46 @@ jobs: name: ci-triage-${{ inputs.pr_number }} path: output/triage.md if-no-files-found: warn + + post: + needs: triage + if: ${{ needs.triage.outputs.has_draft == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + env: + PR_NUMBER: ${{ inputs.pr_number }} + GH_TOKEN: ${{ github.token }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + REPORT_NAME: ${{ github.event.workflow_run.name || github.workflow }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + repository: microsoft/playwright + ref: main + + - name: Download triage draft + uses: actions/download-artifact@v4 + with: + name: ci-triage-${{ inputs.pr_number }} + path: output + + - name: Post triage comment + uses: actions/github-script@v9 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const { collapsePreviousComments } = require('./tests/config/postReportComment'); + const sentinel = ``; + const prNumber = +process.env.PR_NUMBER; + await collapsePreviousComments(github, context, prNumber, sentinel); + const triage = fs.readFileSync('output/triage.md', 'utf8'); + const body = `${triage}\n\nTriaged by the Playwright bot - [agent run](${process.env.WORKFLOW_URL})\n${sentinel}\n`; + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body, + });