diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 34be8fada64..e1b21c1a06d 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -1,62 +1,358 @@ name: E2E Tests run-name: E2E Tests on: - # Only allow manual triggers - workflow_dispatch: + pull_request: + merge_group: + push: + branches: + - master + - branch/** + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true jobs: - build: - name: Build binaries - if: ${{ !startsWith(github.head_ref, 'dependabot/') }} + changes: + name: Check for relevant changes runs-on: ubuntu-latest permissions: contents: read + pull-requests: read - container: - image: ghcr.io/gravitational/teleport-buildbox-centos7:teleport19-amd64 - env: - GOCACHE: /tmp/gocache + outputs: + changed: ${{ steps.changes.outputs.changed }} steps: - - name: Checkout Teleport - uses: actions/checkout@v6 # Cannot upgrade to v4 while this runs in centos:7 due to nodejs GLIBC incompatibility + - name: Checkout + if: github.event_name != 'pull_request' + uses: actions/checkout@v6 + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changes + with: + base: ${{ github.event.pull_request.base.ref || github.event.merge_group.base_ref }} + ref: ${{ github.event.pull_request.head.ref || github.event.merge_group.head_ref }} + filters: | + changed: + - '.github/workflows/e2e-tests.yaml' + - '**/*.go' + - 'go.mod' + - 'go.sum' + - '**/*.rs' + - 'rust-toolchain.toml' + - 'Cargo.toml' + - '**/Cargo.toml' + - 'Cargo.lock' + - 'e2e/**' + - 'web/**' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - '.pnpmfile.cjs' + - '.npmrc' + - 'tsconfig*.json' + - 'gen/proto/ts/**' + - '.github/actions/prepare-workspace/action.yml' + - 'common.mk' + - 'build.assets/Makefile' + - 'build.assets/versions.mk' + - 'build.assets/Dockerfile*' + - 'build.assets/build-webassets-if-changed.sh' + - 'build.assets/build-e2e-binaries.sh' + - 'Makefile' + + build: + name: Build Teleport + needs: changes + if: ${{ needs.changes.outputs.changed == 'true' && !startsWith(github.head_ref, 'dependabot/') }} + runs-on: ubuntu-22.04-16core + timeout-minutes: 30 + + permissions: + contents: read + + outputs: + playwright-version: ${{ steps.playwright-version.outputs.version }} + + container: + image: ghcr.io/gravitational/teleport-buildbox:teleport19 + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 1 - name: Prepare workspace uses: ./.github/actions/prepare-workspace - - name: Run make + - name: Get go mod cache path + id: cache-paths + run: echo "go-mod=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT + + - name: Install node modules + run: pnpm install --frozen-lockfile + + - name: Type-check e2e tests + run: pnpm --filter=@gravitational/e2e type-check + + - name: Get Playwright version + id: playwright-version + run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT" + working-directory: e2e + + - name: Restore Go module cache + id: go-mod-cache + uses: actions/cache/restore@v5 + with: + path: ${{ steps.cache-paths.outputs.go-mod }} + key: go-mod-${{ runner.os }}-${{ hashFiles('go.sum') }} + restore-keys: go-mod-${{ runner.os }}- + + - name: Restore Rust output cache + id: rust-cache + uses: actions/cache/restore@v5 + with: + path: | + web/packages/shared/libs/ironrdp/pkg + target/x86_64-unknown-linux-gnu/release/librdp_client.a + lib/srv/desktop/rdp/rdpclient/librdpclient.h + key: rust-output-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'Cargo.toml', '**/Cargo.toml', 'Makefile', 'build.assets/Makefile', + 'build.assets/versions.mk', 'rust-toolchain.toml', 'lib/srv/desktop/rdp/rdpclient/**/*.rs', 'crates/**/*.rs', 'web/packages/shared/libs/ironrdp/**/*.rs') }} + + - name: Build Teleport, tctl, and tsh + run: make e2e-binaries + env: + IRONRDP_SKIP_BUILD: ${{ steps.rust-cache.outputs.cache-hit == 'true' && '1' || '0' }} + RDPCLIENT_SKIP_CARGO: ${{ steps.rust-cache.outputs.cache-hit == 'true' && '1' || '0' }} + + - name: Print build logs + if: failure() run: | - make binaries + for log in build-logs/*.log; do + echo "::group::$(basename "$log" .log) build log" + cat "$log" + echo "::endgroup::" + done + + - name: Build e2e runner + run: cd e2e/runner && go build -o e2e . + + - name: Build Connect + run: pnpm --filter=@gravitational/teleterm build + env: + IRONRDP_SKIP_BUILD: ${{ steps.rust-cache.outputs.cache-hit == 'true' && '1' || '0' }} - name: Upload binaries uses: actions/upload-artifact@v7 with: - name: build - path: ${{ github.workspace }}/build/ + name: teleport-binaries + path: | + build/teleport + build/tctl + build/tsh-e2e-webauthnmock + e2e/runner/e2e + web/packages/teleterm/build/ retention-days: 1 + - name: Save Go module cache + if: github.event_name == 'push' && steps.go-mod-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ${{ steps.cache-paths.outputs.go-mod }} + key: go-mod-${{ runner.os }}-${{ hashFiles('go.sum') }} + + - name: Save Rust output cache + if: github.event_name == 'push' && steps.rust-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: | + web/packages/shared/libs/ironrdp/pkg + target/x86_64-unknown-linux-gnu/release/librdp_client.a + lib/srv/desktop/rdp/rdpclient/librdpclient.h + key: rust-output-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'Cargo.toml', '**/Cargo.toml', 'Makefile', 'build.assets/Makefile', + 'build.assets/versions.mk', 'rust-toolchain.toml', 'lib/srv/desktop/rdp/rdpclient/**/*.rs', 'crates/**/*.rs', 'web/packages/shared/libs/ironrdp/**/*.rs') }} + test: name: E2E Tests - needs: build - runs-on: ubuntu-latest + needs: [changes, build] + if: ${{ needs.changes.outputs.changed == 'true' && needs.build.result == 'success' }} + runs-on: ubuntu-22.04-8core + timeout-minutes: 30 + + permissions: + contents: read + + env: + HOME: /root + E2E_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + + container: + image: mcr.microsoft.com/playwright:v${{ needs.build.outputs.playwright-version }}-noble + options: --init --ipc=host + env: + DOCKER_HOST: tcp://docker:2375 + + services: + docker: + image: docker:dind + options: --privileged + env: + DOCKER_TLS_CERTDIR: "" + ports: + - 2375:2375 + steps: - uses: actions/checkout@v6 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Install Docker CLI + run: | + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker.gpg + echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu noble stable" > /etc/apt/sources.list.d/docker.list + apt-get update && apt-get install -y docker-ce-cli + + - name: Setup e2e environment + shell: bash + run: | + set -o pipefail + for i in $(seq 1 30); do docker info >/dev/null 2>&1 && break || sleep 1; done + docker info >/dev/null 2>&1 || { echo "Docker failed to start"; exit 1; } + mkdir -p setup-logs + docker pull debian:bookworm-slim 2>&1 | tee setup-logs/docker-pull.log & pid_docker=$! + + corepack enable + pnpm install --frozen-lockfile 2>&1 | tee setup-logs/pnpm-install.log & pid_pnpm=$! + + failed=0 + wait $pid_docker || { echo "::error::docker pull failed with exit code $?"; failed=1; } + wait $pid_pnpm || { echo "::error::pnpm install failed with exit code $?"; failed=1; } + exit $failed + + - name: Upload setup logs + if: failure() + uses: actions/upload-artifact@v7 + with: + name: setup-logs + path: setup-logs/ + retention-days: 7 - uses: actions/download-artifact@v8 with: - name: build - path: ${{ github.workspace }}/build/ + name: teleport-binaries + path: ${{ github.workspace }} - name: Chmod binaries run: | chmod +x build/teleport chmod +x build/tctl - chmod +x build/tsh + chmod +x build/tsh-e2e-webauthnmock + chmod +x e2e/runner/e2e - - name: Build Images + - name: Create Docker config + run: mkdir -p ~/.docker && echo '{}' > ~/.docker/config.json + + - name: Run tests + run: xvfb-run e2e/runner/e2e --no-build --full + env: + E2E_CALLER_DIR: ${{ github.workspace }} + + - name: Publish GitHub report + if: ${{ !cancelled() }} + run: e2e/runner/e2e --github-report + + - name: Upload PR comment + if: ${{ !cancelled() && github.event_name == 'pull_request' }} + uses: actions/upload-artifact@v7 + with: + name: pr-comment + path: e2e/test-results/pr-comment.md + if-no-files-found: ignore + retention-days: 1 + + - name: Upload Playwright report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: playwright-report + path: e2e/playwright-report/ + retention-days: 7 + + - name: Upload test results + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: test-results + path: e2e/test-results/ + retention-days: 7 + + - name: Upload Teleport logs + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: teleport-logs + path: e2e/teleport-*.log + retention-days: 7 + + - name: Upload Docker node logs + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: docker-node-logs + path: e2e/docker-node-*.log + retention-days: 7 + + update-pr-comment: + name: Update PR comment + needs: test + if: ${{ always() && needs.test.result != 'skipped' && github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + timeout-minutes: 5 + + permissions: + pull-requests: write + + steps: + - name: Download PR comment artifact + id: download + uses: actions/download-artifact@v8 + with: + name: pr-comment + path: pr-comment + continue-on-error: true + + - name: Update PR comment + env: + GH_TOKEN: ${{ github.token }} + shell: bash run: | - make -C e2e build - - name: Run Tests - run: | - make test-e2e + comment_file="pr-comment/pr-comment.md" + marker="" + pr_number="${{ github.event.pull_request.number }}" + + # Find existing comment ID with the marker. + comment_id=$(gh api "repos/${{ github.repository }}/issues/${pr_number}/comments" \ + --paginate --jq ".[] | select(.user.type == \"Bot\") | select(.body | contains(\"${marker}\")) | .id" \ + | head -n1) + + if [ "${{ steps.download.outcome }}" == "success" ] && [ -f "$comment_file" ] && [ -s "$comment_file" ]; then + # File has content — update or create comment. + if [ -n "$comment_id" ]; then + gh api -X PATCH "repos/${{ github.repository }}/issues/comments/${comment_id}" \ + -F body=@"$comment_file" + echo "Updated E2E results PR comment" + else + gh api -X POST "repos/${{ github.repository }}/issues/${pr_number}/comments" \ + -F body=@"$comment_file" + echo "Created E2E results PR comment" + fi + elif [ -n "$comment_id" ]; then + # No file or empty file — delete existing comment. + gh api -X DELETE "repos/${{ github.repository }}/issues/comments/${comment_id}" + echo "Deleted existing E2E results PR comment" + else + echo "No comment to update" + fi diff --git a/Makefile b/Makefile index c4d80cea5cb..ac5f7ab9d7a 100644 --- a/Makefile +++ b/Makefile @@ -492,11 +492,17 @@ bpf-up-to-date: must-start-clean/host bpf-bytecode update-vmlinux-h: bpftool btf dump file /sys/kernel/btf/vmlinux format c >bpf/vmlinux.h +RDPCLIENT_SKIP_CARGO ?= 0 + .PHONY: rdpclient rdpclient: rustup-toolchain-warning ifeq ("$(with_rdpclient)", "yes") +ifneq ($(RDPCLIENT_SKIP_CARGO),1) $(RDPCLIENT_ENV) \ cargo build -p rdp-client $(if $(FIPS),--features=fips) --release --locked $(CARGO_TARGET) +else + @echo "Skipping rdp-client cargo build (RDPCLIENT_SKIP_CARGO=1)" +endif endif .PHONY: rdpdecoder @@ -518,13 +524,20 @@ define ironrdp_package_json endef export ironrdp_package_json +IRONRDP_SKIP_BUILD ?= 0 + .PHONY: build-ironrdp-wasm build-ironrdp-wasm: ironrdp = web/packages/shared/libs/ironrdp +ifeq ($(IRONRDP_SKIP_BUILD),1) +build-ironrdp-wasm: + @echo "Skipping ironrdp WASM build (IRONRDP_SKIP_BUILD=1)" +else build-ironrdp-wasm: ensure-wasm-deps RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo build --package ironrdp --lib --target $(CARGO_WASM_TARGET) --release wasm-opt target/$(CARGO_WASM_TARGET)/release/ironrdp.wasm -o target/$(CARGO_WASM_TARGET)/release/ironrdp.wasm -O wasm-bindgen target/$(CARGO_WASM_TARGET)/release/ironrdp.wasm --out-dir $(ironrdp)/pkg --typescript --target web printenv ironrdp_package_json > $(ironrdp)/pkg/package.json +endif # Build libfido2 and dependencies for MacOS. Uses exported C_ARCH variable defined earlier. .PHONY: build-fido2 @@ -1231,6 +1244,12 @@ e2e-aws: | $(TEST_LOG_DIR) $(CGOFLAG) go test -json $(PACKAGES) $(FLAGS) $(ADDFLAGS)\ | $(GOTESTSUM) --junitfile $(TEST_LOG_DIR)/unit-tests-e2e-aws.xml --jsonfile $(TEST_LOG_DIR)/unit-tests-e2e-aws.json --raw-command -- cat +# e2e-binaries builds teleport, tctl, and tsh (with webauthnmock) in parallel +# for use in e2e tests. +.PHONY: e2e-binaries +e2e-binaries: + build.assets/build-e2e-binaries.sh + # # Lint the source code. # By default lint scans the entire repo. Pass GO_LINT_FLAGS='--new' to only scan local diff --git a/build.assets/build-e2e-binaries.sh b/build.assets/build-e2e-binaries.sh new file mode 100755 index 00000000000..c2c7d246aef --- /dev/null +++ b/build.assets/build-e2e-binaries.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# +# Builds teleport, tctl, and tsh (with webauthnmock) in parallel for e2e tests. +# Build logs are written to build-logs/. +# + +set -eo pipefail + +MAKE="${MAKE:-make}" +BUILDDIR="${BUILDDIR:-build}" + +cargo --version && rustc --version && echo "ARCH=$(uname -m)" + +mkdir -p build-logs + +${MAKE} "${BUILDDIR}/teleport" 2>&1 | tee build-logs/teleport.log & pid_teleport=$! +${MAKE} "${BUILDDIR}/tctl" 2>&1 | tee build-logs/tctl.log & pid_tctl=$! +go build -tags webauthnmock -o "${BUILDDIR}/tsh-e2e-webauthnmock" ./tool/tsh 2>&1 | tee build-logs/tsh.log & pid_tsh=$! + +failed=0 +wait $pid_teleport || { echo "::error::make ${BUILDDIR}/teleport failed with exit code $?"; failed=1; } +wait $pid_tctl || { echo "::error::make ${BUILDDIR}/tctl failed with exit code $?"; failed=1; } +wait $pid_tsh || { echo "::error::go build tsh failed with exit code $?"; failed=1; } +exit $failed diff --git a/e2e/package.json b/e2e/package.json index 0ffcc2cb02e..c229a18229b 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -11,9 +11,7 @@ "license": "Apache-2.0", "devDependencies": { "@playwright/test": "^1.58.2", - "@types/node": "^25.5.0", "cbor-x": "^1.6.4", - "tsx": "^4.21.0", - "typescript": "^5.9.3" + "tsx": "^4.21.0" } } diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index fbe40480eea..04fc643e98a 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -31,14 +31,14 @@ const browserDevices: Record = { export default defineConfig({ testDir: './tests', - timeout: 15_000, + timeout: 20_000, fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 1 : 0, workers: process.env.CI ? 1 : undefined, reporter: [ ['html', { open: 'never' }], - ['json', { outputFile: 'test-results/.results.json' }], + ['json', { outputFile: 'test-results/results.json' }], ], use: { diff --git a/e2e/pnpm-lock.yaml b/e2e/pnpm-lock.yaml deleted file mode 100644 index 154479ef8da..00000000000 --- a/e2e/pnpm-lock.yaml +++ /dev/null @@ -1,470 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - '@playwright/test': - specifier: ^1.58.2 - version: 1.58.2 - '@types/node': - specifier: ^25.5.0 - version: 25.5.0 - cbor-x: - specifier: ^1.6.4 - version: 1.6.4 - tsx: - specifier: ^4.21.0 - version: 4.21.0 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - -packages: - - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} - cpu: [arm64] - os: [darwin] - - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} - cpu: [x64] - os: [darwin] - - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} - cpu: [arm64] - os: [linux] - - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} - cpu: [arm] - os: [linux] - - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} - cpu: [x64] - os: [linux] - - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} - cpu: [x64] - os: [win32] - - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@playwright/test@1.58.2': - resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} - engines: {node: '>=18'} - hasBin: true - - '@types/node@25.5.0': - resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} - - cbor-extract@2.2.2: - resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} - hasBin: true - - cbor-x@1.6.4: - resolution: {integrity: sha512-UGKHjp6RHC6QuZ2yy5LCKm7MojM4716DwoSaqwQpaH4DvZvbBTGcoDNTiG9Y2lByXZYFEs9WRkS5tLl96IrF1Q==} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} - engines: {node: '>=18'} - hasBin: true - - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - get-tsconfig@4.13.6: - resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} - - node-gyp-build-optional-packages@5.1.1: - resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} - hasBin: true - - playwright-core@1.58.2: - resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} - engines: {node: '>=18'} - hasBin: true - - playwright@1.58.2: - resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} - engines: {node: '>=18'} - hasBin: true - - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} - engines: {node: '>=18.0.0'} - hasBin: true - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - -snapshots: - - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - optional: true - - '@esbuild/aix-ppc64@0.27.3': - optional: true - - '@esbuild/android-arm64@0.27.3': - optional: true - - '@esbuild/android-arm@0.27.3': - optional: true - - '@esbuild/android-x64@0.27.3': - optional: true - - '@esbuild/darwin-arm64@0.27.3': - optional: true - - '@esbuild/darwin-x64@0.27.3': - optional: true - - '@esbuild/freebsd-arm64@0.27.3': - optional: true - - '@esbuild/freebsd-x64@0.27.3': - optional: true - - '@esbuild/linux-arm64@0.27.3': - optional: true - - '@esbuild/linux-arm@0.27.3': - optional: true - - '@esbuild/linux-ia32@0.27.3': - optional: true - - '@esbuild/linux-loong64@0.27.3': - optional: true - - '@esbuild/linux-mips64el@0.27.3': - optional: true - - '@esbuild/linux-ppc64@0.27.3': - optional: true - - '@esbuild/linux-riscv64@0.27.3': - optional: true - - '@esbuild/linux-s390x@0.27.3': - optional: true - - '@esbuild/linux-x64@0.27.3': - optional: true - - '@esbuild/netbsd-arm64@0.27.3': - optional: true - - '@esbuild/netbsd-x64@0.27.3': - optional: true - - '@esbuild/openbsd-arm64@0.27.3': - optional: true - - '@esbuild/openbsd-x64@0.27.3': - optional: true - - '@esbuild/openharmony-arm64@0.27.3': - optional: true - - '@esbuild/sunos-x64@0.27.3': - optional: true - - '@esbuild/win32-arm64@0.27.3': - optional: true - - '@esbuild/win32-ia32@0.27.3': - optional: true - - '@esbuild/win32-x64@0.27.3': - optional: true - - '@playwright/test@1.58.2': - dependencies: - playwright: 1.58.2 - - '@types/node@25.5.0': - dependencies: - undici-types: 7.18.2 - - cbor-extract@2.2.2: - dependencies: - node-gyp-build-optional-packages: 5.1.1 - optionalDependencies: - '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 - '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 - '@cbor-extract/cbor-extract-linux-x64': 2.2.2 - '@cbor-extract/cbor-extract-win32-x64': 2.2.2 - optional: true - - cbor-x@1.6.4: - optionalDependencies: - cbor-extract: 2.2.2 - - detect-libc@2.1.2: - optional: true - - esbuild@0.27.3: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 - - fsevents@2.3.2: - optional: true - - fsevents@2.3.3: - optional: true - - get-tsconfig@4.13.6: - dependencies: - resolve-pkg-maps: 1.0.0 - - node-gyp-build-optional-packages@5.1.1: - dependencies: - detect-libc: 2.1.2 - optional: true - - playwright-core@1.58.2: {} - - playwright@1.58.2: - dependencies: - playwright-core: 1.58.2 - optionalDependencies: - fsevents: 2.3.2 - - resolve-pkg-maps@1.0.0: {} - - tsx@4.21.0: - dependencies: - esbuild: 0.27.3 - get-tsconfig: 4.13.6 - optionalDependencies: - fsevents: 2.3.3 - - typescript@5.9.3: {} - - undici-types@7.18.2: {} diff --git a/e2e/pnpm-workspace.yaml b/e2e/pnpm-workspace.yaml deleted file mode 100644 index 22be0eefcac..00000000000 --- a/e2e/pnpm-workspace.yaml +++ /dev/null @@ -1 +0,0 @@ -sharedWorkspaceLockfile: false diff --git a/e2e/runner/flags.go b/e2e/runner/flags.go index b8a7702ad95..fc59f5815f1 100644 --- a/e2e/runner/flags.go +++ b/e2e/runner/flags.go @@ -25,7 +25,6 @@ import ( "os" "path/filepath" "slices" - "strconv" "strings" ) @@ -50,6 +49,7 @@ type e2eFlags struct { testFiles []string reportPR int reportRepo string + reportSHA string tracePath string } @@ -67,8 +67,11 @@ func parseFlags(repoRoot string) (*e2eFlags, runMode, error) { modes.register("debug", "run tests with Playwright inspector (PWDEBUG=1)", modeDebug) modes.register("browse", "open a signed-in browser for manual web testing", modeBrowse) modes.register("browse-connect", "open a signed-in Teleport Connect app for manual testing", modeBrowseConnect) - modes.register("report", "download and open a Playwright report for a PR (pass PR number as argument)", modeReport) - modes.register("test-results", "download test results and open a trace for a PR (pass PR number and trace path as arguments)", modeTestResults) + modes.register("github-report", "publish test results as GitHub annotations, job summary, and PR comment (CI only)", modeGitHubReport) + + var testResultsPR int + flag.IntVar(&f.reportPR, "report", 0, "download and open a Playwright report for a given PR number") + flag.IntVar(&testResultsPR, "test-results", 0, "download test results and open a trace for a given PR number (pass trace path as argument)") flag.BoolVar(&f.verbose, "v", false, "enable debug logging") flag.BoolVar(&f.noBuild, "no-build", false, "skip make binaries") // useful for running during development to avoid rebuilding Teleport every time @@ -89,6 +92,7 @@ func parseFlags(repoRoot string) (*e2eFlags, runMode, error) { "override teleport URL for Playwright tests (e.g. https://localhost:3080), if set the runner will skip starting Teleport") flag.StringVar(&f.reportRepo, "repo", "", "GitHub repo name (e.g. teleport.e), auto-detected if omitted") + flag.StringVar(&f.reportSHA, "sha", "", "commit SHA to download artifacts for (overrides PR head SHA)") bindFixtureFlags(flag.CommandLine) modes.bindFlags(flag.CommandLine) @@ -123,26 +127,29 @@ func parseFlags(repoRoot string) (*e2eFlags, runMode, error) { return nil, 0, err } - if mode == modeReport || mode == modeTestResults { + switch { + case f.reportPR > 0 && testResultsPR > 0: + return nil, 0, fmt.Errorf("--report and --test-results are mutually exclusive") + case f.reportPR > 0: + if mode != modeTest { + return nil, 0, fmt.Errorf("--report and --%s are mutually exclusive", mode) + } + mode = modeReport + case testResultsPR > 0: + if mode != modeTest { + return nil, 0, fmt.Errorf("--test-results and --%s are mutually exclusive", mode) + } + mode = modeTestResults + f.reportPR = testResultsPR + args := flag.Args() if len(args) < 1 { - return nil, 0, fmt.Errorf("--%s requires a PR number as the first argument", mode) + return nil, 0, fmt.Errorf("--test-results requires a trace path as an argument") } + f.tracePath = args[0] + } - pr, err := strconv.Atoi(args[0]) - if err != nil { - return nil, 0, fmt.Errorf("invalid PR number %q: %w", args[0], err) - } - - f.reportPR = pr - - if mode == modeTestResults { - if len(args) < 2 { - return nil, 0, fmt.Errorf("--test-results requires a trace path as the second argument") - } - f.tracePath = args[1] - } - } else { + if mode != modeReport && mode != modeTestResults { e2eDir := filepath.Join(repoRoot, "e2e") f.testFiles, err = normalizeTestFiles(e2eDir, flag.Args()) diff --git a/e2e/runner/github.go b/e2e/runner/github.go index 1da045341e0..87811f90203 100644 --- a/e2e/runner/github.go +++ b/e2e/runner/github.go @@ -19,7 +19,6 @@ package main import ( - "context" "encoding/json" "fmt" "io" @@ -28,15 +27,11 @@ import ( "path/filepath" "slices" "strings" - - "github.com/google/go-github/v84/github" - "golang.org/x/oauth2" ) const commentMarker = "" -func writeGitHubReport(e2eDir string) error { - resultsPath := filepath.Join(e2eDir, "test-results", ".results.json") +func writeGitHubReport(resultsPath string) error { data, err := os.ReadFile(resultsPath) if err != nil { return fmt.Errorf("could not read test results: %w", err) @@ -66,8 +61,8 @@ func writeGitHubReport(e2eDir string) error { slog.Warn("could not write job summary", "error", err) } - if err := managePRComment(report, mergedFailures, mergedFlaky); err != nil { - slog.Warn("could not manage PR comment", "error", err) + if err := writePRCommentFile(resultsPath, report, mergedFailures, mergedFlaky); err != nil { + slog.Warn("could not write PR comment file", "error", err) } return nil @@ -230,145 +225,41 @@ func renderMarkdownReport(w io.Writer, report pwReport, failures, flaky []merged if pr := ciPRNumber(); pr > 0 { fmt.Fprint(w, "---\n\n") - fmt.Fprintf(w, "##### View full report\n```\n./e2e/run.sh --report %d\n```\n", pr) + fmt.Fprintf(w, "##### View full report\n```\n./%s\n```\n", ciReportCmd(pr)) } } -func managePRComment(report pwReport, failures, flaky []mergedFailure) error { - token := os.Getenv("GITHUB_TOKEN") - if token == "" { - return fmt.Errorf("GITHUB_TOKEN not set") - } - - prNumber := prNumberFromEvent() - if prNumber == 0 { - slog.Info("not a pull request event, skipping PR comment") - - return nil - } - - repo := os.Getenv("GITHUB_REPOSITORY") - if repo == "" { - slog.Warn("GITHUB_REPOSITORY not set, skipping PR comment") - - return nil - } - - owner, repoName, ok := strings.Cut(repo, "/") - if !ok { - return fmt.Errorf("invalid GITHUB_REPOSITORY format") - } - - ctx := context.Background() - ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) - client := github.NewClient(oauth2.NewClient(ctx, ts)) - - existing := findExistingComment(ctx, client, owner, repoName, prNumber) +// writePRCommentFile writes the PR comment body to a file next to the results +// JSON so that a trusted workflow step can post it via `gh` without passing +// a write-scoped token to this (PR-built) binary. +func writePRCommentFile(resultsPath string, report pwReport, failures, flaky []mergedFailure) error { + commentPath := filepath.Join(filepath.Dir(resultsPath), "pr-comment.md") hasIssues := len(failures) > 0 || len(flaky) > 0 - if !hasIssues { - // If there are no issues, remove existing comment if present and return. - if existing != nil { - if _, err := client.Issues.DeleteComment(ctx, owner, repoName, existing.GetID()); err != nil { - return fmt.Errorf("deleting existing PR comment: %w", err) - } - - slog.Info("deleted existing E2E results PR comment") + // Write an empty file to signal that the comment should be deleted. + if err := os.WriteFile(commentPath, nil, 0o644); err != nil { + return fmt.Errorf("writing empty PR comment file: %w", err) } + slog.Info("wrote empty PR comment file (no issues)", "path", commentPath) + return nil } var body strings.Builder - body.WriteString(commentMarker + "\n") renderMarkdownReport(&body, report, failures, flaky) - commentBody := body.String() - if existing != nil { - if _, _, err := client.Issues.EditComment(ctx, owner, repoName, existing.GetID(), &github.IssueComment{ - Body: github.Ptr(commentBody), - }); err != nil { - return fmt.Errorf("updating existing PR comment: %w", err) - } - - slog.Info("updated E2E results PR comment") - - return nil + if err := os.WriteFile(commentPath, []byte(body.String()), 0o644); err != nil { + return fmt.Errorf("writing PR comment file: %w", err) } - if _, _, err := client.Issues.CreateComment(ctx, owner, repoName, prNumber, &github.IssueComment{ - Body: github.Ptr(commentBody), - }); err != nil { - return fmt.Errorf("creating PR comment: %w", err) - } - - slog.Info("created E2E results PR comment") + slog.Info("wrote PR comment file", "path", commentPath) return nil } -func findExistingComment(ctx context.Context, client *github.Client, owner, repo string, prNumber int) *github.IssueComment { - opts := &github.IssueListCommentsOptions{ - ListOptions: github.ListOptions{PerPage: 100}, - } - - for { - comments, resp, err := client.Issues.ListComments(ctx, owner, repo, prNumber, opts) - if err != nil { - slog.Warn("could not list PR comments", "error", err) - return nil - } - - for _, c := range comments { - if c.GetUser().GetType() != "Bot" { - continue - } - if strings.Contains(c.GetBody(), commentMarker) { - return c - } - } - - if resp.NextPage == 0 { - break - } - - opts.Page = resp.NextPage - } - - return nil -} - -func prNumberFromEvent() int { - eventPath := os.Getenv("GITHUB_EVENT_PATH") - if eventPath == "" { - return 0 - } - - data, err := os.ReadFile(eventPath) - if err != nil { - slog.Warn("could not read event payload", "path", eventPath, "error", err) - return 0 - } - - var event struct { - PullRequest *struct { - Number int `json:"number"` - } `json:"pull_request"` - } - if err := json.Unmarshal(data, &event); err != nil { - slog.Warn("could not parse event payload", "error", err) - return 0 - } - - if event.PullRequest == nil { - return 0 - } - - return event.PullRequest.Number -} - func escapeProp(s string) string { s = strings.ReplaceAll(s, "%", "%25") s = strings.ReplaceAll(s, "\r", "%0D") diff --git a/e2e/runner/main.go b/e2e/runner/main.go index 995a32892b5..f91e38976a7 100644 --- a/e2e/runner/main.go +++ b/e2e/runner/main.go @@ -49,12 +49,22 @@ func main() { os.Exit(1) } + resultsPath := filepath.Join(e2eDir, "test-results", "results.json") + flags, mode, err := parseFlags(filepath.Dir(e2eDir)) if err != nil { slog.Error("failed to parse flags", "error", err) os.Exit(1) } + if mode == modeGitHubReport { + if err := writeGitHubReport(resultsPath); err != nil { + slog.Error("failed to write GitHub report", "error", err) + os.Exit(1) + } + return + } + if mode == modeReport || mode == modeTestResults { repo := flags.reportRepo if repo == "" { @@ -64,6 +74,7 @@ func main() { cfg := &reportConfig{ prNumber: flags.reportPR, repo: repo, + sha: flags.reportSHA, e2eDir: e2eDir, tracePath: flags.tracePath, } @@ -82,7 +93,7 @@ func main() { return } - _ = os.Remove(filepath.Join(e2eDir, "test-results", ".results.json")) + _ = os.Remove(resultsPath) isCI := os.Getenv("CI") != "" runErr := run(flags, mode, e2eDir, isCI) @@ -97,14 +108,8 @@ func main() { tty.Close() } - if isCI { - if err := writeGitHubReport(e2eDir); err != nil { - slog.Error("failed to write GitHub report", "error", err) - } - } - if !flags.quiet { - printTestSummary(e2eDir) + printTestSummary(e2eDir, resultsPath) } if runErr != nil { diff --git a/e2e/runner/mode.go b/e2e/runner/mode.go index 95a5a5b6266..031fd7c63bf 100644 --- a/e2e/runner/mode.go +++ b/e2e/runner/mode.go @@ -34,6 +34,7 @@ const ( modeBrowseConnect modeReport modeTestResults + modeGitHubReport ) func (m runMode) String() string { @@ -54,6 +55,8 @@ func (m runMode) String() string { return "report" case modeTestResults: return "test-results" + case modeGitHubReport: + return "github-report" default: return fmt.Sprintf("unknown(%d)", m) } diff --git a/e2e/runner/playwright.go b/e2e/runner/playwright.go index ec0316f1602..c4e2b72eea0 100644 --- a/e2e/runner/playwright.go +++ b/e2e/runner/playwright.go @@ -330,7 +330,7 @@ func (p *playwrightRunner) outputWriters() (io.Writer, io.Writer) { var showReportCmd string if p.config.isCI { if pr := ciPRNumber(); pr > 0 { - showReportCmd = fmt.Sprintf("e2e/run.sh --report %d", pr) + showReportCmd = ciReportCmd(pr) } } if showReportCmd == "" { diff --git a/e2e/runner/report.go b/e2e/runner/report.go index 8fc03ebe641..c47fd44a616 100644 --- a/e2e/runner/report.go +++ b/e2e/runner/report.go @@ -39,12 +39,13 @@ import ( type reportConfig struct { prNumber int repo string + sha string e2eDir string tracePath string } func runReport(cfg *reportConfig) error { - tmpDir, err := downloadArtifact(cfg.prNumber, cfg.repo, "playwright-report") + tmpDir, err := downloadArtifact(cfg, "playwright-report") if err != nil { return err } @@ -60,7 +61,7 @@ func runReport(cfg *reportConfig) error { } func runTestResults(cfg *reportConfig) error { - tmpDir, err := downloadArtifact(cfg.prNumber, cfg.repo, "test-results") + tmpDir, err := downloadArtifact(cfg, "test-results") if err != nil { return err } @@ -95,23 +96,28 @@ func ghClient(ctx context.Context) (*github.Client, error) { return github.NewClient(oauth2.NewClient(ctx, ts)), nil } -func downloadArtifact(prNumber int, repo, artifactName string) (string, error) { +func downloadArtifact(cfg *reportConfig, artifactName string) (string, error) { ctx := context.Background() client, err := ghClient(ctx) if err != nil { return "", err } - ghRepo := "gravitational/" + repo + ghRepo := "gravitational/" + cfg.repo owner, repoName, _ := strings.Cut(ghRepo, "/") - pr, _, err := client.PullRequests.Get(ctx, owner, repoName, prNumber) - if err != nil { - return "", fmt.Errorf("getting PR #%d: %w", prNumber, err) - } + headSHA := cfg.sha + if headSHA == "" { + pr, _, err := client.PullRequests.Get(ctx, owner, repoName, cfg.prNumber) + if err != nil { + return "", fmt.Errorf("getting PR #%d: %w", cfg.prNumber, err) + } - headSHA := pr.GetHead().GetSHA() - slog.Debug("resolved PR head SHA", "sha", headSHA) + headSHA = pr.GetHead().GetSHA() + slog.Debug("resolved PR head SHA", "sha", headSHA) + } else { + slog.Debug("using provided SHA", "sha", headSHA) + } opts := &github.ListArtifactsOptions{ Name: github.Ptr(artifactName), @@ -125,7 +131,7 @@ func downloadArtifact(prNumber int, repo, artifactName string) (string, error) { } for _, a := range artifacts.Artifacts { - if a.GetWorkflowRun().GetHeadSHA() == headSHA { + if strings.HasPrefix(a.GetWorkflowRun().GetHeadSHA(), headSHA) { target = a break @@ -233,6 +239,24 @@ func ciPRNumber() int { return 0 } +func ciShortHeadSHA() string { + sha := os.Getenv("E2E_HEAD_SHA") + if len(sha) > 8 { + sha = sha[:8] + } + + return sha +} + +func ciReportCmd(pr int) string { + cmd := fmt.Sprintf("e2e/run.sh --report %d", pr) + if sha := ciShortHeadSHA(); sha != "" { + cmd += " --sha " + sha + } + + return cmd +} + func detectRepo(e2eDir string) string { if filepath.Base(filepath.Dir(e2eDir)) == "e" { return "teleport.e" diff --git a/e2e/runner/summary.go b/e2e/runner/summary.go index b300d49bfa9..b32f7848593 100644 --- a/e2e/runner/summary.go +++ b/e2e/runner/summary.go @@ -30,8 +30,7 @@ import ( // printTestSummary reads the Playwright test results and prints them the same way that Playwright does, so // we can show the overall test summary at the end after everything has exited and stopped logging. -func printTestSummary(e2eDir string) { - resultsPath := filepath.Join(e2eDir, "test-results", ".results.json") +func printTestSummary(e2eDir, resultsPath string) { data, err := os.ReadFile(resultsPath) if err != nil { slog.Warn("could not read test results", "path", resultsPath, "error", err) @@ -94,7 +93,7 @@ func printTestSummary(e2eDir string) { var showReportCmd string if ciPR > 0 { - showReportCmd = fmt.Sprintf("e2e/run.sh --report %d", ciPR) + showReportCmd = ciReportCmd(ciPR) } else { showReportCmd = "pnpm show-report" if pathPrefix != "" { @@ -111,6 +110,9 @@ func printTestSummary(e2eDir string) { var traceCmd string if ciPR > 0 { traceCmd = fmt.Sprintf("e2e/run.sh --test-results %d %s", ciPR, t.relPath) + if sha := ciShortHeadSHA(); sha != "" { + traceCmd += " --sha " + sha + } } else { traceCmd = fmt.Sprintf("pnpm exec playwright show-trace %s", pathPrefix+t.relPath) } diff --git a/e2e/tests/connect/stateRestoration.spec.ts b/e2e/tests/connect/stateRestoration.spec.ts index 98aa21169ce..54ab19317d8 100644 --- a/e2e/tests/connect/stateRestoration.spec.ts +++ b/e2e/tests/connect/stateRestoration.spec.ts @@ -32,25 +32,56 @@ import { // These tests manage the app lifecycle manually (multiple launches/closes), so they do not use the // `app` fixture. test.describe('state restoration from disk', () => { - let tempPath: string; + // Run all tests in this block sequentially in a single worker so that beforeAll runs exactly + // once, not once per test (which fullyParallel would otherwise cause). + test.describe.configure({ mode: 'serial' }); - test.beforeEach(async () => { - tempPath = await fs.mkdtemp(path.join(os.tmpdir(), 'connect-e2e-state-')); - await initializeDataDir(tempPath, withDefaultAppConfig({})); + // A snapshot of a data dir with a logged-in session, created once in beforeAll. Tests that need + // logged-in state copy this snapshot instead of calling login again, keeping total login calls to + // a minimum (important to avoid rate-limiting when running full Connect suite). + let loggedInSnapshotPath: string; + + test.beforeAll(async () => { + loggedInSnapshotPath = await fs.mkdtemp( + path.join(os.tmpdir(), 'connect-e2e-state-snapshot-') + ); + await initializeDataDir(loggedInSnapshotPath, withDefaultAppConfig({})); + + // Login once to populate the data dir with session state. + { + await using app = await launchApp(loggedInSnapshotPath); + await login(app.page); + } }); - test.afterEach(async () => { - await fs.rm(tempPath, { recursive: true, force: true }); + test.afterAll(async () => { + await fs.rm(loggedInSnapshotPath, { recursive: true, force: true }); }); + /** + * Creates a fresh disposable temp dir pre-populated with the logged-in snapshot. + */ + async function loggedInDataDir() { + const temp = await fs.mkdtempDisposable( + path.join(os.tmpdir(), 'connect-e2e-state-') + ); + try { + await fs.cp(loggedInSnapshotPath, temp.path, { recursive: true }); + } catch (error) { + temp.remove(); + throw error; + } + return temp; + } + test('relaunch restores tabs', async () => { - // Login and create extra tabs. + await using temp = await loggedInDataDir(); + + // Open a terminal tab so there are 2 tabs (cluster + terminal). { - await using app = await launchApp(tempPath); + await using app = await launchApp(temp.path); const { page } = app; - await login(page); - // Open a terminal tab so there are 2 tabs (cluster + terminal). await page.getByTitle('Additional Actions').click(); await page.getByText('Open new terminal').click(); await expect( @@ -60,7 +91,7 @@ test.describe('state restoration from disk', () => { // Relaunch – the app should offer to restore the terminal tab. { - await using app = await launchApp(tempPath); + await using app = await launchApp(temp.path); const { page } = app; await expect(page.getByText('Reopen previous session')).toBeVisible(); await page.getByRole('button', { name: 'Reopen' }).click(); @@ -77,18 +108,14 @@ test.describe('state restoration from disk', () => { }); test('missing app_state.json does not crash the app', async () => { - // Login to create state files on disk. - { - await using app = await launchApp(tempPath); - await login(app.page); - } + await using temp = await loggedInDataDir(); // Remove app_state.json (keep tsh dir) – the app should not crash. - const appStatePath = path.join(tempPath, 'userData', 'app_state.json'); + const appStatePath = path.join(temp.path, 'userData', 'app_state.json'); await fs.rm(appStatePath); { - await using app = await launchApp(tempPath); + await using app = await launchApp(temp.path); // Without app_state.json, the app has no saved rootClusterUri, so no workspace is activated // and the cluster connect panel prompts the user to pick one. @@ -99,18 +126,14 @@ test.describe('state restoration from disk', () => { }); test('missing tsh home directory does not crash the app', async () => { - // Login to create state files on disk. - { - await using app = await launchApp(tempPath); - await login(app.page); - } + await using temp = await loggedInDataDir(); // Remove the tsh home directory (keep app_state.json) – the app should not crash. - const tshHomePath = path.join(tempPath, 'home', '.tsh'); + const tshHomePath = path.join(temp.path, 'home', '.tsh'); await fs.rm(tshHomePath, { recursive: true }); { - await using app = await launchApp(tempPath); + await using app = await launchApp(temp.path); const { page } = app; // With no tsh dir, no cluster can be connected. @@ -123,9 +146,9 @@ test.describe('state restoration from disk', () => { }); test('logout clears previous tabs', async () => { - await using app = await launchApp(tempPath); + await using temp = await loggedInDataDir(); + await using app = await launchApp(temp.path); const { page } = app; - await login(page); // Open a terminal tab. await page.getByTitle('Additional Actions').click(); @@ -159,11 +182,12 @@ test.describe('state restoration from disk', () => { }); test('identical workspace shape does not trigger restore dialog', async () => { - // Login, then replace the default cluster tab with a new one (same shape). + await using temp = await loggedInDataDir(); + + // Replace the default cluster tab with a new one (same shape). { - await using app = await launchApp(tempPath); + await using app = await launchApp(temp.path); const { page } = app; - await login(page); const clusterTab = page.locator( '[role="tab"][data-doc-kind="doc.cluster"]' @@ -179,7 +203,7 @@ test.describe('state restoration from disk', () => { // Relaunch – no restore dialog since the workspace has the same shape. { - await using app = await launchApp(tempPath); + await using app = await launchApp(temp.path); const { page } = app; const clusterTab = page.locator( @@ -191,10 +215,20 @@ test.describe('state restoration from disk', () => { }); test('window remembers size and position after restart', async () => { + await using temp = await fs.mkdtempDisposable( + path.join(os.tmpdir(), 'connect-e2e-state-') + ); + await initializeDataDir(temp.path, withDefaultAppConfig({})); + // Launch the app and resize the window. - let targetBounds: { x: number; y: number; width: number; height: number }; + let targetBounds: { + x: number; + y: number; + width: number; + height: number; + }; { - await using app = await launchApp(tempPath); + await using app = await launchApp(temp.path); const { page } = app; await expect(page.getByText('Connect a Cluster')).toBeVisible(); @@ -240,7 +274,7 @@ test.describe('state restoration from disk', () => { // Relaunch – the window should restore to the same size & position. { - await using app = await launchApp(tempPath); + await using app = await launchApp(temp.path); // Wait for the app to fully initialize before reading bounds and closing, otherwise the // app may close before the renderer acks the initial cluster store message, causing a diff --git a/e2e/tests/web/authenticated/authconnectors.spec.ts b/e2e/tests/web/authenticated/authconnectors.spec.ts index 327d6d75306..febe5d74d09 100644 --- a/e2e/tests/web/authenticated/authconnectors.spec.ts +++ b/e2e/tests/web/authenticated/authconnectors.spec.ts @@ -19,7 +19,8 @@ import { expect, test } from '@gravitational/e2e/helpers/test'; import { mockWebAuthn } from '@gravitational/e2e/helpers/webauthn'; -test('verify that a user can create and delete an auth connector', async ({ +// TODO(@rudream): re-enable this once the UI bug is fixed. +test.skip('verify that a user can create and delete an auth connector', async ({ page, }) => { await mockWebAuthn(page); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d6da36d76e..4b3ed9ec9d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,7 +132,7 @@ importers: version: 10.2.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/react-vite': specifier: ^10.2.14 - version: 10.2.14(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2)) + version: 10.2.14(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -225,10 +225,22 @@ importers: version: 5.9.3 vite: specifier: ^8.0.0 - version: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2) + version: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) e/web/teleport: {} + e2e: + devDependencies: + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 + cbor-x: + specifier: ^1.6.4 + version: 1.6.4 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + web/packages/build: dependencies: '@babel/core': @@ -254,7 +266,7 @@ importers: version: 27.0.0 '@vitejs/plugin-react-swc': specifier: ^4.3.0 - version: 4.3.0(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2)) + version: 4.3.0(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) babel-plugin-styled-components: specifier: ^2.1.4 version: 2.1.4(@babel/core@7.29.0)(styled-components@6.3.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) @@ -278,7 +290,7 @@ importers: version: 2.5.0 vite-plugin-wasm: specifier: ^3.5.0 - version: 3.5.0(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2)) + version: 3.5.0(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) web/packages/design: dependencies: @@ -486,7 +498,7 @@ importers: version: 6.8.3 electron-vite: specifier: ^5.0.0 - version: 5.0.0(@swc/core@1.15.18)(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2)) + version: 5.0.0(@swc/core@1.15.18)(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) events: specifier: 3.3.0 version: 3.3.0 @@ -1153,6 +1165,36 @@ packages: resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + '@codemirror/autocomplete@6.20.1': resolution: {integrity: sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==} @@ -2631,6 +2673,11 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@protobuf-ts/grpc-transport@2.11.1': resolution: {integrity: sha512-l6wrcFffY+tuNnuyrNCkRM8hDIsAZVLA8Mn7PKdVyYxITosYh60qW663p9kL6TWXYuDCL3oxH8ih3vLKTDyhtg==} peerDependencies: @@ -3824,6 +3871,13 @@ packages: caniuse-lite@1.0.30001776: resolution: {integrity: sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==} + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.4: + resolution: {integrity: sha512-UGKHjp6RHC6QuZ2yy5LCKm7MojM4716DwoSaqwQpaH4DvZvbBTGcoDNTiG9Y2lByXZYFEs9WRkS5tLl96IrF1Q==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -4663,6 +4717,9 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -5542,6 +5599,10 @@ packages: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + node-gyp@11.5.0: resolution: {integrity: sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -5992,6 +6053,9 @@ packages: resolve-pathname@3.0.0: resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} @@ -6428,6 +6492,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -7627,6 +7696,24 @@ snapshots: dependencies: css-tree: 3.2.1 + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + '@codemirror/autocomplete@6.20.1': dependencies: '@codemirror/language': 6.12.2 @@ -8643,11 +8730,11 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: glob: 13.0.6 react-docgen-typescript: 2.4.0(typescript@5.9.3) - vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2) + vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) optionalDependencies: typescript: 5.9.3 @@ -9135,6 +9222,10 @@ snapshots: '@pkgr/core@0.2.9': {} + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@protobuf-ts/grpc-transport@2.11.1(@grpc/grpc-js@1.14.3)': dependencies: '@grpc/grpc-js': 1.14.3 @@ -9295,24 +9386,24 @@ snapshots: - react - react-dom - '@storybook/builder-vite@10.2.14(esbuild@0.27.3)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2))': + '@storybook/builder-vite@10.2.14(esbuild@0.27.3)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@storybook/csf-plugin': 10.2.14(esbuild@0.27.3)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2)) + '@storybook/csf-plugin': 10.2.14(esbuild@0.27.3)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) storybook: 10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2) + vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.2.14(esbuild@0.27.3)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2))': + '@storybook/csf-plugin@10.2.14(esbuild@0.27.3)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: storybook: 10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.3 - vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2) + vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) '@storybook/global@5.0.0': {} @@ -9327,11 +9418,11 @@ snapshots: react-dom: 19.2.4(react@19.2.4) storybook: 10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/react-vite@10.2.14(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2))': + '@storybook/react-vite@10.2.14(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) '@rollup/pluginutils': 5.3.0 - '@storybook/builder-vite': 10.2.14(esbuild@0.27.3)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2)) + '@storybook/builder-vite': 10.2.14(esbuild@0.27.3)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) '@storybook/react': 10.2.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) empathic: 2.0.0 magic-string: 0.30.21 @@ -9341,7 +9432,7 @@ snapshots: resolve: 1.22.11 storybook: 10.2.14(@testing-library/dom@10.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tsconfig-paths: 4.2.0 - vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2) + vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - esbuild - rollup @@ -9893,11 +9984,11 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-react-swc@4.3.0(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2))': + '@vitejs/plugin-react-swc@4.3.0(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 '@swc/core': 1.15.18 - vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2) + vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@swc/helpers' @@ -10336,6 +10427,22 @@ snapshots: caniuse-lite@1.0.30001776: {} + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.4: + optionalDependencies: + cbor-extract: 2.2.2 + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -10898,7 +11005,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-vite@5.0.0(@swc/core@1.15.18)(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2)): + electron-vite@5.0.0(@swc/core@1.15.18)(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) @@ -10906,7 +11013,7 @@ snapshots: esbuild: 0.25.12 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2) + vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) optionalDependencies: '@swc/core': 1.15.18 transitivePeerDependencies: @@ -11322,6 +11429,10 @@ snapshots: get-stream@6.0.1: {} + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -12376,6 +12487,11 @@ snapshots: node-forge@1.4.0: {} + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.0.4 + optional: true + node-gyp@11.5.0: dependencies: env-paths: 2.2.1 @@ -12901,6 +13017,8 @@ snapshots: resolve-pathname@3.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.11: dependencies: is-core-module: 2.16.1 @@ -13351,6 +13469,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -13488,11 +13613,11 @@ snapshots: transitivePeerDependencies: - rollup - vite-plugin-wasm@3.5.0(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2)): + vite-plugin-wasm@3.5.0(vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: - vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2) + vite: 8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) - vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(yaml@2.8.2): + vite@8.0.0(@types/node@24.10.9)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@oxc-project/runtime': 0.115.0 lightningcss: 1.32.0 @@ -13505,6 +13630,7 @@ snapshots: esbuild: 0.27.3 fsevents: 2.3.3 jiti: 2.6.1 + tsx: 4.21.0 yaml: 2.8.2 vscode-languageserver-textdocument@1.0.12: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c1c3a388a3d..4db7b668aba 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,8 +1,10 @@ packages: - web/packages/* - e/web/* + - e2e onlyBuiltDependencies: - '@swc/core' + - cbor-extract - electron - esbuild - msw