Skip to content
Latchkey LogoLatchkey home

Fix flaky tests workflow (microsoft/playwright)

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

Source: microsoft/playwright.github/workflows/fix-flakes.ymlLicense Apache-2.0View source
C
CI health grade
69/100
Health score
3
Jobs per trigger
3
Fixes in the optimized version

In short

  1. CI health: C (fair)
  2. The optimized version below points runs-on at Latchkey managed runners and adds caching and job timeouts.

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.

workflow (.yml), optimized
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
    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: ${{ 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:
          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: ubuntu-latest
    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

  1. Run on Latchkey managed runners with one line (runs-on), which apply the fixes below automatically and self-heal transient failures. Each Linux x64 job moves to the Latchkey size matching its vCPU count, latchkey-small for a standard 2 vCPU runner; macOS, Windows and arm64 jobs stay as written.
  2. Cache dependency installs on the setup step so they are served from cache.
  3. 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. 2 of the 3 run on a standard GitHub-hosted Linux x64 runner, which in a private repository bills $0.006 a minute against $0.0025 on latchkey-small, 58% lower; in a public repository like this one they are free. Estimate the monthly difference with your own run counts.

Actions used in this workflow

  • actions/checkout
  • actions/setup-node
  • actions/upload-artifact
  • actions/download-artifact
  • actions/create-github-app-token

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?
Add caching and job timeouts. Latchkey applies these automatically on managed runners when you point runs-on at Latchkey.

References