mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 13:39:47 +00:00
d2ac07bccf
Since 22 October the E2E jobs often time out installing browsers before any test runs: 29 installs failed that day and 30 on 23 October, against none in the week before. Each job downloads about 119 MB of Ubuntu packages for Chromium and WebKit from the runner's Azure mirror, and some GitHub-hosted runners now get tens of kB/s from it (actions/runner-images#14823 and #15040). The same package set on the same image versions installed in under a minute before then. The three browser jobs now restore the newest saved .deb set with actions/cache/restore and admit it through scripts/browser_os_packages.py. apt reuses a file already in its archive directory after checking only its size, so the helper refreshes the index and installs a private copy of a cached file only when that copy's SHA256 matches a signed index stanza for the package, version and architecture its filename names. After a successful install, and before any journey can fail the job, the helper rebuilds the cache from apt's archives and the job saves it under a run-unique key only when the set changed, so Ubuntu updates refresh the cache and an empty or unchanged set is never saved. The transport bounds and install budgets are unchanged, and the jobs hold no secrets or write tokens, as the workflow trust contract requires for caches.
726 lines
31 KiB
YAML
726 lines
31 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 credential lifetime
|
|
working-directory: tests/integration
|
|
run: node scripts/fixture-auth.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: 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 the completed stable failure before observational work can
|
|
# reach the job deadline. Probation must not take its receipts with it.
|
|
- name: Upload Playwright report
|
|
if: ${{ !cancelled() && steps.stable.outcome == 'failure' }}
|
|
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 == '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' }}
|
|
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 == '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)"
|