Files
pulse/.github/workflows/test-e2e.yml
T
rcourtman d2ac07bccf Cache browser OS packages so slow apt mirrors stop E2E
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.
2026-10-23 18:02:39 +01:00

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)"