mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 13:39:47 +00:00
8a529ac4e9
Include the organization request-bound, owned-cleanup and native backend diagnostic controls in the existing fixture validation step. These tests must protect future changes in recurring CI as well as the focused native proof. Contract-Neutral: CI wiring only: execute existing fixture controls in the existing validation job. Source-bound image distribution, fixture behavior and all runtime and test deadlines are unchanged.
735 lines
32 KiB
YAML
735 lines
32 KiB
YAML
name: Core E2E Tests
|
|
|
|
on:
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
- pulse/v6-release
|
|
- 'release/v*'
|
|
paths:
|
|
- 'frontend-modern/**'
|
|
- 'internal/**'
|
|
- 'tests/integration/**'
|
|
- 'Dockerfile'
|
|
- '.github/workflows/test-e2e.yml'
|
|
- 'scripts/playwright-apt.conf'
|
|
- 'scripts/tests/test_playwright_acquisition.py'
|
|
- 'scripts/browser_os_packages.py'
|
|
- 'scripts/tests/test_browser_os_packages.py'
|
|
- 'scripts/e2e_test_images.py'
|
|
- 'scripts/tests/test_e2e_test_images.py'
|
|
- 'scripts/collect_e2e_container_logs.py'
|
|
- 'scripts/tests/test_e2e_container_logs.py'
|
|
- 'scripts/ci_browser_proof_only.py'
|
|
- 'scripts/tests/test_ci_browser_proof_only.py'
|
|
push:
|
|
branches:
|
|
- main
|
|
- master
|
|
- pulse/v6-release
|
|
- 'release/v*'
|
|
paths:
|
|
- 'frontend-modern/**'
|
|
- 'internal/**'
|
|
- 'tests/integration/**'
|
|
- 'Dockerfile'
|
|
- '.github/workflows/test-e2e.yml'
|
|
- 'scripts/playwright-apt.conf'
|
|
- 'scripts/tests/test_playwright_acquisition.py'
|
|
- 'scripts/browser_os_packages.py'
|
|
- 'scripts/tests/test_browser_os_packages.py'
|
|
- 'scripts/e2e_test_images.py'
|
|
- 'scripts/tests/test_e2e_test_images.py'
|
|
- 'scripts/collect_e2e_container_logs.py'
|
|
- 'scripts/tests/test_e2e_container_logs.py'
|
|
- 'scripts/ci_browser_proof_only.py'
|
|
- 'scripts/tests/test_ci_browser_proof_only.py'
|
|
workflow_dispatch:
|
|
|
|
# Let branch runs finish so busy main and release branches retain verdicts.
|
|
# A newer PR commit supersedes its previous validation, so do not queue the
|
|
# replacement behind obsolete checks. Keep this aligned with Build and Test.
|
|
concurrency:
|
|
group: e2e-${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
scope:
|
|
name: Detect browser receipt-only changes
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 5
|
|
outputs:
|
|
proof_only: ${{ steps.diff.outputs.proof_only }}
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
|
|
- name: Check shipped docs before dependency work
|
|
run: python3 scripts/check_docs_mirror.py
|
|
|
|
- name: Validate browser receipt-only CI admission
|
|
run: python3 scripts/tests/test_ci_browser_proof_only.py
|
|
|
|
- name: Read the complete tree diff
|
|
id: diff
|
|
env:
|
|
DIFF_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
|
|
DIFF_HEAD: ${{ github.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
proof_only=$(python3 scripts/ci_browser_proof_only.py --base "$DIFF_BASE" --head "$DIFF_HEAD")
|
|
echo "proof_only=$proof_only" >> "$GITHUB_OUTPUT"
|
|
|
|
tier-selection:
|
|
name: Validate E2E tier selection
|
|
needs: scope
|
|
if: needs.scope.outputs.proof_only != 'true'
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 10
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# Fail before installs, Docker builds or the eight-shard fan-out.
|
|
- name: Check shipped docs before dependency work
|
|
run: python3 scripts/check_docs_mirror.py
|
|
|
|
- name: Validate shared E2E image admission
|
|
run: python3 scripts/tests/test_e2e_test_images.py
|
|
|
|
- name: Validate bounded container diagnostic retention
|
|
run: python3 scripts/tests/test_e2e_container_logs.py
|
|
|
|
- name: Validate browser dependency acquisition policy
|
|
run: python3 scripts/tests/test_playwright_acquisition.py
|
|
|
|
- name: Validate verified browser OS package reuse
|
|
run: python3 scripts/tests/test_browser_os_packages.py
|
|
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
with:
|
|
node-version: '24'
|
|
cache: 'npm'
|
|
cache-dependency-path: tests/integration/package-lock.json
|
|
|
|
- name: Validate read-only auth renderer diagnostics
|
|
working-directory: tests/integration
|
|
run: node --test scripts/capture-auth-renderer.test.mjs
|
|
|
|
- name: Validate telemetry schema version parity
|
|
working-directory: tests/integration
|
|
run: |
|
|
npm run check:telemetry-schema-version
|
|
node --test scripts/check-telemetry-schema-version.test.mjs
|
|
|
|
- name: Install integration dependencies
|
|
working-directory: tests/integration
|
|
run: npm ci
|
|
|
|
- name: Validate stable/probation project accounting
|
|
working-directory: tests/integration
|
|
run: npm run check:e2e-tiers
|
|
|
|
- name: Validate stable failure reporting
|
|
working-directory: tests/integration
|
|
run: node --test scripts/report-stable-e2e-failures.test.mjs
|
|
|
|
- name: Validate fixture authentication and organization lifecycle
|
|
working-directory: tests/integration
|
|
run: >-
|
|
node --test scripts/fixture-auth.test.mjs
|
|
scripts/organization-fixtures.test.mjs
|
|
scripts/managed-local-backend.test.mjs
|
|
|
|
offline-org-provisioning:
|
|
name: Offline Organization provisioning
|
|
needs: scope
|
|
if: needs.scope.outputs.proof_only != 'true'
|
|
# Fixture acceptance only, not quarantine promotion or private RBAC proof.
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 25
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
# Fail before installs, Docker builds or the eight-shard fan-out.
|
|
- name: Check shipped docs before dependency work
|
|
run: python3 scripts/check_docs_mirror.py
|
|
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
with:
|
|
node-version: '24'
|
|
cache: 'npm'
|
|
cache-dependency-path: |
|
|
tests/integration/package-lock.json
|
|
frontend-modern/package-lock.json
|
|
internal/cloudcp/portal/frontend/package-lock.json
|
|
- name: Set up Go
|
|
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
|
with:
|
|
go-version-file: go.mod
|
|
# Apply only to this disposable hosted runner, never the maintainer host.
|
|
# Keep the provider's repositories and apt authentication unchanged.
|
|
- name: Bound browser OS dependency acquisition
|
|
run: sudo install -m 0644 scripts/playwright-apt.conf /etc/apt/apt.conf.d/99pulse-playwright-acquire
|
|
|
|
# The browser OS packages (about 120 MB for Chromium plus WebKit) come
|
|
# from the runner's Azure Ubuntu mirror, which since 22 October 2026
|
|
# serves some runners at tens of kB/s (actions/runner-images#14823 and
|
|
# #15040). Reuse the newest saved package set. apt would accept a cached
|
|
# file on size alone, so scripts/browser_os_packages.py admits only files
|
|
# whose SHA256 matches the freshly fetched signed index.
|
|
- name: Restore browser OS packages
|
|
id: browser-os-packages
|
|
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: ~/.cache/pulse-browser-os-packages
|
|
key: browser-os-packages-${{ runner.os }}-chromium-${{ hashFiles('tests/integration/package-lock.json') }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
restore-keys: |
|
|
browser-os-packages-${{ runner.os }}-chromium-${{ hashFiles('tests/integration/package-lock.json') }}-
|
|
browser-os-packages-${{ runner.os }}-chromium-
|
|
|
|
- name: Admit verified browser OS packages
|
|
run: python3 scripts/browser_os_packages.py seed "$HOME/.cache/pulse-browser-os-packages"
|
|
|
|
- name: Install locked dependencies
|
|
run: |
|
|
npm ci --prefix tests/integration
|
|
npm ci --prefix frontend-modern
|
|
npm ci --prefix internal/cloudcp/portal/frontend
|
|
|
|
- name: Install Playwright browser and OS dependencies
|
|
timeout-minutes: 10
|
|
working-directory: tests/integration
|
|
run: npx playwright install --with-deps chromium
|
|
|
|
- name: Collect browser OS packages
|
|
id: browser-os-collect
|
|
run: python3 scripts/browser_os_packages.py collect "$HOME/.cache/pulse-browser-os-packages"
|
|
|
|
- name: Save browser OS packages
|
|
if: steps.browser-os-collect.outputs.changed == 'true'
|
|
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: ~/.cache/pulse-browser-os-packages
|
|
key: ${{ steps.browser-os-packages.outputs.cache-primary-key }}
|
|
- name: Validate offline issuer boundaries
|
|
working-directory: tests/integration
|
|
run: >-
|
|
node --test scripts/offline-license-issuer.test.mjs
|
|
scripts/with-offline-entitlements.test.mjs
|
|
scripts/entitlement-bootstrap.test.mjs
|
|
- name: Prove authenticated default and created-org activation
|
|
working-directory: tests/integration
|
|
env:
|
|
PULSE_E2E_USE_LOCAL_BACKEND: 'true'
|
|
PULSE_E2E_SKIP_PLAYWRIGHT_INSTALL: 'true'
|
|
run: >-
|
|
node scripts/with-offline-entitlements.mjs node scripts/run-playwright.mjs
|
|
--config=playwright.multi-tenant-diagnostic.config.ts
|
|
--grep 'Scenario 1:|create, update, member manage' --workers=1 --retries=0
|
|
# The same issuer's Pro key, pasted into Settings the way a customer
|
|
# applies a purchase, then cleared back to Community.
|
|
- name: Prove Settings licence activation and clear
|
|
working-directory: tests/integration
|
|
env:
|
|
PULSE_E2E_USE_LOCAL_BACKEND: 'true'
|
|
PULSE_E2E_SKIP_PLAYWRIGHT_INSTALL: 'true'
|
|
PULSE_E2E_REPORT_DIR: playwright-report/license-activation
|
|
PULSE_E2E_RESULTS_DIR: test-results/license-activation
|
|
run: >-
|
|
node scripts/with-offline-entitlements.mjs node scripts/run-playwright.mjs
|
|
tests/10-v6-license-activation.spec.ts --project=chromium --workers=1 --retries=0
|
|
- name: Upload offline provisioning failure report
|
|
if: failure()
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: offline-org-provisioning-report
|
|
path: |
|
|
tests/integration/playwright-report/multi-tenant-diagnostic/
|
|
tests/integration/playwright-report/license-activation/
|
|
retention-days: 3
|
|
|
|
test-images:
|
|
name: Build source-bound E2E images once
|
|
needs: tier-selection
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 30
|
|
outputs:
|
|
artifact-id: ${{ steps.images.outputs.artifact-id }}
|
|
manifest-sha256: ${{ steps.bundle.outputs.manifest-sha256 }}
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# Same provider, pins and e2e_runtime contract as the former nine builds.
|
|
# A provider refusal remains terminal: no mirror, login or pull retry.
|
|
- name: Build and bind the test images
|
|
id: bundle
|
|
run: python3 scripts/e2e_test_images.py build "$RUNNER_TEMP/pulse-e2e-images"
|
|
|
|
- name: Upload this run's exact test images
|
|
id: images
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: e2e-test-images-${{ github.sha }}-${{ github.run_attempt }}
|
|
path: |
|
|
${{ runner.temp }}/pulse-e2e-images/manifest.json
|
|
${{ runner.temp }}/pulse-e2e-images/images.tar.gz
|
|
retention-days: 1
|
|
compression-level: 0
|
|
if-no-files-found: error
|
|
|
|
e2e:
|
|
name: Playwright Core E2E (shard ${{ matrix.shard }}/8)
|
|
needs: [tier-selection, test-images]
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 60
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
shard: [1, 2, 3, 4, 5, 6, 7, 8]
|
|
|
|
env:
|
|
PULSE_E2E_SEED_IMAGE: pulse-e2e-seed:test
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
with:
|
|
node-version: '24'
|
|
cache: 'npm'
|
|
cache-dependency-path: tests/integration/package-lock.json
|
|
|
|
# Apply only to this disposable hosted runner, never the maintainer host.
|
|
# Keep the provider's repositories and apt authentication unchanged.
|
|
- name: Bound browser OS dependency acquisition
|
|
run: sudo install -m 0644 scripts/playwright-apt.conf /etc/apt/apt.conf.d/99pulse-playwright-acquire
|
|
|
|
# The browser OS packages (about 120 MB for Chromium plus WebKit) come
|
|
# from the runner's Azure Ubuntu mirror, which since 22 October 2026
|
|
# serves some runners at tens of kB/s (actions/runner-images#14823 and
|
|
# #15040). Reuse the newest saved package set. apt would accept a cached
|
|
# file on size alone, so scripts/browser_os_packages.py admits only files
|
|
# whose SHA256 matches the freshly fetched signed index.
|
|
- name: Restore browser OS packages
|
|
id: browser-os-packages
|
|
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: ~/.cache/pulse-browser-os-packages
|
|
key: browser-os-packages-${{ runner.os }}-chromium-webkit-${{ hashFiles('tests/integration/package-lock.json') }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
restore-keys: |
|
|
browser-os-packages-${{ runner.os }}-chromium-webkit-${{ hashFiles('tests/integration/package-lock.json') }}-
|
|
browser-os-packages-${{ runner.os }}-chromium-webkit-
|
|
|
|
- name: Admit verified browser OS packages
|
|
run: python3 scripts/browser_os_packages.py seed "$HOME/.cache/pulse-browser-os-packages"
|
|
|
|
- name: Install Playwright dependencies
|
|
# Chromium + WebKit requested 116 MB of authenticated OS packages in
|
|
# the retained setup failure. Active downloads near 90 kB/s need over
|
|
# 20 minutes before installation/browser downloads, not ten. Keep the
|
|
# 60-minute job and every test deadline unchanged; apt's transport
|
|
# timeouts and incomplete-index refusal still bound stalled requests.
|
|
timeout-minutes: 30
|
|
working-directory: tests/integration
|
|
run: |
|
|
npm ci
|
|
npx playwright install --with-deps chromium webkit
|
|
|
|
- name: Collect browser OS packages
|
|
id: browser-os-collect
|
|
run: python3 scripts/browser_os_packages.py collect "$HOME/.cache/pulse-browser-os-packages"
|
|
|
|
- name: Save browser OS packages
|
|
if: steps.browser-os-collect.outputs.changed == 'true'
|
|
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: ~/.cache/pulse-browser-os-packages
|
|
key: ${{ steps.browser-os-packages.outputs.cache-primary-key }}
|
|
|
|
- name: Install Pulse Account frontend dependencies
|
|
working-directory: internal/cloudcp/portal/frontend
|
|
run: npm ci
|
|
|
|
- name: Validate native settings scroll geometry
|
|
if: matrix.shard == 1
|
|
working-directory: tests/integration
|
|
run: node --test scripts/settings-scroll-audit.test.mjs
|
|
|
|
- name: Download this run's exact test images
|
|
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
with:
|
|
artifact-ids: ${{ needs.test-images.outputs.artifact-id }}
|
|
path: ${{ runner.temp }}/pulse-e2e-images
|
|
merge-multiple: true
|
|
|
|
- name: Admit the source-bound test images
|
|
env:
|
|
PULSE_E2E_MANIFEST_SHA256: ${{ needs.test-images.outputs.manifest-sha256 }}
|
|
run: python3 scripts/e2e_test_images.py load "$RUNNER_TEMP/pulse-e2e-images"
|
|
|
|
- name: Start test containers
|
|
working-directory: tests/integration
|
|
env:
|
|
PULSE_E2E_BOOTSTRAP_TOKEN: 0123456789abcdef0123456789abcdef0123456789abcdef
|
|
PULSE_E2E_SKIP_PLAYWRIGHT_INSTALL: "true"
|
|
PULSE_MULTI_TENANT_ENABLED: "true"
|
|
run: node scripts/pretest.mjs
|
|
|
|
# One shared fixture fault previously consumed 47 minutes of repeated
|
|
# setup/login failures before probation hit the job limit. Admit the
|
|
# same authenticated inventory and history once, before either tier.
|
|
- name: Admit the default mock runtime
|
|
id: fixture
|
|
timeout-minutes: 8
|
|
working-directory: tests/integration
|
|
env:
|
|
PULSE_E2E_BOOTSTRAP_TOKEN: 0123456789abcdef0123456789abcdef0123456789abcdef
|
|
PULSE_E2E_REQUIRE_DEFAULT_MOCK_READY: "true"
|
|
PULSE_MULTI_TENANT_ENABLED: "true"
|
|
run: npx playwright test --config=fixture-readiness.config.ts
|
|
|
|
- name: Upload failed fixture admission
|
|
if: ${{ !cancelled() && steps.fixture.outcome == 'failure' }}
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: fixture-readiness-shard-${{ matrix.shard }}
|
|
path: |
|
|
tests/integration/playwright-report-readiness/
|
|
tests/integration/test-results-readiness/
|
|
retention-days: 7
|
|
|
|
# Two-tier run (see PROBATION_SPECS in tests/integration/e2e-tiering.mjs
|
|
# for the tier mechanism and the promotion/demotion rule). The stable tier
|
|
# is the gate; the probation tier reuses the same containers but reports
|
|
# through continue-on-error so a probation flake cannot paint main red.
|
|
- name: Run stable-tier E2E suite
|
|
id: stable
|
|
working-directory: tests/integration
|
|
env:
|
|
PULSE_E2E_BOOTSTRAP_TOKEN: 0123456789abcdef0123456789abcdef0123456789abcdef
|
|
PULSE_E2E_SKIP_DOCKER: "true"
|
|
PULSE_E2E_SKIP_PLAYWRIGHT_INSTALL: "true"
|
|
PULSE_E2E_PERF: "1"
|
|
PULSE_E2E_REQUIRE_DEFAULT_MOCK_READY: "true"
|
|
PULSE_E2E_TIER: "stable"
|
|
PULSE_MULTI_TENANT_ENABLED: "true"
|
|
run: npm test -- --shard=${{ matrix.shard }}/8
|
|
|
|
# Playwright's report artifacts preserve the full forensic record, but
|
|
# artifact bodies and job logs require an authenticated GitHub session.
|
|
# Project the bounded failed testcase identity into ordinary check
|
|
# annotations so a red stable gate is diagnosable from the API without
|
|
# rerunning, demoting, or skipping the failure.
|
|
- name: Report stable-tier failures
|
|
if: ${{ !cancelled() && steps.stable.outcome == 'failure' }}
|
|
working-directory: tests/integration
|
|
run: node scripts/report-stable-e2e-failures.mjs test-results/junit.xml
|
|
|
|
# Preserve failed attempts, including a passing first retry, before
|
|
# probation can reach the job deadline. Retry traces and failure images
|
|
# are diagnostic bytes even when the suite's final outcome is success.
|
|
- name: Upload Playwright report
|
|
if: ${{ !cancelled() && (steps.stable.outcome == 'failure' || hashFiles('tests/integration/test-results/**/trace.zip', 'tests/integration/test-results/**/test-failed-*.png') != '') }}
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: playwright-report-shard-${{ matrix.shard }}
|
|
path: tests/integration/playwright-report/
|
|
retention-days: 3
|
|
|
|
# The HTML report already contains screenshots, videos and traces.
|
|
# Keep only the separate machine-readable result here.
|
|
- name: Upload JUnit results
|
|
if: ${{ !cancelled() && (steps.stable.outcome == 'success' || steps.stable.outcome == 'failure') }}
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: junit-results-shard-${{ matrix.shard }}
|
|
path: tests/integration/test-results/junit.xml
|
|
retention-days: 3
|
|
|
|
# Runs even when the stable tier failed (its data still counts toward
|
|
# promotion), but not when the stable tier was skipped — that means the
|
|
# environment never came up and every probation spec would fail on it.
|
|
# Pull requests skip it. It cannot gate a PR, and the promotion rule in
|
|
# e2e-tiering.mjs counts only main runs, so on a PR it only held each
|
|
# of the eight shard runners for about eight more minutes.
|
|
- name: Run probation-tier E2E suite (non-gating)
|
|
id: probation
|
|
if: ${{ !cancelled() && github.event_name != 'pull_request' && steps.stable.conclusion != 'skipped' }}
|
|
continue-on-error: true
|
|
# A broken probation surface can otherwise consume the entire 60m job
|
|
# budget through retries and cancel the job before continue-on-error
|
|
# can make the observational result non-gating. Playwright's own
|
|
# global timeout preserves its reports; the step timeout is a backstop
|
|
# for runner/process failures outside Playwright.
|
|
timeout-minutes: 12
|
|
working-directory: tests/integration
|
|
env:
|
|
PULSE_E2E_BOOTSTRAP_TOKEN: 0123456789abcdef0123456789abcdef0123456789abcdef
|
|
PULSE_E2E_SKIP_DOCKER: "true"
|
|
PULSE_E2E_SKIP_PLAYWRIGHT_INSTALL: "true"
|
|
PULSE_E2E_PERF: "1"
|
|
PULSE_E2E_REQUIRE_DEFAULT_MOCK_READY: "true"
|
|
PULSE_E2E_TIER: "probation"
|
|
PULSE_E2E_REPORT_DIR: playwright-report-probation
|
|
PULSE_E2E_RESULTS_DIR: test-results-probation
|
|
PULSE_MULTI_TENANT_ENABLED: "true"
|
|
run: >-
|
|
npm test -- --shard=${{ matrix.shard }}/8 --pass-with-no-tests
|
|
--max-failures=5 --global-timeout=600000
|
|
|
|
- name: Report probation-tier outcome
|
|
if: ${{ !cancelled() && steps.probation.outcome != 'skipped' }}
|
|
run: |
|
|
if [ "${{ steps.probation.outcome }}" = "failure" ]; then
|
|
echo "⚠️ Probation tier FAILED on shard ${{ matrix.shard }} (non-gating; see the playwright-report-probation artifact). A probation spec's 10-consecutive-green promotion count restarts." >> "$GITHUB_STEP_SUMMARY"
|
|
else
|
|
echo "Probation tier passed on shard ${{ matrix.shard }} (non-gating)." >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
|
|
- name: Collect container logs
|
|
if: always()
|
|
timeout-minutes: 3
|
|
run: python3 scripts/collect_e2e_container_logs.py "$RUNNER_TEMP/pulse-e2e-container-logs"
|
|
|
|
# Preserve all diagnostic bytes, including partial/failed reads, while
|
|
# the combined Actions log stays small enough to inspect its verdicts.
|
|
- name: Upload complete container diagnostics
|
|
if: always()
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: e2e-container-logs-shard-${{ matrix.shard }}-${{ github.sha }}-${{ github.run_attempt }}
|
|
path: ${{ runner.temp }}/pulse-e2e-container-logs/
|
|
retention-days: 3
|
|
if-no-files-found: error
|
|
|
|
# continue-on-error keeps the job green on a probation failure, so
|
|
# failure() never fires for these; key off the step outcome instead.
|
|
- name: Upload probation Playwright report
|
|
if: ${{ !cancelled() && (steps.probation.outcome == 'failure' || hashFiles('tests/integration/test-results-probation/**/trace.zip', 'tests/integration/test-results-probation/**/test-failed-*.png') != '') }}
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: playwright-report-probation-shard-${{ matrix.shard }}
|
|
path: tests/integration/playwright-report-probation/
|
|
retention-days: 3
|
|
|
|
- name: Upload probation JUnit results
|
|
if: ${{ !cancelled() && (steps.probation.outcome == 'success' || steps.probation.outcome == 'failure') }}
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: junit-results-probation-shard-${{ matrix.shard }}
|
|
path: tests/integration/test-results-probation/junit.xml
|
|
retention-days: 3
|
|
|
|
agent-registration:
|
|
name: Agent registration lifecycle
|
|
needs: [tier-selection, test-images]
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 30
|
|
env:
|
|
PULSE_E2E_SEED_IMAGE: pulse-e2e-seed:test
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# Fail before installs, Docker builds or the eight-shard fan-out.
|
|
- name: Check shipped docs before dependency work
|
|
run: python3 scripts/check_docs_mirror.py
|
|
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
with:
|
|
node-version: '24'
|
|
package-manager-cache: false
|
|
|
|
# Apply only to this disposable hosted runner, never the maintainer host.
|
|
# Keep the provider's repositories and apt authentication unchanged.
|
|
- name: Bound browser OS dependency acquisition
|
|
run: sudo install -m 0644 scripts/playwright-apt.conf /etc/apt/apt.conf.d/99pulse-playwright-acquire
|
|
|
|
# The browser OS packages (about 120 MB for Chromium plus WebKit) come
|
|
# from the runner's Azure Ubuntu mirror, which since 22 October 2026
|
|
# serves some runners at tens of kB/s (actions/runner-images#14823 and
|
|
# #15040). Reuse the newest saved package set. apt would accept a cached
|
|
# file on size alone, so scripts/browser_os_packages.py admits only files
|
|
# whose SHA256 matches the freshly fetched signed index.
|
|
- name: Restore browser OS packages
|
|
id: browser-os-packages
|
|
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: ~/.cache/pulse-browser-os-packages
|
|
key: browser-os-packages-${{ runner.os }}-chromium-${{ hashFiles('tests/integration/package-lock.json') }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
restore-keys: |
|
|
browser-os-packages-${{ runner.os }}-chromium-${{ hashFiles('tests/integration/package-lock.json') }}-
|
|
browser-os-packages-${{ runner.os }}-chromium-
|
|
|
|
- name: Admit verified browser OS packages
|
|
run: python3 scripts/browser_os_packages.py seed "$HOME/.cache/pulse-browser-os-packages"
|
|
|
|
- name: Install Playwright dependencies
|
|
timeout-minutes: 10
|
|
working-directory: tests/integration
|
|
run: |
|
|
npm ci
|
|
npx playwright install --with-deps chromium
|
|
|
|
- name: Collect browser OS packages
|
|
id: browser-os-collect
|
|
run: python3 scripts/browser_os_packages.py collect "$HOME/.cache/pulse-browser-os-packages"
|
|
|
|
- name: Save browser OS packages
|
|
if: steps.browser-os-collect.outputs.changed == 'true'
|
|
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: ~/.cache/pulse-browser-os-packages
|
|
key: ${{ steps.browser-os-packages.outputs.cache-primary-key }}
|
|
|
|
- name: Install Pulse Account frontend dependencies
|
|
working-directory: internal/cloudcp/portal/frontend
|
|
run: npm ci
|
|
|
|
- name: Download this run's exact test images
|
|
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
with:
|
|
artifact-ids: ${{ needs.test-images.outputs.artifact-id }}
|
|
path: ${{ runner.temp }}/pulse-e2e-images
|
|
merge-multiple: true
|
|
|
|
- name: Admit the source-bound test images
|
|
env:
|
|
PULSE_E2E_MANIFEST_SHA256: ${{ needs.test-images.outputs.manifest-sha256 }}
|
|
run: python3 scripts/e2e_test_images.py load "$RUNNER_TEMP/pulse-e2e-images"
|
|
|
|
- name: Start non-mock test container
|
|
working-directory: tests/integration
|
|
env:
|
|
PULSE_BASE_URL: http://localhost:7655
|
|
PULSE_E2E_BOOTSTRAP_TOKEN: 0123456789abcdef0123456789abcdef0123456789abcdef
|
|
PULSE_E2E_SKIP_PLAYWRIGHT_INSTALL: "true"
|
|
PULSE_MOCK_MODE: "false"
|
|
PULSE_MULTI_TENANT_ENABLED: "true"
|
|
run: node scripts/pretest.mjs
|
|
|
|
- name: Run agent registration lifecycle
|
|
working-directory: tests/integration
|
|
env:
|
|
PULSE_BASE_URL: http://localhost:7655
|
|
PULSE_E2E_AGENT_JOURNEY: "true"
|
|
PULSE_E2E_BOOTSTRAP_TOKEN: 0123456789abcdef0123456789abcdef0123456789abcdef
|
|
PULSE_E2E_SKIP_DOCKER: "true"
|
|
PULSE_E2E_SKIP_PLAYWRIGHT_INSTALL: "true"
|
|
PULSE_MOCK_MODE: "false"
|
|
PULSE_MULTI_TENANT_ENABLED: "true"
|
|
run: npx playwright test tests/journeys/04-agent-install-registration.spec.ts --project=chromium
|
|
|
|
- name: Collect container logs
|
|
if: always()
|
|
timeout-minutes: 3
|
|
run: python3 scripts/collect_e2e_container_logs.py "$RUNNER_TEMP/pulse-e2e-container-logs" --registration
|
|
|
|
- name: Upload complete container diagnostics
|
|
if: always()
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: e2e-container-logs-agent-registration-${{ github.sha }}-${{ github.run_attempt }}
|
|
path: ${{ runner.temp }}/pulse-e2e-container-logs/
|
|
retention-days: 3
|
|
if-no-files-found: error
|
|
|
|
- name: Upload Playwright report
|
|
if: failure()
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: playwright-report-agent-registration
|
|
path: tests/integration/playwright-report/
|
|
retention-days: 3
|
|
|
|
- name: Upload test videos and screenshots
|
|
if: failure()
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: test-failures-agent-registration
|
|
path: tests/integration/test-results/
|
|
retention-days: 3
|
|
|
|
e2e-verdict:
|
|
name: E2E verdict
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 5
|
|
needs:
|
|
- scope
|
|
- tier-selection
|
|
- test-images
|
|
- e2e
|
|
- agent-registration
|
|
if: always()
|
|
steps:
|
|
# The e2e shards fail only on stable-tier failures: the probation tier
|
|
# runs behind continue-on-error inside each shard, so its failures are
|
|
# reported (shard summaries + probation artifacts) without reaching
|
|
# this verdict. Mechanism and promotion rule: PROBATION_SPECS in
|
|
# tests/integration/e2e-tiering.mjs.
|
|
- name: Check E2E results
|
|
env:
|
|
PROOF_ONLY: ${{ needs.scope.outputs.proof_only }}
|
|
run: |
|
|
if [ "${{ needs.scope.result }}" != "success" ]; then
|
|
echo "E2E change-scope admission did not pass."
|
|
exit 1
|
|
fi
|
|
if [ "$PROOF_ONLY" = "true" ]; then
|
|
# A skip is not a test pass. Require the expected all-skipped
|
|
# topology; never use a receipt to clear a failed runtime job.
|
|
for result in "${{ needs.tier-selection.result }}" "${{ needs.test-images.result }}" "${{ needs.e2e.result }}" "${{ needs.agent-registration.result }}"; do
|
|
if [ "$result" != "skipped" ]; then
|
|
echo "Unexpected runtime result for a browser receipt-only diff: $result"
|
|
exit 1
|
|
fi
|
|
done
|
|
echo "Browser receipt-only tree diff; runtime E2E is not applicable. Canonical Governance still validates evidence."
|
|
exit 0
|
|
fi
|
|
if [ "$PROOF_ONLY" != "false" ]; then
|
|
echo "E2E change scope is unknown; no runtime success is established."
|
|
exit 1
|
|
fi
|
|
if [ "${{ needs.tier-selection.result }}" != "success" ]; then
|
|
echo "E2E tier selection is invalid (result: ${{ needs.tier-selection.result }})"
|
|
exit 1
|
|
fi
|
|
if [ "${{ needs.test-images.result }}" != "success" ]; then
|
|
echo "Source-bound E2E test images were not produced (result: ${{ needs.test-images.result }})"
|
|
exit 1
|
|
fi
|
|
if [ "${{ needs.e2e.result }}" != "success" ]; then
|
|
echo "Stable-tier E2E shards did not all pass (result: ${{ needs.e2e.result }})"
|
|
exit 1
|
|
fi
|
|
if [ "${{ needs.agent-registration.result }}" != "success" ]; then
|
|
echo "Agent registration lifecycle did not pass (result: ${{ needs.agent-registration.result }})"
|
|
exit 1
|
|
fi
|
|
echo "All stable-tier E2E shards and the agent registration lifecycle passed (probation tier is non-gating)"
|