diff --git a/.github/scripts/pw-triage-comment.js b/.github/scripts/pw-triage-comment.js
new file mode 100644
index 0000000000..0edba012c5
--- /dev/null
+++ b/.github/scripts/pw-triage-comment.js
@@ -0,0 +1,65 @@
+#!/usr/bin/env node
+// Render the sticky PR shard-triage comment from $MATRIX + the select log (argv[2]) to stdout.
+const fs = require("fs");
+
+function readLog(path) {
+ if (!path) return "";
+ try {
+ return fs.readFileSync(path, "utf8").trim();
+ } catch {
+ return "";
+ }
+}
+
+const marker = process.env.MARKER || "";
+const smokeActive = process.env.SMOKE_ACTIVE === "true";
+const headSha = (process.env.HEAD_SHA || "").slice(0, 9);
+const attempt = process.env.ATTEMPT || "1";
+const log = readLog(process.argv[2]);
+
+let matrix;
+try {
+ matrix = JSON.parse(process.env.MATRIX || "{}");
+} catch {
+ matrix = {};
+}
+const shards = Array.isArray(matrix.include) ? matrix.include : [];
+
+// smoke_active only says selection ran; a global_paths hit still returns the full
+// matrix, so infer the real mode from the log rather than the flag alone.
+const fellBackToFull = /falling back to full matrix|full matrix:/.test(log);
+const mode = smokeActive && !fellBackToFull ? "smoke (changed-path)" : "full matrix";
+
+const specsOf = (s) => (Array.isArray(s.run_files) ? s.run_files.length : 0);
+const totalSpecs = shards.reduce((n, s) => n + specsOf(s), 0);
+const shardList = shards
+ .map((s) => `${s.testfolder} (${specsOf(s)})`)
+ .join(" 路 ");
+
+const lines = [
+ marker,
+ "### 馃幆 Playwright shard selection",
+ `Mode: **${mode}** 路 **${shards.length} shard${shards.length === 1 ? "" : "s"}** 路 **${totalSpecs} spec${totalSpecs === 1 ? "" : "s"}**`,
+ "",
+ shardList || "_no shards selected_",
+];
+
+if (log) {
+ lines.push(
+ "",
+ "Why these shards
",
+ "",
+ "```",
+ log,
+ "```",
+ "",
+ " "
+ );
+}
+
+lines.push(
+ "",
+ `Updated for \`${headSha}\` 路 attempt ${attempt} 路 updates in place on every push.`
+);
+
+process.stdout.write(lines.join("\n") + "\n");
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index 71ebd2433b..f0e140742c 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -322,6 +322,30 @@ jobs:
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
+ # One sticky comment per PR (found by the hidden marker) so the triage result updates
+ # in place on every push instead of spamming a fresh comment. Same idiom as Kinora below.
+ - name: Upsert shard-selection comment on PR
+ if: ${{ github.event_name == 'pull_request' }}
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ secrets.ORG_ADMIN_TOKEN }} # PAT: comment as a human, not github-actions[bot]
+ REPO: ${{ github.repository }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ MATRIX: ${{ steps.gen.outputs.matrix }}
+ SMOKE_ACTIVE: ${{ steps.gen.outputs.smoke_active }}
+ ATTEMPT: ${{ github.run_attempt }}
+ run: |
+ MARKER=""
+ BODY=$(MARKER="$MARKER" node .github/scripts/pw-triage-comment.js /tmp/pw-select-log.txt)
+ CID=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --paginate \
+ -q ".[] | select(.body | contains(\"$MARKER\")) | .id" | head -1)
+ if [ -n "$CID" ]; then
+ gh api -X PATCH "repos/$REPO/issues/comments/$CID" -f body="$BODY" >/dev/null && echo "Updated triage comment ($CID)."
+ else
+ gh api -X POST "repos/$REPO/issues/$PR_NUMBER/comments" -f body="$BODY" >/dev/null && echo "Posted triage comment."
+ fi
+
ui_integration_tests:
timeout-minutes: 45
name: e2e / ${{ matrix.testfolder }}