Skip to content
Latchkey

Fix flaky tests workflow (microsoft/playwright)

The Fix flaky tests workflow from microsoft/playwright, explained and optimized by Latchkey.

C

CI health: C - fair

The optimized version below adds caching, job timeouts.

Source: microsoft/playwright.github/workflows/fix-flakes.ymlLicense Apache-2.0View source

What it does

This is the Fix flaky tests workflow from the microsoft/playwright repository, a real project running GitHub Actions. It is shown here with attribution under its Apache-2.0 license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
name: "Fix flaky tests"

on:
  workflow_dispatch:
    inputs:
      hint:
        description: "Optional focus hint passed to the triage and fix agents (e.g. 'webkit flakes')"
        type: string
  schedule:
    - cron: "17 6 * * 1-5"

permissions: {}

jobs:
  triage:
    runs-on: ubuntu-latest
    if: github.repository == 'microsoft/playwright'
    outputs:
      runner: ${{ steps.validate.outputs.runner }}
      has_target: ${{ steps.validate.outputs.has_target }}
    permissions:
      actions: read
      copilot-requests: write
    concurrency:
      group: fix-flakes-triage
      cancel-in-progress: false
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Set up Node.js
        uses: actions/setup-node@v6
        with:
          node-version: lts/*

      - name: Install dependencies
        run: npm ci

      - name: Download and refresh test-results DB
        env:
          GITHUB_TOKEN: ${{ github.token }}
        run: |
          node utils/test-results-db/cli.ts download
          node utils/test-results-db/cli.ts update --lookback-days 3

      - name: Install Copilot CLI
        run: npm install -g @github/copilot

      - name: Pick an eligible OS with Copilot CLI
        shell: bash
        env:
          COPILOT_GITHUB_TOKEN: ${{ github.token }}
          HINT: ${{ inputs.hint }}
        run: |
          mkdir -p output
          cat > output/triage-prompt.md <<EOF
          You are triage for the fix-flakes workflow. Pick the ONE operating system where booting
          an expensive runner to fix a flaky or red test is most worthwhile right now, and hand
          off only that runner label. Do NOT fix anything.

          Optimise for the OS with the highest-impact actionable flakiness/reds that isn't
          already being worked on OR already fixed. A candidate is dead if a fix PR touches it
          (open, or recently merged/closed) - the DB window still holds the failing runs from
          before a fix landed, so a just-fixed test keeps showing old fails and looks bimodal.
          For a top candidate, check open and merged/closed PRs plus any linked annotation
          issue (GitHub MCP tools) for a plausible fix before betting an OS on it. All else
          equal, favour variety so Linux, Windows and macOS each get attention over time.

          The DB is already downloaded at utils/test-results-db/test-results.duckdb - query it
          via the playwright-test-results skill. \`bot_name\` encodes the OS.

          Write your answer to output/runner.txt, and nothing else there: exactly one of these
          six runner labels, or the single word \`none\` if no OS is worth a run.
            ubuntu-22.04   ubuntu-22.04-arm   ubuntu-24.04
            macos-14-xlarge   macos-15-xlarge   windows-latest
          Map a candidate's \`bot_name\` to the closest of those (e.g. \`*-macos-15-*\` ->
          macos-15-xlarge). Explain your reasoning briefly in your final message.

          ${HINT:+Focus hint from the operator: $HINT}
          EOF
          copilot \
            --allow-all-tools \
            --allow-all-paths \
            --no-ask-user \
            --enable-all-github-mcp-tools \
            --share=output/copilot-session.md \
            --model claude-opus-4.8 \
            --max-ai-credits 200 \
            -p "$(cat output/triage-prompt.md)"

      - name: Validate chosen runner
        id: validate
        shell: bash
        run: |
          runner="$(tr -d '[:space:]' < output/runner.txt 2>/dev/null || true)"
          if [ -z "$runner" ] || [ "$runner" = "none" ]; then
            echo "No eligible platform this run - skipping the fix job."
            echo "has_target=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          case "$runner" in
            ubuntu-22.04|ubuntu-22.04-arm|ubuntu-24.04|macos-14-xlarge|macos-15-xlarge|windows-latest) ;;
            *) echo "::error::Triage chose a non-allowlisted runner: '$runner'." >&2; exit 1 ;;
          esac
          echo "Triage chose runner: $runner"
          echo "runner=$runner" >> "$GITHUB_OUTPUT"
          echo "has_target=true" >> "$GITHUB_OUTPUT"

      - name: Add triage transcript to job summary
        if: always()
        shell: bash
        run: |
          {
            echo "## Fix-flakes triage transcript"
            echo ''
            cat "output/copilot-session.md" 2>/dev/null || echo "(no transcript)"
          } >> "$GITHUB_STEP_SUMMARY"

  fix:
    needs: triage
    if: needs.triage.outputs.has_target == 'true'
    runs-on: ${{ needs.triage.outputs.runner }}
    timeout-minutes: 60
    outputs:
      has_fix: ${{ steps.export.outputs.has_fix }}
    permissions:
      actions: read
      copilot-requests: write
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          ref: ${{ github.sha }}
          fetch-depth: 0

      - name: Set up Node.js
        uses: actions/setup-node@v6
        with:
          node-version: lts/*

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Install browsers
        shell: bash
        run: |
          if [ "$RUNNER_OS" = "Linux" ]; then
            npx playwright install --with-deps
          else
            npx playwright install
          fi

      - name: Download and refresh test-results DB
        env:
          GITHUB_TOKEN: ${{ github.token }}
        run: |
          node utils/test-results-db/cli.ts download
          node utils/test-results-db/cli.ts update --lookback-days 3

      - name: Install Copilot CLI
        run: npm install -g @github/copilot

      - name: Configure git identity
        shell: bash
        run: |
          git config user.name "Copilot"
          git config user.email "223556219+Copilot@users.noreply.github.com"

      - name: Record base commit
        id: base
        shell: bash
        run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

      - name: Fix a flaky/red test with Copilot CLI
        shell: bash
        env:
          COPILOT_GITHUB_TOKEN: ${{ github.token }}
          RUNNER: ${{ needs.triage.outputs.runner }}
          HINT: ${{ inputs.hint }}
          WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
        run: |
          mkdir -p output
          cat > output/fix-prompt.md <<EOF
          Read .github/workflows/fix-flakes-prompt.md and follow it.

          You are running on $RUNNER.
          Include the link to this run in your commit message: $WORKFLOW_URL.

          ${HINT:+Operator focus hint: $HINT}
          EOF
          copilot \
            --allow-all-tools \
            --allow-all-paths \
            --no-ask-user \
            --enable-all-github-mcp-tools \
            --share=output/copilot-session.md \
            --model claude-opus-4.8 \
            --max-ai-credits 1000 \
            -p "$(cat output/fix-prompt.md)"

      - name: Export the fix commit
        id: export
        shell: bash
        run: |
          base="${{ steps.base.outputs.sha }}"
          count=$(git rev-list --count "$base"..HEAD)
          if [ "$count" -eq 0 ]; then
            echo "Agent produced no commit - nothing to open."
            echo "has_fix=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          if [ "$count" -ne 1 ]; then
            echo "::error::Expected exactly one commit, got $count. Refusing to hand off." >&2
            exit 1
          fi
          mkdir -p handoff
          git format-patch -1 HEAD --stdout > handoff/fix.patch
          echo "has_fix=true" >> "$GITHUB_OUTPUT"

      - name: Upload handoff (fix patch)
        if: steps.export.outputs.has_fix == 'true'
        uses: actions/upload-artifact@v4
        with:
          name: fix-flakes-handoff-${{ needs.triage.outputs.runner }}
          path: handoff/
          if-no-files-found: error

      - name: Add session transcript to job summary
        if: always()
        shell: bash
        run: |
          {
            echo "## Fix-flakes session transcript (${{ needs.triage.outputs.runner }})"
            echo ''
            cat "output/copilot-session.md" 2>/dev/null || echo "(no transcript)"
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Upload session transcript
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: fix-flakes-session-${{ needs.triage.outputs.runner }}
          path: output/copilot-session.md
          if-no-files-found: warn

  open_pr:
    needs: [triage, fix]
    if: needs.fix.outputs.has_fix == 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - name: Checkout base commit
        uses: actions/checkout@v6
        with:
          ref: ${{ github.sha }}
          fetch-depth: 0

      - name: Download handoff
        uses: actions/download-artifact@v4
        with:
          name: fix-flakes-handoff-${{ needs.triage.outputs.runner }}
          path: handoff

      - uses: actions/create-github-app-token@v3
        id: app-token
        with:
          app-id: ${{ vars.PLAYWRIGHT_APP_ID }}
          private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }}

      - name: Apply commit and open PR
        env:
          GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
          RUNNER: ${{ needs.triage.outputs.runner }}
          REVIEW_OVERRIDE: skn0tt
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git fetch --no-tags origin main:refs/remotes/origin/main
          branch="fix-flakes/${RUNNER}-${{ github.run_id }}"
          git checkout -b "$branch"
          git am handoff/fix.patch
          git push origin "$branch"
          suggested=$(git log -1 --format='%(trailers:key=Suggested-reviewer,valueonly)' | head -n1 | tr -d '[:space:]@')
          reviewer="${REVIEW_OVERRIDE:-$suggested}"
          args=(--repo "${{ github.repository }}" --base main --head "$branch" --fill)
          if [ -n "$reviewer" ]; then
            args+=(--reviewer "$reviewer")
          else
            echo "::warning::No reviewer (empty override and no Suggested-reviewer trailer)."
          fi
          gh pr create "${args[@]}"

The same workflow, on Latchkey

Estimated ~20% faster on cache hits, plus fewer wasted runs and a safer supply chain. Added and changed lines are highlighted.

name: "Fix flaky tests" on:  workflow_dispatch:    inputs:      hint:        description: "Optional focus hint passed to the triage and fix agents (e.g. 'webkit flakes')"        type: string  schedule:    - cron: "17 6 * * 1-5" permissions: {} jobs:  triage:    timeout-minutes: 30    runs-on: latchkey-small    if: github.repository == 'microsoft/playwright'    outputs:      runner: ${{ steps.validate.outputs.runner }}      has_target: ${{ steps.validate.outputs.has_target }}    permissions:      actions: read      copilot-requests: write    concurrency:      group: fix-flakes-triage      cancel-in-progress: false    steps:      - name: Checkout        uses: actions/checkout@v6       - name: Set up Node.js        uses: actions/setup-node@v6        with:          cache: 'npm'          node-version: lts/*       - name: Install dependencies        run: npm ci       - name: Download and refresh test-results DB        env:          GITHUB_TOKEN: ${{ github.token }}        run: |          node utils/test-results-db/cli.ts download          node utils/test-results-db/cli.ts update --lookback-days 3       - name: Install Copilot CLI        run: npm install -g @github/copilot       - name: Pick an eligible OS with Copilot CLI        shell: bash        env:          COPILOT_GITHUB_TOKEN: ${{ github.token }}          HINT: ${{ inputs.hint }}        run: |          mkdir -p output          cat > output/triage-prompt.md <<EOF          You are triage for the fix-flakes workflow. Pick the ONE operating system where booting          an expensive runner to fix a flaky or red test is most worthwhile right now, and hand          off only that runner label. Do NOT fix anything.           Optimise for the OS with the highest-impact actionable flakiness/reds that isn't          already being worked on OR already fixed. A candidate is dead if a fix PR touches it          (open, or recently merged/closed) - the DB window still holds the failing runs from          before a fix landed, so a just-fixed test keeps showing old fails and looks bimodal.          For a top candidate, check open and merged/closed PRs plus any linked annotation          issue (GitHub MCP tools) for a plausible fix before betting an OS on it. All else          equal, favour variety so Linux, Windows and macOS each get attention over time.           The DB is already downloaded at utils/test-results-db/test-results.duckdb - query it          via the playwright-test-results skill. \`bot_name\` encodes the OS.           Write your answer to output/runner.txt, and nothing else there: exactly one of these          six runner labels, or the single word \`none\` if no OS is worth a run.            ubuntu-22.04   ubuntu-22.04-arm   ubuntu-24.04            macos-14-xlarge   macos-15-xlarge   windows-latest          Map a candidate's \`bot_name\` to the closest of those (e.g. \`*-macos-15-*\` ->          macos-15-xlarge). Explain your reasoning briefly in your final message.           ${HINT:+Focus hint from the operator: $HINT}          EOF          copilot \            --allow-all-tools \            --allow-all-paths \            --no-ask-user \            --enable-all-github-mcp-tools \            --share=output/copilot-session.md \            --model claude-opus-4.8 \            --max-ai-credits 200 \            -p "$(cat output/triage-prompt.md)"       - name: Validate chosen runner        id: validate        shell: bash        run: |          runner="$(tr -d '[:space:]' < output/runner.txt 2>/dev/null || true)"          if [ -z "$runner" ] || [ "$runner" = "none" ]; then            echo "No eligible platform this run - skipping the fix job."            echo "has_target=false" >> "$GITHUB_OUTPUT"            exit 0          fi          case "$runner" in            ubuntu-22.04|ubuntu-22.04-arm|ubuntu-24.04|macos-14-xlarge|macos-15-xlarge|windows-latest) ;;            *) echo "::error::Triage chose a non-allowlisted runner: '$runner'." >&2; exit 1 ;;          esac          echo "Triage chose runner: $runner"          echo "runner=$runner" >> "$GITHUB_OUTPUT"          echo "has_target=true" >> "$GITHUB_OUTPUT"       - name: Add triage transcript to job summary        if: always()        shell: bash        run: |          {            echo "## Fix-flakes triage transcript"            echo ''            cat "output/copilot-session.md" 2>/dev/null || echo "(no transcript)"          } >> "$GITHUB_STEP_SUMMARY"   fix:    needs: triage    if: needs.triage.outputs.has_target == 'true'    runs-on: latchkey-small    timeout-minutes: 60    outputs:      has_fix: ${{ steps.export.outputs.has_fix }}    permissions:      actions: read      copilot-requests: write    steps:      - name: Checkout        uses: actions/checkout@v6        with:          ref: ${{ github.sha }}          fetch-depth: 0       - name: Set up Node.js        uses: actions/setup-node@v6        with:          cache: 'npm'          node-version: lts/*       - name: Install dependencies        run: npm ci       - name: Build        run: npm run build       - name: Install browsers        shell: bash        run: |          if [ "$RUNNER_OS" = "Linux" ]; then            npx playwright install --with-deps          else            npx playwright install          fi       - name: Download and refresh test-results DB        env:          GITHUB_TOKEN: ${{ github.token }}        run: |          node utils/test-results-db/cli.ts download          node utils/test-results-db/cli.ts update --lookback-days 3       - name: Install Copilot CLI        run: npm install -g @github/copilot       - name: Configure git identity        shell: bash        run: |          git config user.name "Copilot"          git config user.email "223556219+Copilot@users.noreply.github.com"       - name: Record base commit        id: base        shell: bash        run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"       - name: Fix a flaky/red test with Copilot CLI        shell: bash        env:          COPILOT_GITHUB_TOKEN: ${{ github.token }}          RUNNER: ${{ needs.triage.outputs.runner }}          HINT: ${{ inputs.hint }}          WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}        run: |          mkdir -p output          cat > output/fix-prompt.md <<EOF          Read .github/workflows/fix-flakes-prompt.md and follow it.           You are running on $RUNNER.          Include the link to this run in your commit message: $WORKFLOW_URL.           ${HINT:+Operator focus hint: $HINT}          EOF          copilot \            --allow-all-tools \            --allow-all-paths \            --no-ask-user \            --enable-all-github-mcp-tools \            --share=output/copilot-session.md \            --model claude-opus-4.8 \            --max-ai-credits 1000 \            -p "$(cat output/fix-prompt.md)"       - name: Export the fix commit        id: export        shell: bash        run: |          base="${{ steps.base.outputs.sha }}"          count=$(git rev-list --count "$base"..HEAD)          if [ "$count" -eq 0 ]; then            echo "Agent produced no commit - nothing to open."            echo "has_fix=false" >> "$GITHUB_OUTPUT"            exit 0          fi          if [ "$count" -ne 1 ]; then            echo "::error::Expected exactly one commit, got $count. Refusing to hand off." >&2            exit 1          fi          mkdir -p handoff          git format-patch -1 HEAD --stdout > handoff/fix.patch          echo "has_fix=true" >> "$GITHUB_OUTPUT"       - name: Upload handoff (fix patch)        if: steps.export.outputs.has_fix == 'true'        uses: actions/upload-artifact@v4        with:          name: fix-flakes-handoff-${{ needs.triage.outputs.runner }}          path: handoff/          if-no-files-found: error       - name: Add session transcript to job summary        if: always()        shell: bash        run: |          {            echo "## Fix-flakes session transcript (${{ needs.triage.outputs.runner }})"            echo ''            cat "output/copilot-session.md" 2>/dev/null || echo "(no transcript)"          } >> "$GITHUB_STEP_SUMMARY"       - name: Upload session transcript        if: always()        uses: actions/upload-artifact@v4        with:          name: fix-flakes-session-${{ needs.triage.outputs.runner }}          path: output/copilot-session.md          if-no-files-found: warn   open_pr:    timeout-minutes: 30    needs: [triage, fix]    if: needs.fix.outputs.has_fix == 'true'    runs-on: latchkey-small    permissions:      contents: write      pull-requests: write    steps:      - name: Checkout base commit        uses: actions/checkout@v6        with:          ref: ${{ github.sha }}          fetch-depth: 0       - name: Download handoff        uses: actions/download-artifact@v4        with:          name: fix-flakes-handoff-${{ needs.triage.outputs.runner }}          path: handoff       - uses: actions/create-github-app-token@v3        id: app-token        with:          app-id: ${{ vars.PLAYWRIGHT_APP_ID }}          private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }}       - name: Apply commit and open PR        env:          GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}          RUNNER: ${{ needs.triage.outputs.runner }}          REVIEW_OVERRIDE: skn0tt        run: |          git config user.name "github-actions[bot]"          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"          git fetch --no-tags origin main:refs/remotes/origin/main          branch="fix-flakes/${RUNNER}-${{ github.run_id }}"          git checkout -b "$branch"          git am handoff/fix.patch          git push origin "$branch"          suggested=$(git log -1 --format='%(trailers:key=Suggested-reviewer,valueonly)' | head -n1 | tr -d '[:space:]@')          reviewer="${REVIEW_OVERRIDE:-$suggested}"          args=(--repo "${{ github.repository }}" --base main --head "$branch" --fill)          if [ -n "$reviewer" ]; then            args+=(--reviewer "$reviewer")          else            echo "::warning::No reviewer (empty override and no Suggested-reviewer trailer)."          fi          gh pr create "${args[@]}" 

What changed

  • Run on Latchkey managed runners with one line (runs-on), which apply the fixes below automatically and self-heal transient failures. This example uses latchkey-small; pick the runner size that fits the job.
  • Cache dependency installs on the setup step so they are served from cache.
  • Add a job timeout so a hung step cannot burn hours of runner time.

What Latchkey heals here

This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:

  • Dependency installs
  • End-to-end and browser tests

This workflow runs 3 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow

Frequently asked questions

What does the Fix flaky tests workflow (microsoft/playwright) workflow do?
This is the Fix flaky tests workflow from the microsoft/playwright repository, a real project running GitHub Actions. It is shown here with attribution under its Apache-2.0 license.
What CI health grade does this workflow get?
This Node.js workflow grades C. Paste your own workflow into the Latchkey grader to see its grade and the exact fixes.
How can I improve this Node.js workflow?
Apply caching, job timeouts. Latchkey applies these automatically on managed runners when you point runs-on at Latchkey.

References