From a0c4dc6c0ee7bd77576cb3fef16421d3b79698b4 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 1 Oct 2026 21:45:55 +0300 Subject: [PATCH] devops(ci): run CI on the upstream branch itself so its PR can merge The PR the playwright-cli watch opened (#8) sat BLOCKED with zero checks. The upstream workflows ran CI as a reusable ci.yml call, and a called workflow attaches its check runs to the caller's commit -- main -- under "test / / node ". The PR is opened with the workflow token, which starts no pull_request CI, so its head commit never got the " / node " checks the main ruleset requires. A safe roll could never be merged automatically either: the merge step only commented. ci.yml now takes workflow_dispatch instead of workflow_call, and both upstream workflows dispatch it on the branch they pushed through .github/actions/ci-on-branch, then wait for the run. workflow_dispatch is the one event the workflow token may start, and its check runs land on the branch head under the plain job names. The result feeds the PR body, the tests-failed label and the safe merge as before; a failed CI still fails the run, so the scheduled-run notification fires. The wait polls `gh run view` rather than `gh run watch`, which also reads check annotations and would need a checks permission the workflows do not grant. --- .claude/skills/dev/roll.md | 5 +- .github/actions/ci-on-branch/action.yml | 56 +++++++++++++++++++ .github/workflows/ci.yml | 17 ++---- .github/workflows/upstream-playwright-cli.yml | 34 +++++++---- .github/workflows/upstream-roll.yml | 44 +++++++++------ CHANGELOG.md | 1 + 6 files changed, 115 insertions(+), 42 deletions(-) create mode 100644 .github/actions/ci-on-branch/action.yml diff --git a/.claude/skills/dev/roll.md b/.claude/skills/dev/roll.md index d6e42e2..13a7d17 100644 --- a/.claude/skills/dev/roll.md +++ b/.claude/skills/dev/roll.md @@ -41,10 +41,11 @@ Nothing is written when blocked: the merges with conflict markers go to `.roll/c ## Automation -- `.github/workflows/upstream-roll.yml` runs daily: detects a newer, old-enough `patchright-core`, rolls it on `roll_`, runs the full CI matrix on that branch (the reusable `ci.yml`), and opens the PR with the report. `safe` + green CI is merged automatically; `review` waits for a human (label `needs-review`); `blocked` opens an issue (label `upstream-blocked`) with the report and the conflicted merges and writes nothing; scheduled runs skip that version while the issue is open. Manual runs take a version, `dry_run` (report only) and `force`. +- `.github/workflows/upstream-roll.yml` runs daily: detects a newer, old-enough `patchright-core`, rolls it on `roll_`, runs `ci.yml` on that branch and waits for it (`.github/actions/ci-on-branch`), and opens the PR with the report. `safe` + green CI is merged automatically; `review` waits for a human (label `needs-review`); `blocked` opens an issue (label `upstream-blocked`) with the report and the conflicted merges and writes nothing; scheduled runs skip that version while the issue is open. Manual runs take a version, `dry_run` (report only) and `force`. - `.github/workflows/upstream-playwright-cli.yml` runs weekly with the same verdicts for `microsoft/playwright-cli` (`npm run watch:playwright-cli`, `--apply` to take a clean merge). - Both workflows need Issues enabled on the repository (a fork has them off by default) for the `blocked` verdict to land anywhere; without them the lookup only warns, but a blocked roll fails the job with the report in the summary and the artifact. -- The automatic merge uses the workflow token: keep the repository setting "Allow GitHub Actions to create and approve pull requests" on, and do not require reviews from branch protection for `roll_*` branches, or the merge step only leaves a comment and the PR waits. +- The automatic merge uses the workflow token: keep the repository setting "Allow GitHub Actions to create and approve pull requests" on, and do not require reviews in the `main` ruleset, or the merge step only leaves a comment and the PR waits. +- The `main` ruleset requires the CI job names (` / node `) on the PR head. A PR opened with the workflow token starts no CI, and a reusable `ci.yml` call would put its checks on the caller's commit under `test / ...`, so the workflows dispatch `ci.yml` on the branch instead: `workflow_dispatch` is the one event the workflow token may start, and its checks land on the branch head under the plain names. Change the ruleset's required checks together with the matrix in `ci.yml`, or every PR stays blocked. ## Start of a session diff --git a/.github/actions/ci-on-branch/action.yml b/.github/actions/ci-on-branch/action.yml new file mode 100644 index 0000000..315926e --- /dev/null +++ b/.github/actions/ci-on-branch/action.yml @@ -0,0 +1,56 @@ +name: CI on a pushed branch +description: > + Dispatch ci.yml on a branch this workflow pushed and wait for the run. A pull request opened + with the workflow token starts no CI, and a reusable ci.yml call attaches its check runs to the + caller's commit under `test / ...`; a workflow_dispatch run (the one event the workflow token + may start) puts them on the branch head under the names the `main` ruleset requires. + +inputs: + branch: + description: 'The branch to test; its head must be checked out' + required: true + +outputs: + result: + description: 'Conclusion of the CI run (success, failure, cancelled, ...; missing if it never started)' + value: ${{ steps.ci.outputs.result }} + url: + description: 'Link to the CI run' + value: ${{ steps.ci.outputs.url }} + +runs: + using: composite + steps: + - id: ci + shell: bash + env: + GH_TOKEN: ${{ github.token }} + BRANCH: ${{ inputs.branch }} + run: | + sha=$(git rev-parse HEAD) + gh workflow run ci.yml --ref "$BRANCH" + # The dispatch returns no run id: find the run by its commit (every push here is a new one). + id= + for _ in $(seq 1 24); do + sleep 5 + id=$(gh run list --workflow ci.yml --event workflow_dispatch --commit "$sha" --limit 1 --json databaseId --jq '.[0].databaseId // empty') + [ -n "$id" ] && break + done + if [ -z "$id" ]; then + echo "::error::The CI run dispatched on $BRANCH ($sha) did not show up within two minutes." + echo "result=missing" >> "$GITHUB_OUTPUT" + exit 0 + fi + url=$(gh run view "$id" --json url --jq .url) + echo "url=$url" >> "$GITHUB_OUTPUT" + echo "Waiting for CI on $BRANCH: $url" + # Not `gh run watch`: it also reads check annotations, which needs a permission the + # calling workflows do not grant. The job's timeout bounds the wait. + while :; do + state=$(gh run view "$id" --json status,conclusion --jq '.status + " " + .conclusion') || state= + case "$state" in completed\ *) break ;; esac + sleep 30 + done + result=${state#completed } + echo "CI on $BRANCH: $result" + echo "result=${result:-missing}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72ad563..8a7da1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,18 +5,13 @@ on: branches: [main] pull_request: branches: [main] - # Reusable: the upstream workflows run the same matrix on a roll branch before opening - # its pull request (a PR created by the workflow token does not trigger CI on its own). - workflow_call: - inputs: - ref: - description: 'git ref to test (default: the caller ref)' - required: false - type: string - default: '' + # The upstream workflows dispatch this on the branch they pushed (.github/actions/ci-on-branch): + # a PR opened with the workflow token starts no CI, and the `main` ruleset requires these job + # names on the PR head. Keep its required checks in step with the matrix below. + workflow_dispatch: concurrency: - group: ci-${{ github.ref }}-${{ inputs.ref }} + group: ci-${{ github.ref }} cancel-in-progress: true jobs: @@ -35,8 +30,6 @@ jobs: node: 22 steps: - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref || github.ref }} - uses: actions/setup-node@v7 with: node-version: ${{ matrix.node }} diff --git a/.github/workflows/upstream-playwright-cli.yml b/.github/workflows/upstream-playwright-cli.yml index b15597d..b280d7a 100644 --- a/.github/workflows/upstream-playwright-cli.yml +++ b/.github/workflows/upstream-playwright-cli.yml @@ -6,6 +6,8 @@ name: Upstream watch (microsoft/playwright-cli) # safe only files this fork does not use changed -> bookkeeping PR, merged when CI is green # review a mapped file merged cleanly, or a rewritten file changed upstream -> PR waits # blocked merge conflict -> issue with the report; nothing written +# CI runs on the pushed branch itself (.github/actions/ci-on-branch), so its checks sit on the +# PR head where the `main` ruleset looks for them. on: schedule: @@ -18,6 +20,7 @@ on: default: false permissions: + actions: write # dispatch ci.yml on the tracking branch contents: write pull-requests: write issues: write @@ -101,22 +104,15 @@ jobs: echo "branch=$branch" >> "$GITHUB_OUTPUT" echo "pushed=true" >> "$GITHUB_OUTPUT" - test: + pull-request: needs: track if: needs.track.outputs.pushed == 'true' - uses: ./.github/workflows/ci.yml - with: - ref: ${{ needs.track.outputs.branch }} - - pull-request: - needs: [track, test] - if: always() && needs.track.outputs.pushed == 'true' runs-on: ubuntu-latest + timeout-minutes: 60 env: GH_TOKEN: ${{ github.token }} BRANCH: ${{ needs.track.outputs.branch }} CLASSIFICATION: ${{ needs.track.outputs.classification }} - CI_RESULT: ${{ needs.test.result }} steps: - uses: actions/checkout@v7 with: @@ -125,8 +121,16 @@ jobs: with: name: playwright-cli-report path: .roll + - name: Run CI on the tracking branch + id: ci + uses: ./.github/actions/ci-on-branch + with: + branch: ${{ needs.track.outputs.branch }} - name: Create or update the pull request id: pr + env: + CI_RESULT: ${{ steps.ci.outputs.result }} + CI_URL: ${{ steps.ci.outputs.url }} run: | for label in upstream upstream-playwright-cli auto-merge needs-review tests-failed; do gh label create "$label" 2>/dev/null || true; done labels="upstream,upstream-playwright-cli" @@ -135,7 +139,8 @@ jobs: review) labels="$labels,needs-review" ;; esac [ "$CI_RESULT" = "success" ] || labels="$labels,tests-failed" - { cat .roll/playwright-cli-report.md; printf '\nCI on this branch: **%s**\n' "$CI_RESULT"; } > body.md + ci="**$CI_RESULT**"; [ -n "$CI_URL" ] && ci="$ci ([run]($CI_URL))" + { cat .roll/playwright-cli-report.md; printf '\nCI on this branch: %s\n' "$ci"; } > body.md number=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number') if [ -z "$number" ]; then gh pr create --base main --head "$BRANCH" --title "chore: track microsoft/playwright-cli" --body-file body.md --label "$labels" @@ -145,7 +150,12 @@ jobs: fi echo "number=$number" >> "$GITHUB_OUTPUT" - name: Merge automatically (nothing we use changed, green CI) - if: env.CLASSIFICATION == 'safe' && env.CI_RESULT == 'success' + if: env.CLASSIFICATION == 'safe' && steps.ci.outputs.result == 'success' run: | gh pr merge "${{ steps.pr.outputs.number }}" --squash --delete-branch \ - || gh pr comment "${{ steps.pr.outputs.number }}" --body "Safe and green, but the workflow could not merge (branch protection or repository settings). Merge by hand." + || gh pr comment "${{ steps.pr.outputs.number }}" --body "Safe and green, but the workflow could not merge (the \`main\` ruleset or repository settings). Merge by hand." + - name: Fail the run when CI did not pass + if: steps.ci.outputs.result != 'success' + run: | + echo "::error::CI on $BRANCH: ${{ steps.ci.outputs.result }}. The pull request is open with the tests-failed label." + exit 1 diff --git a/.github/workflows/upstream-roll.yml b/.github/workflows/upstream-roll.yml index 5d62625..dba246d 100644 --- a/.github/workflows/upstream-roll.yml +++ b/.github/workflows/upstream-roll.yml @@ -1,8 +1,9 @@ name: Upstream roll (patchright-core) # Daily: when a newer patchright-core is on npm and at least 48 h old (its patcher breaks for -# a few days after a Playwright release), roll it on a branch, run the full CI matrix on that -# branch and open a pull request carrying the roll report (scripts/roll.ts): +# a few days after a Playwright release), roll it on a branch, run ci.yml on that branch (its +# checks land on the branch head, where the `main` ruleset looks for them) and open a pull +# request carrying the roll report (scripts/roll.ts): # safe nothing our client is forked from changed -> merged automatically when CI is green # review upstream changes merged cleanly, but the client, daemon flags or skill moved # -> the PR waits for a human (labels: upstream, needs-review) @@ -30,6 +31,7 @@ on: default: false permissions: + actions: write # dispatch ci.yml on the roll branch contents: write pull-requests: write issues: write @@ -144,23 +146,16 @@ jobs: echo "branch=$branch" >> "$GITHUB_OUTPUT" echo "pushed=true" >> "$GITHUB_OUTPUT" - test: + pull-request: needs: [detect, roll] if: needs.roll.outputs.pushed == 'true' - uses: ./.github/workflows/ci.yml - with: - ref: ${{ needs.roll.outputs.branch }} - - pull-request: - needs: [detect, roll, test] - if: always() && needs.roll.outputs.pushed == 'true' runs-on: ubuntu-latest + timeout-minutes: 60 env: GH_TOKEN: ${{ github.token }} BRANCH: ${{ needs.roll.outputs.branch }} TARGET: ${{ needs.detect.outputs.target }} CLASSIFICATION: ${{ needs.roll.outputs.classification }} - CI_RESULT: ${{ needs.test.result }} steps: - uses: actions/checkout@v7 with: @@ -169,8 +164,16 @@ jobs: with: name: roll-report path: .roll + - name: Run CI on the roll branch + id: ci + uses: ./.github/actions/ci-on-branch + with: + branch: ${{ needs.roll.outputs.branch }} - name: Create or update the pull request id: pr + env: + CI_RESULT: ${{ steps.ci.outputs.result }} + CI_URL: ${{ steps.ci.outputs.url }} run: | for label in upstream auto-merge needs-review tests-failed; do gh label create "$label" 2>/dev/null || true; done labels="upstream" @@ -179,7 +182,8 @@ jobs: review) labels="$labels,needs-review" ;; esac [ "$CI_RESULT" = "success" ] || labels="$labels,tests-failed" - { cat .roll/report.md; printf '\nCI on this branch: **%s**\n' "$CI_RESULT"; } > body.md + ci="**$CI_RESULT**"; [ -n "$CI_URL" ] && ci="$ci ([run]($CI_URL))" + { cat .roll/report.md; printf '\nCI on this branch: %s\n' "$ci"; } > body.md number=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number') if [ -z "$number" ]; then gh pr create --base main --head "$BRANCH" --title "chore: roll patchright-core to $TARGET" --body-file body.md --label "$labels" @@ -189,15 +193,23 @@ jobs: fi echo "number=$number" >> "$GITHUB_OUTPUT" - name: Merge automatically (safe roll, green CI) - if: env.CLASSIFICATION == 'safe' && env.CI_RESULT == 'success' + if: env.CLASSIFICATION == 'safe' && steps.ci.outputs.result == 'success' run: | gh pr merge "${{ steps.pr.outputs.number }}" --squash --delete-branch \ - || gh pr comment "${{ steps.pr.outputs.number }}" --body "The roll is safe and CI is green, but the workflow could not merge (branch protection or repository settings). Merge by hand." + || gh pr comment "${{ steps.pr.outputs.number }}" --body "The roll is safe and CI is green, but the workflow could not merge (the \`main\` ruleset or repository settings). Merge by hand." - name: Explain why the pull request stays open - if: env.CLASSIFICATION != 'safe' || env.CI_RESULT != 'success' + if: env.CLASSIFICATION != 'safe' || steps.ci.outputs.result != 'success' + env: + CI_RESULT: ${{ steps.ci.outputs.result }} + CI_URL: ${{ steps.ci.outputs.url }} run: | if [ "$CI_RESULT" != "success" ]; then - gh pr comment "${{ steps.pr.outputs.number }}" --body "CI result on this branch: $CI_RESULT. Not merged; see the workflow run for the failing job." + gh pr comment "${{ steps.pr.outputs.number }}" --body "CI result on this branch: $CI_RESULT. Not merged; see ${CI_URL:-the workflow run} for the failing job." else gh pr comment "${{ steps.pr.outputs.number }}" --body "Upstream changes merged cleanly but the client surface moved (see the report); a human review is required before merging." fi + - name: Fail the run when CI did not pass + if: steps.ci.outputs.result != 'success' + run: | + echo "::error::CI on $BRANCH: ${{ steps.ci.outputs.result }}. The pull request is open with the tests-failed label." + exit 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e518ef..01e057d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to this project are documented here. The format follows [Kee - Windows: a staged Chrome update no longer puts a version in the user agent that the browser is not serving. Chrome unpacks the next build into a second `\` directory beside the running one and keeps serving the old build until it restarts, so the newest directory could disagree with the client hints (user agent `Chrome/153`, `uaFullVersion` `152.0.7977.82`) — the exact contradiction a detector looks for. With more than one directory the executable's own version resource decides. - Upstream roll: a repository with issues disabled (a fork has them off by default) no longer fails the `detect` job. The blocked-issue lookup warns and continues, and a blocked roll that cannot open an issue says so with the report in the job summary and the artifact. - Upstream workflows: the report artifact is uploaded again, so the pull request gets opened. `upload-artifact@v4` skips hidden files by default and the report lives in `.roll/`, so nothing was uploaded and the `pull-request` job died on the download; the playwright-cli watch has failed this way on every upstream change since playwright-cli 0.1.20. A missing report now fails the upload step itself. The manual online selftest had the same problem with `.patchright-cli/`. +- Upstream workflows: the pull request they open can be merged. CI ran as a reusable call, which puts its checks on the `main` commit of the triggering run under `test / ...`, and a pull request opened with the workflow token starts no CI of its own, so the PR head had no checks and the `main` ruleset blocked it, the automatic merge of a `safe` roll included. The workflows now dispatch `ci.yml` on the branch and wait for it; the checks land on the PR head under the required names. - `npm pack` from a working tree no longer picks up the ignored `skills/patchright-cli-workspace/` evaluation runs: `files` names `skills/patchright-cli` instead of `skills`. Release tarballs, built from a clean checkout, are unchanged. ### Upstream