diff --git a/.github/workflows/ci-host.yaml b/.github/workflows/ci-host.yaml index e38a35c0d9e..b570e8926c2 100644 --- a/.github/workflows/ci-host.yaml +++ b/.github/workflows/ci-host.yaml @@ -938,18 +938,77 @@ jobs: needs: [host-test, check-percy] runs-on: ubuntu-latest timeout-minutes: 15 + # Job-level so the reject step can test for their presence in its `if:` — + # a step's own `env:` block is not in scope for that step's condition. + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/init - name: Finalise Percy - run: npx percy build:finalize + id: finalize + run: | + set -eo pipefail + npx percy build:finalize 2>&1 | tee /tmp/percy-finalize.log + # Percy build URLs are https://percy.io////builds/. + # Allow 1-3 segments before /builds so a change to that structure + # doesn't break the parse, and tolerate no match — the reject step + # is gated on a non-empty build id anyway. + BUILD_URL=$(grep -oE 'https://percy\.io/[A-Za-z0-9_-]+(/[A-Za-z0-9_-]+){1,3}/builds/[0-9]+' /tmp/percy-finalize.log | tail -1) || true + echo "build_id=${BUILD_URL##*/}" >> "$GITHUB_OUTPUT" + echo "build_url=$BUILD_URL" >> "$GITHUB_OUTPUT" + if [ -z "$BUILD_URL" ]; then + echo "::warning::Could not parse the Percy build id from finalize output; the reject step will be skipped." + fi working-directory: packages/host env: PERCY_TOKEN: ${{ secrets.PERCY_TOKEN_HOST }} PERCY_PARALLEL_NONCE: ${{ github.run_id }}-${{ github.run_attempt }} + # A shard that dies contributes no snapshots, but finalize still seals + # the build. On `main`, where the project's auto-approve-branch-filter + # promotes builds without review, that partial set becomes the baseline + # every later branch is compared against — and the snapshots the dead + # shard would have produced resurface as diffs on unrelated PRs. + # Rejecting the build keeps it from becoming a baseline. + # + # `percy build:reject` authenticates with BrowserStack account + # credentials rather than the project token: @percy/cli-build reads + # BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY (see + # fetchCredentials in @percy/cli-build). Both are required — the CLI + # exits non-zero when either is missing — so the step runs only when + # both are present and otherwise skips itself rather than failing the + # job. + - name: Reject the Percy build when a host shard failed + if: >- + needs.host-test.result != 'success' + && steps.finalize.outputs.build_id != '' + && env.BROWSERSTACK_USERNAME != '' + && env.BROWSERSTACK_ACCESS_KEY != '' + run: | + set -eo pipefail + npx percy build:reject "${{ steps.finalize.outputs.build_id }}" + echo "::warning::Rejected Percy build ${{ steps.finalize.outputs.build_url }} — host-test finished as '${{ needs.host-test.result }}', so its snapshot set is incomplete and must not become a baseline." + working-directory: packages/host + env: + PERCY_TOKEN: ${{ secrets.PERCY_TOKEN_HOST }} + + # The exact complement of the reject step's condition, so an incomplete + # build never passes through both silently. + - name: Note that an incomplete build was left unrejected + if: >- + needs.host-test.result != 'success' + && ( + steps.finalize.outputs.build_id == '' + || env.BROWSERSTACK_USERNAME == '' + || env.BROWSERSTACK_ACCESS_KEY == '' + ) + run: | + echo "::warning::host-test finished as '${{ needs.host-test.result }}', so this Percy build's snapshot set is incomplete, but it could not be rejected and may be auto-approved into the baseline. Percy build id: '${{ steps.finalize.outputs.build_id }}' (empty means the finalize output could not be parsed); BROWSERSTACK_USERNAME set: ${{ env.BROWSERSTACK_USERNAME != '' }}; BROWSERSTACK_ACCESS_KEY set: ${{ env.BROWSERSTACK_ACCESS_KEY != '' }}." + host-merge-reports-and-publish: name: Merge Host reports and publish if: ${{ !cancelled() && (needs.host-test.result == 'success' || needs.host-test.result == 'failure') }}