mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
dc68611fbf
* Default the events transport to WebSockets WORKFLOW_EVENTS_TRANSPORT=http is the opt-out. Only that exact value disables it, so a typo'd or empty value fails toward the default rather than quietly pinning a deployment to HTTP. The prerequisite the gate named for defaulting on is met: postEventFrameOverWs opens a client span per frame. What is still missing is Vercel's outgoing-requests view, which reads instrumented fetch calls rather than spans and so cannot show a transport that issues no request. Co-Authored-By: opencode <opencode@vercel.com> Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com> * docs: WORKFLOW_EVENTS_TRANSPORT defaults to ws Three places still documented http as the default. Each now states the opt-out is the exact value http, rather than leaving 'default: ws' to imply that anything non-ws disables it — the asymmetry is deliberate in the code and is the part a reader would otherwise get wrong. Also drops 'Experimental' from the Vercel World page: a setting that is on for everyone by default is not opt-in experimental, whatever else it is. Co-Authored-By: opencode <opencode@vercel.com> Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com> * Fix the gate's own unit tests for the flipped default Five tests in ws-transport.test.ts still encoded the opt-in semantics. Three were the isWsEventsTransportEnabled table itself; the other two (openWsChannel 'does nothing when the gate is off', and the channel release equivalent) relied on the suite's ambient unset environment meaning 'off', which it no longer does. Both now set http explicitly. Two tests in ws-transport-spans.test.ts asserted HTTP-side span behaviour the same way. The write one would have kept passing by falling through resolveWsTransport's null rather than because the gate was off - passing for the wrong reason, which is what this file exists to catch. Also makes the opt-out case-insensitive and trimmed. The gate is deliberately asymmetric - unrecognized values take the default - but that asymmetry should not extend to swallowing HTTP or ' http '. Whoever reaches for the escape hatch is plausibly mid-incident, and silently ignoring their opt-out over a capital letter is the same class of silent-wrong-transport bug this flip is meant to stop shipping. 554 tests pass in packages/world-vercel. Co-Authored-By: opencode <opencode@vercel.com> Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com> * ci: add a required forced-HTTP e2e lane (#3703) Flipping the default makes e2e-vercel-prod a WebSocket lane: it sets no WORKFLOW_EVENTS_TRANSPORT, and unset now means ws. Nothing in the file would exercise the HTTP events transport against a real deployment any more, so this is not additive coverage — it replaces coverage the flip silently removed. Unconditional and required rather than label-gated like the WS lane. HTTP is now the fallback, and the fallback is silent: resolveWsTransport returning null costs a write nothing and logs nothing, which is the shape of the durabench bug this stack came out of. Two apps rather than the WS lane's four, since every row is a real vercel deploy charged to every PR. nextjs-turbopack is the only fixture emitting OTEL spans, so it is the one that can show which transport actually ran; express covers the non-Next server path. Also corrects the WS lane's docblock, which claimed every other job exercises HTTP only. That stopped being true one commit ago. Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com> * Fail loudly when step_completed falls back to HTTP under a strict flag The WS e2e lane asserts that the transport is harmless, not that it is used: an event written over HTTP produces the same run outcome as one written over the socket, so the lane stayed green through the entire period the transport was silently demoted. WORKFLOW_INTERNAL_EVENTS_TRANSPORT_STRICT turns that one case into a failed run, and the WS lane now sets it. Scoped to step_completed alone, because most fallback is legitimate: run_created is written outside any invocation that opens a channel; run_started routinely lands before the channel is registered (34% HTTP on a healthy deployment); step_created and wait_created mostly fold into events.createBatch, which is not wired to the socket; and a write after the invocation released its claim falls back by design. step_completed is issued after a step body has run, and was 100% ws across every WS-enabled deployment measured on two SDK versions. The flag reads as off unless the value is exactly 1 or true - the opposite asymmetry from the transport gate, which treats an unrecognized value as on. That gate risks a deployment sitting quietly on the wrong transport; this one fails runs, and should not be acquired by a typo. Co-Authored-By: opencode <opencode@vercel.com> Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com> * ci: run the WS transport lane on every PR It was opt-in behind ws-transport-test because four real vercel deploys were too much to charge an unrelated PR for a transport that was off by default. Flipping the default expires that reasoning from both ends: the cost is no longer for someone else's feature, and this is now the only lane that asserts the socket carried the events. e2e-vercel-prod inherits the new default but checks nothing, so behind a label the average PR would move every deployment onto WebSockets with nothing verifying they were used. Drops WS_REQUIRED from the gate along with it. That existed only to let the lane be legitimately skipped on an unlabelled PR; with no label the lane is required unconditionally, like e2e-vercel-prod and the HTTP lane, and the skipped case is now a failure rather than a warning. Gate script extracted and run against the cases that matter: ws skipped fails on a standard PR, ws skipped fails under workflow-server-test, and all-green passes. Co-Authored-By: opencode <opencode@vercel.com> Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com> * ci: widen the HTTP transport lane to six server shapes Before the flip, HTTP was the default and all 28 e2e-vercel-prod lane-runs covered it. After the flip they cover WebSockets instead, and this lane is the entirety of the HTTP coverage - two apps was too thin for a transport that is still supported. Six, not the full 14, because every row is a real vercel deploy charged to every PR. Chosen by server shape rather than count: example (baseline), nextjs-turbopack (Next, and the only fixture emitting OTEL spans), vite (Vite SSR), express (Node req/res), nitro (h3, also covers nuxt) and hono (fetch-API Request/Response, a different mount shape from express). The rest duplicate a shape already covered; python is left out because it has no conformance gate and needs routes this suite does not serve. The first four match the WS lane's matrix on purpose, so the same fixture runs on both transports and a failure on one can be read against the other. Project ids and slugs are copied from e2e-vercel-prod and verified equal to it; both lanes already use the same team and token. Co-Authored-By: opencode <opencode@vercel.com> Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com> --------- Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
1793 lines
79 KiB
YAML
1793 lines
79 KiB
YAML
name: Tests
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
tags:
|
|
- "!*"
|
|
pull_request:
|
|
types:
|
|
- opened
|
|
- reopened
|
|
- synchronize
|
|
|
|
concurrency:
|
|
# Unique group for this workflow and branch
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
|
|
|
jobs:
|
|
ci-scope:
|
|
name: Detect CI Scope
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
fast-path: ${{ steps.scope.outputs.runtime-fast-path }}
|
|
validation-fast-path: ${{ steps.scope.outputs.validation-fast-path }}
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Classify changed files
|
|
id: scope
|
|
uses: ./.github/actions/detect-ci-scope
|
|
with:
|
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
# Phase 0: Update PR comment to show tests are running
|
|
pr-comment-start:
|
|
name: Create PR Comment
|
|
runs-on: ubuntu-latest
|
|
needs: ci-scope
|
|
if: github.event_name == 'pull_request' && needs.ci-scope.outputs.fast-path != 'true'
|
|
timeout-minutes: 2
|
|
|
|
steps:
|
|
- name: Find existing test comment
|
|
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
|
id: find-comment
|
|
with:
|
|
issue-number: ${{ github.event.pull_request.number }}
|
|
comment-author: 'github-actions[bot]'
|
|
body-includes: '<!-- e2e-test-results -->'
|
|
|
|
- name: Get existing comment body
|
|
if: steps.find-comment.outputs.comment-id != ''
|
|
id: get-comment
|
|
uses: actions/github-script@v7
|
|
env:
|
|
STARTED_AT: ${{ github.run_started_at }}
|
|
with:
|
|
script: |
|
|
const fs = require('fs');
|
|
const comment = await github.rest.issues.getComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: ${{ steps.find-comment.outputs.comment-id }}
|
|
});
|
|
const body = comment.data.body;
|
|
// Check if there are actual results (tables)
|
|
if (body.includes('|') && body.includes('Passed')) {
|
|
// Extract results section (everything after header)
|
|
const resultsSection = body
|
|
.replace(/<!-- e2e-test-results -->\n## 🧪 E2E Test Results\n\n> ⚠️ \*\*Results below are stale\*\*[^\n]*\n\n/g, '')
|
|
.replace(/<!-- e2e-test-results -->\n## 🧪 E2E Test Results\n\n/g, '')
|
|
.replace(/⏳ \*\*Tests are running\.\.\.\*\*\n\n---\n_Started at:[^_]*_\n\n---\n\n/g, '')
|
|
.replace(/⏳ \*\*Tests are running\.\.\.\*\*\n\n---\n_Started at:[^_]*_/g, '')
|
|
.trim();
|
|
if (resultsSection && resultsSection.includes('|')) {
|
|
// Write the full stale-banner message to disk and pass the
|
|
// path to the sticky-pull-request-comment action below.
|
|
// Inlining the previous results via `message:` blew past
|
|
// ARG_MAX once the matrix doubled (snapshot + replay).
|
|
const startedAt = process.env.STARTED_AT;
|
|
const message =
|
|
'<!-- e2e-test-results -->\n' +
|
|
'## 🧪 E2E Test Results\n\n' +
|
|
'> ⚠️ **Results below are stale** and not from the latest commit. This comment will be updated when CI completes on the latest run.\n\n' +
|
|
'⏳ **Tests are running...**\n\n' +
|
|
'---\n' +
|
|
`_Started at: ${startedAt}_\n\n` +
|
|
'---\n\n' +
|
|
resultsSection +
|
|
'\n';
|
|
const path = `${process.env.RUNNER_TEMP}/stale-comment.md`;
|
|
fs.writeFileSync(path, message);
|
|
core.setOutput('has-results', 'true');
|
|
core.setOutput('stale-comment-path', path);
|
|
} else {
|
|
core.setOutput('has-results', 'false');
|
|
}
|
|
} else {
|
|
core.setOutput('has-results', 'false');
|
|
}
|
|
|
|
- name: Create new test comment
|
|
if: steps.find-comment.outputs.comment-id == ''
|
|
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
|
|
with:
|
|
header: e2e-test-results
|
|
message: |
|
|
<!-- e2e-test-results -->
|
|
## 🧪 E2E Test Results
|
|
|
|
⏳ **Tests are running...**
|
|
|
|
This comment will be updated with the results when the tests complete.
|
|
|
|
---
|
|
_Started at: ${{ github.run_started_at }}_
|
|
|
|
- name: Update existing test comment with stale warning
|
|
if: steps.find-comment.outputs.comment-id != '' && steps.get-comment.outputs.has-results == 'true'
|
|
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
|
|
with:
|
|
header: e2e-test-results
|
|
path: ${{ steps.get-comment.outputs.stale-comment-path }}
|
|
|
|
- name: Update existing test comment without results
|
|
if: steps.find-comment.outputs.comment-id != '' && steps.get-comment.outputs.has-results != 'true'
|
|
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
|
|
with:
|
|
header: e2e-test-results
|
|
message: |
|
|
<!-- e2e-test-results -->
|
|
## 🧪 E2E Test Results
|
|
|
|
⏳ **Tests are running...**
|
|
|
|
This comment will be updated with the results when the tests complete.
|
|
|
|
---
|
|
_Started at: ${{ github.run_started_at }}_
|
|
|
|
unit:
|
|
name: Unit Tests (${{ matrix.os }})
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 30
|
|
needs: ci-scope
|
|
if: ${{ needs.ci-scope.outputs.validation-fast-path != 'true' && !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, windows-latest]
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
setup-rust: 'true'
|
|
install-args: '--ignore-scripts'
|
|
build-packages: 'false'
|
|
|
|
- name: Run Unit Tests
|
|
run: pnpm test --filter='!./docs' --filter='!./workbench/vitest'
|
|
|
|
serializable-revivers:
|
|
name: Serializable Reviver Compatibility
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
needs: ci-scope
|
|
if: ${{ needs.ci-scope.outputs.validation-fast-path != 'true' && !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }}
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
install-args: '--ignore-scripts'
|
|
build-packages: 'false'
|
|
|
|
- name: Build reviver packages
|
|
run: pnpm turbo run build --filter='@workflow/cli' --filter='@workflow/web-shared'
|
|
|
|
- name: Check web-shared and CLI revivers
|
|
run: pnpm vitest run packages/web-shared/test/serializable-revivers.test.ts
|
|
|
|
build-error-messages:
|
|
name: Node.js Module Build Errors Test
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
needs: ci-scope
|
|
if: ${{ needs.ci-scope.outputs.fast-path != 'true' && !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }}
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
|
|
|
- name: Setup Node.js 22.x
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22.x
|
|
cache: "pnpm"
|
|
|
|
- name: Install Dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Run Initial Build
|
|
run: pnpm turbo run build --filter='!./workbench/*'
|
|
|
|
- name: Generate Workflows Registry
|
|
run: node workbench/scripts/generate-workflows-registry.js workbench/nextjs-turbopack/workflows workbench/nextjs-turbopack/_workflows.ts
|
|
|
|
- name: Run Build Error Tests
|
|
run: pnpm vitest run packages/core/e2e/build-errors.test.ts
|
|
env:
|
|
APP_NAME: "nextjs-turbopack"
|
|
|
|
# Regression test for the o2flow beta.26 incident: hook.resume() from a
|
|
# plain API route must resolve the workflow world inside an isolated
|
|
# (lambda-like) route bundle. Runs in this job because it also performs
|
|
# a production Turbopack build of the nextjs-turbopack workbench.
|
|
- name: Run Route Bundle Isolation Tests
|
|
run: pnpm vitest run packages/core/e2e/route-bundle-isolation.test.ts
|
|
env:
|
|
APP_NAME: "nextjs-turbopack"
|
|
|
|
vitest-plugin:
|
|
name: Vitest Plugin Tests (${{ matrix.vm }})
|
|
runs-on: ubuntu-latest
|
|
needs: ci-scope
|
|
if: ${{ needs.ci-scope.outputs.fast-path != 'true' }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
# Workflow VM engines: node:vm (default) and the opt-in QuickJS
|
|
# WASM engine (WORKFLOW_VM=quickjs).
|
|
vm: [node, quickjs]
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
setup-rust: 'true'
|
|
install-args: '--ignore-scripts'
|
|
build-packages: 'false'
|
|
|
|
- name: Build packages
|
|
run: pnpm turbo run build --filter='!./workbench/*'
|
|
|
|
- name: Run Vitest Plugin Tests
|
|
run: pnpm test
|
|
working-directory: workbench/vitest
|
|
env:
|
|
WORKFLOW_VM: ${{ matrix.vm }}
|
|
|
|
e2e-package-build:
|
|
name: Build Shared E2E Packages
|
|
runs-on: ubuntu-latest
|
|
needs: ci-scope
|
|
if: ${{ needs.ci-scope.outputs.fast-path != 'true' && !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }}
|
|
timeout-minutes: 30
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup environment and build packages
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
build-packages: 'true'
|
|
|
|
- name: Upload package build artifacts
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: e2e-package-build-artifacts
|
|
# Keep this list aligned with package build outputs in turbo.json.
|
|
# upload-artifact strips the common packages/ root; consumers restore into packages/.
|
|
path: |
|
|
packages/*/dist
|
|
packages/*/.well-known
|
|
packages/*/src/version.ts
|
|
packages/core/src/runtime/vm-serde-bundle.generated.ts
|
|
packages/core/src/runtime/quickjs-assets.generated.ts
|
|
packages/swc-plugin-workflow/swc_plugin_workflow.wasm
|
|
packages/swc-plugin-workflow/build-hash.json
|
|
include-hidden-files: true
|
|
retention-days: 1
|
|
|
|
e2e-vercel-prod:
|
|
name: E2E Vercel Prod Tests (${{ matrix.app.name }} - ${{ matrix.vm }})
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
needs: ci-scope
|
|
if: ${{ needs.ci-scope.outputs.fast-path != 'true' }}
|
|
permissions:
|
|
id-token: write
|
|
contents: read
|
|
deployments: read
|
|
statuses: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
# Workflow VM engines: node:vm (default) and the opt-in QuickJS
|
|
# WASM engine. The env var is set on the e2e test runner, which is
|
|
# the client that starts runs against the deployed app — start()
|
|
# stamps executionContext.workflowVm so the deployed handler
|
|
# executes each run on the requested engine.
|
|
vm: [node, quickjs]
|
|
app:
|
|
- name: "example"
|
|
project-id: "prj_xWq20Dd860HHAfzMjK2Mb6TPVxMa"
|
|
project-slug: "example-workflow"
|
|
- name: "nextjs-turbopack"
|
|
project-id: "prj_yjkM7UdHliv8bfxZ1sMJQf1pMpdi"
|
|
project-slug: "example-nextjs-workflow-turbopack"
|
|
- name: "nextjs-webpack"
|
|
project-id: "prj_avRPBF3eWjh6iDNQgmhH4VOg27h0"
|
|
project-slug: "example-nextjs-workflow-webpack"
|
|
- name: "nitro"
|
|
project-id: "prj_e7DZirYdLrQKXNrlxg7KmA6ABx8r"
|
|
project-slug: "workbench-nitro-workflow"
|
|
- name: "vite"
|
|
project-id: "prj_uLIcNZNDmETulAvj5h0IcDHi5432"
|
|
project-slug: "workbench-vite-workflow"
|
|
- name: "nuxt"
|
|
project-id: "prj_oTgiz3SGX2fpZuM6E0P38Ts8de6d"
|
|
project-slug: "workbench-nuxt-workflow"
|
|
- name: "sveltekit"
|
|
project-id: "prj_MqnBLm71ceXGSnm3Fs8i8gBnI23G"
|
|
project-slug: "workbench-sveltekit-workflow"
|
|
- name: "hono"
|
|
project-id: "prj_p0GIEsfl53L7IwVbosPvi9rPSOYW"
|
|
project-slug: "workbench-hono-workflow"
|
|
- name: "express"
|
|
project-id: "prj_cCZjpBy92VRbKHHbarDMhOHtkuIr"
|
|
project-slug: "workbench-express-workflow"
|
|
- name: "fastify"
|
|
project-id: "prj_5Yap0VDQ633v998iqQ3L3aQ25Cck"
|
|
project-slug: "workbench-fastify-workflow"
|
|
- name: "nest"
|
|
project-id: "prj_AV3YrzooDhMlvfmZybqeugHRk48Y"
|
|
project-slug: "workbench-nestjs-workflow"
|
|
- name: "astro"
|
|
project-id: "prj_YDAXj3K8LM0hgejuIMhioz2yLgTI"
|
|
project-slug: "workbench-astro-workflow"
|
|
- name: "tanstack-start"
|
|
project-id: "prj_643jeVugTMq5ivsOFQHcbLG1qcnu"
|
|
project-slug: "workbench-tanstack-start-workflow"
|
|
- name: "python"
|
|
project-id: "prj_MtdPMACTDJnukz8LzWM65Fob0IVU"
|
|
project-slug: "workbench-python-workflow"
|
|
exclude:
|
|
# `vm` selects the JS engine the workflow body runs on. There is no
|
|
# Python meaning to that axis — the Python app has one runtime — so
|
|
# only the `node` half of the pair is a real configuration.
|
|
#
|
|
# GitHub Actions matches an `exclude` entry by deep equality on every
|
|
# key it names, so the whole `app` object has to be repeated verbatim.
|
|
# Keep it in sync with the entry above.
|
|
- vm: quickjs
|
|
app:
|
|
name: "python"
|
|
project-id: "prj_MtdPMACTDJnukz8LzWM65Fob0IVU"
|
|
project-slug: "workbench-python-workflow"
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
WORKFLOW_PUBLIC_MANIFEST: '1'
|
|
WORKFLOW_RETURN_VALUE_POLL_INTERVAL_MS: '5000'
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
build-packages: 'false'
|
|
|
|
- name: Build CLI
|
|
run: pnpm turbo run build --filter='@workflow/cli'
|
|
|
|
# `changeset-release/*` PRs (the changesets "Version Packages" PR) are
|
|
# deliberately not deployed: the branch is force-pushed and can point at
|
|
# main's HEAD SHA, and Vercel keeps one commit status per project per
|
|
# SHA, so a same-SHA preview deployment overwrites the production one
|
|
# that the step below reads the deployment ID from. Every project's
|
|
# vercel.json therefore sets `git.deploymentEnabled` false for that
|
|
# branch, which leaves these PRs with no deployment of their own to wait
|
|
# for. Their content is main plus a version-bump commit, so they run
|
|
# against the production deployment main already produced instead.
|
|
- name: Waiting for the Vercel deployment
|
|
id: waitForDeployment
|
|
if: ${{ !startsWith(github.head_ref, 'changeset-release/') }}
|
|
uses: vercel/wait-for-deployment-action@0e2b0c5c5cce31f1648108aeec56467187aca037
|
|
with:
|
|
project-slug: ${{ matrix.app.project-slug }}
|
|
timeout: 1000
|
|
check-interval: 15
|
|
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}
|
|
|
|
# The base SHA, not the PR head: that is the main commit whose
|
|
# production deployment carries the code under test. Waiting on it also
|
|
# means a production build still in flight is waited out instead of
|
|
# testing whatever happens to be aliased to production. The commit-status
|
|
# ID this action reads is unambiguous for main SHAs once this repo stops
|
|
# deploying `changeset-release/main` (see above) — that branch was the
|
|
# only one that ever deployed a commit main also deployed.
|
|
- name: Waiting for the Vercel deployment (changeset-release PR)
|
|
id: prodDeployment
|
|
if: ${{ startsWith(github.head_ref, 'changeset-release/') }}
|
|
uses: vercel/wait-for-deployment-action@0e2b0c5c5cce31f1648108aeec56467187aca037
|
|
with:
|
|
project-slug: ${{ matrix.app.project-slug }}
|
|
timeout: 1000
|
|
check-interval: 15
|
|
environment: production
|
|
sha: ${{ github.event.pull_request.base.sha }}
|
|
|
|
- name: Record E2E start time
|
|
id: e2eStart
|
|
run: echo "ms=$(($(date +%s) * 1000))" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Run E2E Tests
|
|
run: pnpm run test:e2e --reporter=verbose --reporter=json --reporter=./packages/core/e2e/github-reporter.ts "--outputFile=e2e-vercel-prod-$APP_NAME-$WORKFLOW_VM.json"
|
|
env:
|
|
NODE_OPTIONS: "--enable-source-maps"
|
|
DEPLOYMENT_URL: ${{ steps.waitForDeployment.outputs.deployment-url || steps.prodDeployment.outputs.deployment-url }}
|
|
VERCEL_DEPLOYMENT_ID: ${{ steps.waitForDeployment.outputs.deployment-id || steps.prodDeployment.outputs.deployment-id }}
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
WORKFLOW_VM: ${{ matrix.vm }}
|
|
# changeset-release PRs test main's production deployment, so they
|
|
# must be treated as a production run everywhere downstream.
|
|
WORKFLOW_VERCEL_ENV: ${{ (github.ref == 'refs/heads/main' || startsWith(github.head_ref, 'changeset-release/')) && 'production' || 'preview' }}
|
|
WORKFLOW_VERCEL_AUTH_TOKEN: ${{ secrets.VERCEL_LABS_TOKEN }}
|
|
WORKFLOW_VERCEL_TEAM: "team_nO2mCG4W8IxPIeKoSsqwAxxB"
|
|
WORKFLOW_VERCEL_PROJECT: ${{ matrix.app.project-id }}
|
|
WORKFLOW_VERCEL_PROJECT_SLUG: ${{ matrix.app.project-slug }}
|
|
# The trusted-sources OIDC token (x-vercel-trusted-oidc-idp-token)
|
|
# is minted on demand inside `scripts/trusted-sources-headers.mjs`
|
|
# via the runner's ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN env vars
|
|
# (exposed when `permissions: id-token: write` is set on the job),
|
|
# and re-minted before its 5-minute expiry. This lets long e2e
|
|
# suites keep a fresh token throughout the run instead of using
|
|
# a single pre-minted token that would expire mid-suite.
|
|
# See: https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/trusted-sources
|
|
# Point PRs at the protected workflow-server preview; unset on main
|
|
# so production runs hit the public vercel-workflow.com URL. Also
|
|
# unset for changeset-release PRs: they test a production deployment
|
|
# that is wired to the production workflow-server, and the harness
|
|
# has to read run state from the same server the app writes it to.
|
|
VERCEL_WORKFLOW_SERVER_URL: ${{ github.ref != 'refs/heads/main' && !startsWith(github.head_ref, 'changeset-release/') && secrets.VERCEL_WORKFLOW_SERVER_URL || '' }}
|
|
|
|
- name: Capture runtime logs on failure
|
|
if: failure()
|
|
env:
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
WORKFLOW_VERCEL_AUTH_TOKEN: ${{ secrets.VERCEL_LABS_TOKEN }}
|
|
WORKFLOW_VERCEL_TEAM: "team_nO2mCG4W8IxPIeKoSsqwAxxB"
|
|
WORKFLOW_VERCEL_PROJECT: ${{ matrix.app.project-id }}
|
|
WORKFLOW_VERCEL_ENV: ${{ (github.ref == 'refs/heads/main' || startsWith(github.head_ref, 'changeset-release/')) && 'production' || 'preview' }}
|
|
VERCEL_DEPLOYMENT_ID: ${{ steps.waitForDeployment.outputs.deployment-id || steps.prodDeployment.outputs.deployment-id }}
|
|
E2E_START_MS: ${{ steps.e2eStart.outputs.ms }}
|
|
run: node .github/scripts/fetch-e2e-runtime-logs.mjs
|
|
|
|
- name: Generate E2E summary
|
|
if: always()
|
|
env:
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
WORKFLOW_VM: ${{ matrix.vm }}
|
|
run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Vercel Prod ($APP_NAME - $WORKFLOW_VM)" >> $GITHUB_STEP_SUMMARY || true
|
|
|
|
- name: Upload E2E results
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: e2e-results-vercel-prod-${{ matrix.app.name }}-${{ matrix.vm }}
|
|
path: |
|
|
e2e-vercel-prod-${{ matrix.app.name }}-${{ matrix.vm }}.json
|
|
e2e-metadata-${{ matrix.app.name }}-vercel.json
|
|
e2e-failures-${{ matrix.app.name }}-vercel.json
|
|
e2e-flaky-${{ matrix.app.name }}-vercel.json
|
|
e2e-infra-${{ matrix.app.name }}-vercel.json
|
|
e2e-diagnostics-${{ matrix.app.name }}-vercel.json
|
|
e2e-runtime-logs-${{ matrix.app.name }}-vercel.json
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|
|
|
|
e2e-vercel-multi-region:
|
|
# Vercel-specific multi-region routing suite (start()'s `region`
|
|
# option). Deliberately separate from e2e-vercel-prod: that job runs
|
|
# the shared suite as a matrix across all worlds/frameworks, while
|
|
# this functionality only exists for @workflow/world-vercel and only
|
|
# the nextjs-turbopack workbench is deployed multi-region
|
|
# (workbench/nextjs-turbopack/vercel.json pins iad1+sfo1+fra1).
|
|
name: E2E Vercel Multi-Region Tests (nextjs-turbopack)
|
|
runs-on: ubuntu-latest
|
|
# 19 regions: the implicit-route cases pay one cold start each,
|
|
# sequentially.
|
|
timeout-minutes: 35
|
|
needs: ci-scope
|
|
if: ${{ needs.ci-scope.outputs.fast-path != 'true' }}
|
|
permissions:
|
|
id-token: write
|
|
contents: read
|
|
deployments: read
|
|
statuses: read
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
WORKFLOW_PUBLIC_MANIFEST: '1'
|
|
WORKFLOW_RETURN_VALUE_POLL_INTERVAL_MS: '5000'
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
build-packages: 'false'
|
|
|
|
- name: Build CLI
|
|
run: pnpm turbo run build --filter='@workflow/cli'
|
|
|
|
# See the note on e2e-vercel-prod: `changeset-release/*` PRs have no
|
|
# deployment of their own and test main's production deployment.
|
|
- name: Waiting for the Vercel deployment
|
|
id: waitForDeployment
|
|
if: ${{ !startsWith(github.head_ref, 'changeset-release/') }}
|
|
uses: vercel/wait-for-deployment-action@0e2b0c5c5cce31f1648108aeec56467187aca037
|
|
with:
|
|
project-slug: "example-nextjs-workflow-turbopack"
|
|
timeout: 1000
|
|
check-interval: 15
|
|
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}
|
|
|
|
- name: Waiting for the Vercel deployment (changeset-release PR)
|
|
id: prodDeployment
|
|
if: ${{ startsWith(github.head_ref, 'changeset-release/') }}
|
|
uses: vercel/wait-for-deployment-action@0e2b0c5c5cce31f1648108aeec56467187aca037
|
|
with:
|
|
project-slug: "example-nextjs-workflow-turbopack"
|
|
timeout: 1000
|
|
check-interval: 15
|
|
environment: production
|
|
sha: ${{ github.event.pull_request.base.sha }}
|
|
|
|
- name: Run Multi-Region E2E Tests
|
|
run: pnpm vitest run packages/core/e2e/e2e-region.test.ts --reporter=verbose --reporter=json --reporter=./packages/core/e2e/github-reporter.ts "--outputFile=e2e-vercel-multi-region-$APP_NAME.json"
|
|
env:
|
|
NODE_OPTIONS: "--enable-source-maps"
|
|
DEPLOYMENT_URL: ${{ steps.waitForDeployment.outputs.deployment-url || steps.prodDeployment.outputs.deployment-url }}
|
|
VERCEL_DEPLOYMENT_ID: ${{ steps.waitForDeployment.outputs.deployment-id || steps.prodDeployment.outputs.deployment-id }}
|
|
APP_NAME: "nextjs-turbopack"
|
|
# changeset-release PRs test main's production deployment, so they
|
|
# must be treated as a production run everywhere downstream.
|
|
WORKFLOW_VERCEL_ENV: ${{ (github.ref == 'refs/heads/main' || startsWith(github.head_ref, 'changeset-release/')) && 'production' || 'preview' }}
|
|
WORKFLOW_VERCEL_AUTH_TOKEN: ${{ secrets.VERCEL_LABS_TOKEN }}
|
|
WORKFLOW_VERCEL_TEAM: "team_nO2mCG4W8IxPIeKoSsqwAxxB"
|
|
WORKFLOW_VERCEL_PROJECT: "prj_yjkM7UdHliv8bfxZ1sMJQf1pMpdi"
|
|
WORKFLOW_VERCEL_PROJECT_SLUG: "example-nextjs-workflow-turbopack"
|
|
# See the note on e2e-vercel-prod: PRs point at the protected
|
|
# workflow-server preview; unset on main for production.
|
|
VERCEL_WORKFLOW_SERVER_URL: ${{ github.ref != 'refs/heads/main' && !startsWith(github.head_ref, 'changeset-release/') && secrets.VERCEL_WORKFLOW_SERVER_URL || '' }}
|
|
|
|
- name: Capture runtime logs on failure
|
|
if: failure()
|
|
env:
|
|
APP_NAME: "nextjs-turbopack"
|
|
WORKFLOW_VERCEL_AUTH_TOKEN: ${{ secrets.VERCEL_LABS_TOKEN }}
|
|
WORKFLOW_VERCEL_TEAM: "team_nO2mCG4W8IxPIeKoSsqwAxxB"
|
|
WORKFLOW_VERCEL_PROJECT: "prj_yjkM7UdHliv8bfxZ1sMJQf1pMpdi"
|
|
WORKFLOW_VERCEL_ENV: ${{ (github.ref == 'refs/heads/main' || startsWith(github.head_ref, 'changeset-release/')) && 'production' || 'preview' }}
|
|
VERCEL_DEPLOYMENT_ID: ${{ steps.waitForDeployment.outputs.deployment-id || steps.prodDeployment.outputs.deployment-id }}
|
|
run: node .github/scripts/fetch-e2e-runtime-logs.mjs
|
|
|
|
- name: Upload E2E results
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: e2e-results-vercel-multi-region-nextjs-turbopack
|
|
path: |
|
|
e2e-vercel-multi-region-nextjs-turbopack.json
|
|
e2e-metadata-nextjs-turbopack-vercel.json
|
|
e2e-failures-nextjs-turbopack-vercel.json
|
|
e2e-flaky-nextjs-turbopack-vercel.json
|
|
e2e-infra-nextjs-turbopack-vercel.json
|
|
e2e-diagnostics-nextjs-turbopack-vercel.json
|
|
e2e-runtime-logs-nextjs-turbopack-vercel.json
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|
|
|
|
e2e-vercel-ws-transport:
|
|
# Dedicated coverage for world-vercel's WebSocket events transport
|
|
# (WORKFLOW_EVENTS_TRANSPORT=ws — see events-v4.ts's
|
|
# isWsEventsTransportEnabled). This lane predates the default flip,
|
|
# when it was the only WS coverage in the file; it is kept because it
|
|
# still pins the transport explicitly, which every other Vercel lane
|
|
# now only gets by inheriting the default. See
|
|
# e2e-vercel-http-transport below for the other side.
|
|
#
|
|
# Mirrors e2e-vercel-prod's shape (same apps subset, same full e2e
|
|
# suite) but deploys itself via a plain `vercel deploy` (no
|
|
# `--prebuilt`) instead of waiting for the GitHub App's passive
|
|
# deployment — that's the only way to get a *second*, independent
|
|
# deployment of a project we don't own the Environment Variables UI
|
|
# for, with its own env var, without creating a whole new project.
|
|
#
|
|
# Deliberately NOT `--prebuilt`: an earlier version of this job built
|
|
# locally on the runner via `vercel build --prebuilt` and hit a string
|
|
# of environment-specific bugs that don't reproduce on a real
|
|
# deployment (missing `serverExternalPackages` tracing for
|
|
# `@workflow/world-vercel`, nitro preset auto-detection picking
|
|
# `node-server` instead of `vercel`, needing the whole monorepo
|
|
# pre-built on this runner). Plain `vercel deploy` uploads source and
|
|
# builds remotely on Vercel's own infra — the exact same pipeline the
|
|
# GitHub App uses for e2e-vercel-prod's deployments — so none of that
|
|
# applies here.
|
|
# Runs on every PR. It was opt-in behind `ws-transport-test` when four real
|
|
# `vercel deploy`s were too much to charge an unrelated PR for a transport
|
|
# that was off by default. That reasoning expired with the default: every
|
|
# deployment now uses WebSockets, so this stopped being a cost charged for
|
|
# someone else's feature and became the only check that the feature works.
|
|
name: E2E Vercel WS Transport Test (${{ matrix.app.name }})
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
needs: ci-scope
|
|
# Mirrors e2e-vercel-prod and the HTTP lane: this is the only lane that
|
|
# *asserts* the socket carried the events, since e2e-vercel-prod inherits
|
|
# the default but checks nothing.
|
|
if: ${{ needs.ci-scope.outputs.fast-path != 'true' }}
|
|
permissions:
|
|
id-token: write
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
app:
|
|
- name: "example"
|
|
project-id: "prj_xWq20Dd860HHAfzMjK2Mb6TPVxMa"
|
|
project-slug: "example-workflow"
|
|
- name: "nextjs-turbopack"
|
|
project-id: "prj_yjkM7UdHliv8bfxZ1sMJQf1pMpdi"
|
|
project-slug: "example-nextjs-workflow-turbopack"
|
|
- name: "express"
|
|
project-id: "prj_cCZjpBy92VRbKHHbarDMhOHtkuIr"
|
|
project-slug: "workbench-express-workflow"
|
|
# Vite resolves `ws`'s absent optional accelerators to a stub
|
|
# instead of failing the require, so a build check can't see the
|
|
# breakage and only a masked frame at or above 48 bytes trips it
|
|
# (see WORKFLOW_OPTIONAL_WS_NATIVE_MODULES). Every CBOR event
|
|
# frame clears that, so this lane is the coverage for it.
|
|
- name: "vite"
|
|
project-id: "prj_uLIcNZNDmETulAvj5h0IcDHi5432"
|
|
project-slug: "workbench-vite-workflow"
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
WORKFLOW_PUBLIC_MANIFEST: '1'
|
|
WS_TEAM_ID: "team_nO2mCG4W8IxPIeKoSsqwAxxB"
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
build-packages: 'false'
|
|
|
|
- name: Build CLI
|
|
run: pnpm turbo run build --filter='@workflow/cli'
|
|
|
|
# Pinned and installed once rather than `npx vercel@<version>` per
|
|
# command — repeated `npx` resolution showed inconsistent behavior
|
|
# (different Node version in the CLI's own banner, a doubled project
|
|
# path) across otherwise-identical earlier CI attempts.
|
|
- name: Install vercel CLI
|
|
run: npm install -g vercel@56.2.0
|
|
|
|
- name: Deploy a dedicated WS-transport preview
|
|
id: wsDeploy
|
|
env:
|
|
WS_VERCEL_TOKEN: ${{ secrets.VERCEL_LABS_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
# `vercel deploy` (source-upload mode, no --prebuilt) re-applies
|
|
# the linked project's own Root Directory setting internally —
|
|
# unlike `vercel build`, it expects to run from the repo root and
|
|
# narrow down itself, the same way the GitHub App's own deploy
|
|
# does. Passing `--cwd=<app dir>` here double-joins that Root
|
|
# Directory underneath itself ("workbench/x/workbench/x does not
|
|
# exist"). Link and deploy from the repo root — no --cwd at all —
|
|
# and let the project's own Root Directory setting do the
|
|
# narrowing, matching how every passively-triggered deployment in
|
|
# this file already works.
|
|
vercel pull --yes --environment=preview \
|
|
--project="${{ matrix.app.project-id }}" \
|
|
--scope="$WS_TEAM_ID" --token="$WS_VERCEL_TOKEN"
|
|
# `vercel deploy` writes the new deployment's URL to stdout and
|
|
# progress/logs to stderr, specifically so it composes as
|
|
# `url=$(vercel deploy ...)` in scripts — do not redirect stderr
|
|
# into this capture. `--target=preview` is explicit here even
|
|
# though it's the default without `--prod` — these are shared,
|
|
# production-adjacent projects, and this shouldn't ever silently
|
|
# start deploying to their production aliases if a future edit
|
|
# changes flags around this call.
|
|
# Strict mode turns a silent HTTP demotion into a failed run, so this
|
|
# lane actually asserts the socket carried traffic instead of only
|
|
# asserting the feature was harmless. Scoped to `step_completed` —
|
|
# see STRICT_WS_EVENT_TYPES in events-v4.ts for why the other event
|
|
# types fall back legitimately. Never set on e2e-vercel-prod.
|
|
URL=$(vercel deploy --yes --target=preview \
|
|
-e WORKFLOW_EVENTS_TRANSPORT=ws \
|
|
-e WORKFLOW_INTERNAL_EVENTS_TRANSPORT_STRICT=1 \
|
|
--scope="$WS_TEAM_ID" --token="$WS_VERCEL_TOKEN")
|
|
echo "url=$URL" >> "$GITHUB_OUTPUT"
|
|
# Best-effort: only used by the runtime-logs capture on failure,
|
|
# which already skips gracefully if this doesn't parse.
|
|
DEPLOYMENT_ID=$(vercel inspect "$URL" --format=json \
|
|
--scope="$WS_TEAM_ID" --token="$WS_VERCEL_TOKEN" 2>/dev/null \
|
|
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{console.log(JSON.parse(d).id||'')}catch{}})" || true)
|
|
echo "deploymentId=$DEPLOYMENT_ID" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Record E2E start time
|
|
id: e2eStart
|
|
run: echo "ms=$(($(date +%s) * 1000))" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Run E2E Tests
|
|
run: pnpm run test:e2e --reporter=verbose --reporter=json --reporter=./packages/core/e2e/github-reporter.ts "--outputFile=e2e-vercel-ws-transport-$APP_NAME.json"
|
|
env:
|
|
NODE_OPTIONS: "--enable-source-maps"
|
|
DEPLOYMENT_URL: ${{ steps.wsDeploy.outputs.url }}
|
|
VERCEL_DEPLOYMENT_ID: ${{ steps.wsDeploy.outputs.deploymentId }}
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
WORKFLOW_VERCEL_ENV: "preview"
|
|
WORKFLOW_VERCEL_AUTH_TOKEN: ${{ secrets.VERCEL_LABS_TOKEN }}
|
|
WORKFLOW_VERCEL_TEAM: ${{ env.WS_TEAM_ID }}
|
|
WORKFLOW_VERCEL_PROJECT: ${{ matrix.app.project-id }}
|
|
WORKFLOW_VERCEL_PROJECT_SLUG: ${{ matrix.app.project-slug }}
|
|
VERCEL_WORKFLOW_SERVER_URL: ${{ github.ref != 'refs/heads/main' && secrets.VERCEL_WORKFLOW_SERVER_URL || '' }}
|
|
|
|
- name: Capture runtime logs on failure
|
|
if: failure()
|
|
env:
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
WORKFLOW_VERCEL_AUTH_TOKEN: ${{ secrets.VERCEL_LABS_TOKEN }}
|
|
WORKFLOW_VERCEL_TEAM: ${{ env.WS_TEAM_ID }}
|
|
WORKFLOW_VERCEL_PROJECT: ${{ matrix.app.project-id }}
|
|
WORKFLOW_VERCEL_ENV: "preview"
|
|
VERCEL_DEPLOYMENT_ID: ${{ steps.wsDeploy.outputs.deploymentId }}
|
|
E2E_START_MS: ${{ steps.e2eStart.outputs.ms }}
|
|
run: node .github/scripts/fetch-e2e-runtime-logs.mjs
|
|
|
|
- name: Generate E2E summary
|
|
if: always()
|
|
env:
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Vercel WS Transport ($APP_NAME)" >> $GITHUB_STEP_SUMMARY || true
|
|
|
|
- name: Upload E2E results
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: e2e-results-vercel-ws-transport-${{ matrix.app.name }}
|
|
path: |
|
|
e2e-vercel-ws-transport-${{ matrix.app.name }}.json
|
|
e2e-metadata-${{ matrix.app.name }}-vercel.json
|
|
e2e-failures-${{ matrix.app.name }}-vercel.json
|
|
e2e-flaky-${{ matrix.app.name }}-vercel.json
|
|
e2e-infra-${{ matrix.app.name }}-vercel.json
|
|
e2e-diagnostics-${{ matrix.app.name }}-vercel.json
|
|
e2e-runtime-logs-${{ matrix.app.name }}-vercel.json
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|
|
|
|
e2e-vercel-http-transport:
|
|
# The counterpart to e2e-vercel-ws-transport, and the reason it exists
|
|
# is the default flip: with WORKFLOW_EVENTS_TRANSPORT defaulting to
|
|
# `ws`, e2e-vercel-prod — which sets no transport env var at all — is
|
|
# now a WebSocket lane. Nothing in this file would exercise the HTTP
|
|
# events transport against a real Vercel deployment any more. This is
|
|
# not additive coverage; it replaces coverage that the flip silently
|
|
# took away.
|
|
#
|
|
# HTTP is now the fallback path, and the fallback is *silent*:
|
|
# resolveWsTransport returning null costs a write nothing and logs
|
|
# nothing. A path that is both unexercised and quiet is the exact
|
|
# shape of the durabench bug this stack came out of, so this lane is
|
|
# unconditional and required rather than label-gated like the WS one.
|
|
#
|
|
# Six apps, chosen by server shape rather than by count. Before the flip
|
|
# HTTP was the default, so all 28 e2e-vercel-prod lane-runs covered it;
|
|
# after the flip they cover WebSockets instead and this lane is the whole
|
|
# of the HTTP coverage. Each row is a real `vercel deploy` charged to
|
|
# every PR, so the full 14 is too expensive, but two was too thin for a
|
|
# transport that is still supported.
|
|
#
|
|
# The shapes, one fixture each:
|
|
# example - baseline, no framework server
|
|
# nextjs-turbopack - Next; also the only fixture emitting OTEL spans,
|
|
# so the only one that can show which transport ran
|
|
# vite - Vite SSR
|
|
# express - Node req/res server
|
|
# nitro - Nitro/h3, which also covers nuxt
|
|
# hono - fetch-API Request/Response server, a genuinely
|
|
# different mount shape from express
|
|
#
|
|
# Omitted because they duplicate a shape already here: nextjs-webpack
|
|
# (same runtime as turbopack, different bundler), nuxt (nitro), fastify
|
|
# and nest (both express-shaped), astro and tanstack-start (adapter
|
|
# variants). `python` is omitted because it has no conformance gate and
|
|
# needs routes this suite does not serve - see the e2e-python job.
|
|
#
|
|
# The first four deliberately match the WS lane's matrix, so the same
|
|
# fixture is exercised on both transports and a failure on one can be
|
|
# compared against the other.
|
|
#
|
|
# Deploys itself with `vercel deploy` rather than `--prebuilt` for the
|
|
# same reasons documented on e2e-vercel-ws-transport above.
|
|
name: E2E Vercel HTTP Transport Test (${{ matrix.app.name }})
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
needs: ci-scope
|
|
# Mirrors e2e-vercel-prod's condition exactly, so this lane runs in
|
|
# every case that one does, including under `workflow-server-test`.
|
|
if: ${{ needs.ci-scope.outputs.fast-path != 'true' }}
|
|
permissions:
|
|
id-token: write
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
app:
|
|
- name: "example"
|
|
project-id: "prj_xWq20Dd860HHAfzMjK2Mb6TPVxMa"
|
|
project-slug: "example-workflow"
|
|
- name: "nextjs-turbopack"
|
|
project-id: "prj_yjkM7UdHliv8bfxZ1sMJQf1pMpdi"
|
|
project-slug: "example-nextjs-workflow-turbopack"
|
|
- name: "vite"
|
|
project-id: "prj_uLIcNZNDmETulAvj5h0IcDHi5432"
|
|
project-slug: "workbench-vite-workflow"
|
|
- name: "express"
|
|
project-id: "prj_cCZjpBy92VRbKHHbarDMhOHtkuIr"
|
|
project-slug: "workbench-express-workflow"
|
|
- name: "nitro"
|
|
project-id: "prj_e7DZirYdLrQKXNrlxg7KmA6ABx8r"
|
|
project-slug: "workbench-nitro-workflow"
|
|
- name: "hono"
|
|
project-id: "prj_p0GIEsfl53L7IwVbosPvi9rPSOYW"
|
|
project-slug: "workbench-hono-workflow"
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
WORKFLOW_PUBLIC_MANIFEST: '1'
|
|
WS_TEAM_ID: "team_nO2mCG4W8IxPIeKoSsqwAxxB"
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
build-packages: 'false'
|
|
|
|
- name: Build CLI
|
|
run: pnpm turbo run build --filter='@workflow/cli'
|
|
|
|
- name: Install vercel CLI
|
|
run: npm install -g vercel@56.2.0
|
|
|
|
- name: Deploy a dedicated HTTP-transport preview
|
|
id: httpDeploy
|
|
env:
|
|
WS_VERCEL_TOKEN: ${{ secrets.VERCEL_LABS_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
vercel pull --yes --environment=preview \
|
|
--project="${{ matrix.app.project-id }}" \
|
|
--scope="$WS_TEAM_ID" --token="$WS_VERCEL_TOKEN"
|
|
# `http` is the one value that opts out; the gate compares against
|
|
# exactly this string, so a typo here would silently test the
|
|
# default instead of the fallback and this lane would quietly
|
|
# become a duplicate of e2e-vercel-prod.
|
|
URL=$(vercel deploy --yes --target=preview \
|
|
-e WORKFLOW_EVENTS_TRANSPORT=http \
|
|
--scope="$WS_TEAM_ID" --token="$WS_VERCEL_TOKEN")
|
|
echo "url=$URL" >> "$GITHUB_OUTPUT"
|
|
DEPLOYMENT_ID=$(vercel inspect "$URL" --format=json \
|
|
--scope="$WS_TEAM_ID" --token="$WS_VERCEL_TOKEN" 2>/dev/null \
|
|
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{console.log(JSON.parse(d).id||'')}catch{}})" || true)
|
|
echo "deploymentId=$DEPLOYMENT_ID" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Record E2E start time
|
|
id: e2eStart
|
|
run: echo "ms=$(($(date +%s) * 1000))" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Run E2E Tests
|
|
run: pnpm run test:e2e --reporter=verbose --reporter=json --reporter=./packages/core/e2e/github-reporter.ts "--outputFile=e2e-vercel-http-transport-$APP_NAME.json"
|
|
env:
|
|
NODE_OPTIONS: "--enable-source-maps"
|
|
DEPLOYMENT_URL: ${{ steps.httpDeploy.outputs.url }}
|
|
VERCEL_DEPLOYMENT_ID: ${{ steps.httpDeploy.outputs.deploymentId }}
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
WORKFLOW_VERCEL_ENV: "preview"
|
|
WORKFLOW_VERCEL_AUTH_TOKEN: ${{ secrets.VERCEL_LABS_TOKEN }}
|
|
WORKFLOW_VERCEL_TEAM: ${{ env.WS_TEAM_ID }}
|
|
WORKFLOW_VERCEL_PROJECT: ${{ matrix.app.project-id }}
|
|
WORKFLOW_VERCEL_PROJECT_SLUG: ${{ matrix.app.project-slug }}
|
|
VERCEL_WORKFLOW_SERVER_URL: ${{ github.ref != 'refs/heads/main' && secrets.VERCEL_WORKFLOW_SERVER_URL || '' }}
|
|
|
|
- name: Capture runtime logs on failure
|
|
if: failure()
|
|
env:
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
WORKFLOW_VERCEL_AUTH_TOKEN: ${{ secrets.VERCEL_LABS_TOKEN }}
|
|
WORKFLOW_VERCEL_TEAM: ${{ env.WS_TEAM_ID }}
|
|
WORKFLOW_VERCEL_PROJECT: ${{ matrix.app.project-id }}
|
|
WORKFLOW_VERCEL_ENV: "preview"
|
|
VERCEL_DEPLOYMENT_ID: ${{ steps.httpDeploy.outputs.deploymentId }}
|
|
E2E_START_MS: ${{ steps.e2eStart.outputs.ms }}
|
|
run: node .github/scripts/fetch-e2e-runtime-logs.mjs
|
|
|
|
- name: Generate E2E summary
|
|
if: always()
|
|
env:
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Vercel HTTP Transport ($APP_NAME)" >> $GITHUB_STEP_SUMMARY || true
|
|
|
|
- name: Upload E2E results
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: e2e-results-vercel-http-transport-${{ matrix.app.name }}
|
|
path: |
|
|
e2e-vercel-http-transport-${{ matrix.app.name }}.json
|
|
e2e-metadata-${{ matrix.app.name }}-vercel.json
|
|
e2e-failures-${{ matrix.app.name }}-vercel.json
|
|
e2e-flaky-${{ matrix.app.name }}-vercel.json
|
|
e2e-infra-${{ matrix.app.name }}-vercel.json
|
|
e2e-diagnostics-${{ matrix.app.name }}-vercel.json
|
|
e2e-runtime-logs-${{ matrix.app.name }}-vercel.json
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|
|
|
|
getTestMatrix:
|
|
name: Get Test Matrix
|
|
runs-on: ubuntu-latest
|
|
needs: ci-scope
|
|
if: ${{ needs.ci-scope.outputs.fast-path != 'true' && !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }}
|
|
outputs:
|
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
|
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
install-dependencies: 'false'
|
|
build-packages: 'false'
|
|
cache-pnpm: 'false'
|
|
|
|
- id: set-matrix
|
|
run: |
|
|
MATRIX_JSON="$(node ./scripts/create-test-matrix.mjs | jq -c .)"
|
|
echo "matrix=$MATRIX_JSON" >> "$GITHUB_OUTPUT"
|
|
|
|
e2e-local-dev:
|
|
name: E2E Local Dev Tests (${{ matrix.app.name }} - ${{ matrix.app.runLabel }})
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
if: ${{ needs.ci-scope.outputs.fast-path != 'true' && !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }}
|
|
needs: [ci-scope, getTestMatrix, e2e-package-build]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{fromJson(needs.getTestMatrix.outputs.matrix)}}
|
|
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
WORKFLOW_PUBLIC_MANIFEST: '1'
|
|
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
install-dependencies: 'false'
|
|
build-packages: 'false'
|
|
|
|
- name: Setup canary
|
|
if: ${{ matrix.app.canary }}
|
|
env:
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
run: |
|
|
cat packages/next/package.json | jq '.dependencies.next|="16.3.1-canary.17"' > packages/next/package.json.new && mv packages/next/package.json.new packages/next/package.json
|
|
cat "workbench/$APP_NAME/package.json" | jq '.dependencies.next|="16.3.1-canary.17"' > "workbench/$APP_NAME/package.json.new" && mv "workbench/$APP_NAME/package.json.new" "workbench/$APP_NAME/package.json"
|
|
pnpm install --no-frozen-lockfile
|
|
|
|
- name: Install Dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Download shared package builds
|
|
if: ${{ !matrix.app.canary }}
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: e2e-package-build-artifacts
|
|
path: packages
|
|
|
|
- name: Run Initial Build for Next.js canary
|
|
if: ${{ matrix.app.canary }}
|
|
run: pnpm turbo run build --filter='!./workbench/*'
|
|
|
|
- name: Prepare workbench path
|
|
id: prepare-workbench
|
|
uses: ./.github/actions/prepare-workbench-path
|
|
with:
|
|
app-name: ${{ matrix.app.name }}
|
|
|
|
- name: Run E2E Tests
|
|
run: |
|
|
export DEV_SERVER_LOG_PATH="$GITHUB_WORKSPACE/dev-server-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.log"
|
|
rm -f "$DEV_SERVER_LOG_PATH"
|
|
(cd "$WORKBENCH_APP_PATH" && pnpm dev 2>&1 | tee "$DEV_SERVER_LOG_PATH") &
|
|
echo "starting tests in 10 seconds" && sleep 10
|
|
pnpm vitest run packages/core/e2e/dev.test.ts; sleep 10
|
|
pnpm run test:e2e --reporter=verbose --reporter=json --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-dev-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json
|
|
env:
|
|
NODE_OPTIONS: "--enable-source-maps"
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
WORKBENCH_APP_PATH: ${{ steps.prepare-workbench.outputs.workbench_app_path }}
|
|
DEPLOYMENT_URL: "http://localhost:${{ matrix.app.name == 'sveltekit' && '5173' || (matrix.app.name == 'astro' && '4321' || '3000') }}"
|
|
DEV_TEST_CONFIG: ${{ toJSON(matrix.app) }}
|
|
WORKFLOW_DEV_HMR_LOGS: "1"
|
|
NEXT_CANARY: ${{ matrix.app.canary && '1' || '' }}
|
|
WORKFLOW_VM: ${{ matrix.app.vm || '' }}
|
|
|
|
- name: Generate E2E summary
|
|
if: always()
|
|
run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Dev (${{ matrix.app.name }} - ${{ matrix.app.runLabel }})" >> $GITHUB_STEP_SUMMARY || true
|
|
|
|
- name: Upload E2E results
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: e2e-results-local-dev-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}
|
|
path: |
|
|
e2e-local-dev-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json
|
|
e2e-flaky-${{ matrix.app.name }}-local.json
|
|
e2e-infra-${{ matrix.app.name }}-local.json
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|
|
|
|
e2e-local-prod:
|
|
name: E2E Local Prod Tests (${{ matrix.app.name }} - ${{ matrix.app.runLabel }})
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
if: ${{ needs.ci-scope.outputs.fast-path != 'true' && !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }}
|
|
needs: [ci-scope, getTestMatrix, e2e-package-build]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{fromJson(needs.getTestMatrix.outputs.matrix)}}
|
|
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
WORKFLOW_PUBLIC_MANIFEST: '1'
|
|
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
install-dependencies: 'false'
|
|
build-packages: 'false'
|
|
|
|
- name: Setup canary
|
|
if: ${{ matrix.app.canary }}
|
|
env:
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
run: |
|
|
cat packages/next/package.json | jq '.dependencies.next|="16.3.1-canary.17"' > packages/next/package.json.new && mv packages/next/package.json.new packages/next/package.json
|
|
cat "workbench/$APP_NAME/package.json" | jq '.dependencies.next|="16.3.1-canary.17"' > "workbench/$APP_NAME/package.json.new" && mv "workbench/$APP_NAME/package.json.new" "workbench/$APP_NAME/package.json"
|
|
pnpm install --no-frozen-lockfile
|
|
|
|
- name: Install Dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Download shared package builds
|
|
if: ${{ !matrix.app.canary }}
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: e2e-package-build-artifacts
|
|
path: packages
|
|
|
|
- name: Run Initial Build for Next.js canary
|
|
if: ${{ matrix.app.canary }}
|
|
run: pnpm turbo run build --filter='!./workbench/*'
|
|
|
|
- name: Prepare workbench path
|
|
id: prepare-workbench
|
|
uses: ./.github/actions/prepare-workbench-path
|
|
with:
|
|
app-name: ${{ matrix.app.name }}
|
|
|
|
- name: Run Build Tests
|
|
run: pnpm vitest run packages/core/e2e/local-build.test.ts
|
|
env:
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
WORKBENCH_APP_PATH: ${{ steps.prepare-workbench.outputs.workbench_app_path }}
|
|
|
|
# Asserts the shape of the manifest the step above just built. It needs
|
|
# its own invocation rather than another file in that one: local-build
|
|
# is what runs `pnpm build`, and vitest would run both files
|
|
# concurrently, against a manifest that does not exist yet.
|
|
- name: Run Manifest Tests
|
|
run: pnpm vitest run packages/core/e2e/manifest.test.ts
|
|
env:
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
WORKBENCH_APP_PATH: ${{ steps.prepare-workbench.outputs.workbench_app_path }}
|
|
|
|
- name: Run E2E Tests
|
|
run: |
|
|
export PROD_SERVER_LOG_PATH="$GITHUB_WORKSPACE/prod-server-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.log"
|
|
(cd "$WORKBENCH_APP_PATH" && pnpm start 2>&1 | tee "$PROD_SERVER_LOG_PATH") &
|
|
echo "starting tests in 10 seconds" && sleep 10
|
|
pnpm run test:e2e --reporter=verbose --reporter=json --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-prod-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json
|
|
env:
|
|
NODE_OPTIONS: "--enable-source-maps"
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
WORKBENCH_APP_PATH: ${{ steps.prepare-workbench.outputs.workbench_app_path }}
|
|
DEPLOYMENT_URL: "http://localhost:${{ matrix.app.name == 'sveltekit' && '4173' || (matrix.app.name == 'astro' && '4321' || '3000') }}"
|
|
NEXT_CANARY: ${{ matrix.app.canary && '1' || '' }}
|
|
WORKFLOW_VM: ${{ matrix.app.vm || '' }}
|
|
|
|
- name: Generate E2E summary
|
|
if: always()
|
|
run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Prod (${{ matrix.app.name }} - ${{ matrix.app.runLabel }})" >> $GITHUB_STEP_SUMMARY || true
|
|
|
|
- name: Upload E2E results
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: e2e-results-local-prod-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}
|
|
path: |
|
|
e2e-local-prod-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json
|
|
e2e-flaky-${{ matrix.app.name }}-local.json
|
|
e2e-infra-${{ matrix.app.name }}-local.json
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|
|
|
|
e2e-local-postgres:
|
|
name: E2E Local Postgres Tests (${{ matrix.app.name }} - ${{ matrix.app.runLabel }})
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
if: ${{ needs.ci-scope.outputs.fast-path != 'true' && !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }}
|
|
needs: [ci-scope, getTestMatrix, e2e-package-build]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{fromJson(needs.getTestMatrix.outputs.matrix)}}
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:18-alpine
|
|
env:
|
|
POSTGRES_USER: world
|
|
POSTGRES_PASSWORD: world
|
|
POSTGRES_DB: world
|
|
ports:
|
|
- 5432:5432
|
|
options: >-
|
|
--health-cmd pg_isready
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
WORKFLOW_PUBLIC_MANIFEST: '1'
|
|
WORKFLOW_TARGET_WORLD: "@workflow/world-postgres"
|
|
WORKFLOW_POSTGRES_URL: "postgres://world:world@localhost:5432/world"
|
|
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
install-dependencies: 'false'
|
|
build-packages: 'false'
|
|
|
|
- name: Setup canary
|
|
if: ${{ matrix.app.canary }}
|
|
env:
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
run: |
|
|
cat packages/next/package.json | jq '.dependencies.next|="16.3.1-canary.17"' > packages/next/package.json.new && mv packages/next/package.json.new packages/next/package.json
|
|
cat "workbench/$APP_NAME/package.json" | jq '.dependencies.next|="16.3.1-canary.17"' > "workbench/$APP_NAME/package.json.new" && mv "workbench/$APP_NAME/package.json.new" "workbench/$APP_NAME/package.json"
|
|
pnpm install --no-frozen-lockfile
|
|
|
|
- name: Install Dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Download shared package builds
|
|
if: ${{ !matrix.app.canary }}
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: e2e-package-build-artifacts
|
|
path: packages
|
|
|
|
- name: Run Initial Build for Next.js canary
|
|
if: ${{ matrix.app.canary }}
|
|
run: pnpm turbo run build --filter='!./workbench/*'
|
|
|
|
- name: Setup PostgreSQL Database
|
|
run: ./packages/world-postgres/bin/setup.js
|
|
|
|
- name: Prepare workbench path
|
|
id: prepare-workbench
|
|
uses: ./.github/actions/prepare-workbench-path
|
|
with:
|
|
app-name: ${{ matrix.app.name }}
|
|
|
|
- name: Run Build Tests
|
|
run: pnpm vitest run packages/core/e2e/local-build.test.ts
|
|
env:
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
WORKBENCH_APP_PATH: ${{ steps.prepare-workbench.outputs.workbench_app_path }}
|
|
|
|
- name: Run E2E Tests
|
|
run: |
|
|
export PROD_SERVER_LOG_PATH="$GITHUB_WORKSPACE/prod-server-postgres-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.log"
|
|
(cd "$WORKBENCH_APP_PATH" && pnpm start 2>&1 | tee "$PROD_SERVER_LOG_PATH") &
|
|
echo "starting tests in 10 seconds" && sleep 10
|
|
pnpm run test:e2e --reporter=verbose --reporter=json --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-postgres-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json
|
|
env:
|
|
NODE_OPTIONS: "--enable-source-maps"
|
|
APP_NAME: ${{ matrix.app.name }}
|
|
WORKBENCH_APP_PATH: ${{ steps.prepare-workbench.outputs.workbench_app_path }}
|
|
DEPLOYMENT_URL: "http://localhost:${{ matrix.app.name == 'sveltekit' && '4173' || (matrix.app.name == 'astro' && '4321' || '3000') }}"
|
|
NEXT_CANARY: ${{ matrix.app.canary && '1' || '' }}
|
|
WORKFLOW_VM: ${{ matrix.app.vm || '' }}
|
|
|
|
- name: Generate E2E summary
|
|
if: always()
|
|
run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Postgres (${{ matrix.app.name }} - ${{ matrix.app.runLabel }})" >> $GITHUB_STEP_SUMMARY || true
|
|
|
|
- name: Upload E2E results
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: e2e-results-local-postgres-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}
|
|
path: |
|
|
e2e-local-postgres-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json
|
|
e2e-flaky-${{ matrix.app.name }}-local.json
|
|
e2e-infra-${{ matrix.app.name }}-local.json
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|
|
|
|
# Cross-language conformance: the same TypeScript e2e suite, driven against the
|
|
# Python workbench app instead of a JavaScript one. Deliberately its own job
|
|
# rather than a `getTestMatrix` row — every step in the two local lanes is JS
|
|
# toolchain (`pnpm dev` / `pnpm start`, the canary `next` patch, `dev.test.ts`
|
|
# and `local-build.test.ts`, and the per-framework `DEV_TEST_CONFIG` of
|
|
# compiler-generated paths), and none of it has a Python counterpart. The
|
|
# `e2e-vercel-prod` lane, whose rows only need a URL, does carry `python` as an
|
|
# ordinary matrix row.
|
|
#
|
|
# What this lane protects is the gate, not the fixtures it happens to run: a
|
|
# test that stops skipping correctly, or a fixture declared in
|
|
# `e2e-conformance.json` that the app stops registering, fails here and nowhere
|
|
# else.
|
|
e2e-python:
|
|
name: E2E Python Conformance
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
if: ${{ needs.ci-scope.outputs.fast-path != 'true' && !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }}
|
|
needs: [ci-scope, e2e-package-build]
|
|
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
WORKFLOW_PUBLIC_MANIFEST: '1'
|
|
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
build-packages: 'false'
|
|
|
|
# The Python app needs nothing from `packages/`, but the driver does: the
|
|
# vitest suite imports `@workflow/core` and shells out to the `workflow`
|
|
# CLI to inspect runs.
|
|
- name: Download shared package builds
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: e2e-package-build-artifacts
|
|
path: packages
|
|
|
|
# Pinned rather than floating to latest: `uv sync --locked` below is only
|
|
# a meaningful gate if the tool reading the lock is a known quantity.
|
|
# Pinning here rather than via `[tool.uv] required-version` is deliberate
|
|
# — that key would also bind the uv that `@vercel/python` bundles on the
|
|
# build image, which we do not control.
|
|
- name: Setup uv
|
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
with:
|
|
version: '0.12.2'
|
|
enable-cache: true
|
|
cache-dependency-glob: workbench/python/uv.lock
|
|
|
|
# `--locked` rather than `--frozen`: a `pyproject.toml` edit that was never
|
|
# relocked should fail the job, not be silently ignored.
|
|
- name: Install Python dependencies
|
|
working-directory: workbench/python
|
|
run: uv sync --locked
|
|
|
|
- name: Run E2E Tests
|
|
run: |
|
|
APP_LOG="$GITHUB_WORKSPACE/python-app.log"
|
|
rm -f "$APP_LOG"
|
|
(cd workbench/python && uv run uvicorn app:app --port 3000 > "$APP_LOG" 2>&1) &
|
|
|
|
for _ in $(seq 1 30); do
|
|
if curl -sf http://localhost:3000/.well-known/workflow/v1/manifest.json -o /dev/null; then
|
|
echo "app is up"
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
curl -sf http://localhost:3000/.well-known/workflow/v1/manifest.json | head -c 2000
|
|
echo
|
|
|
|
# Only `e2e.test.ts` — `e2e-agent.test.ts` (which `pnpm test:e2e` also
|
|
# runs) has no conformance gate and needs routes this app does not serve.
|
|
pnpm vitest run packages/core/e2e/e2e.test.ts \
|
|
--reporter=verbose --reporter=json \
|
|
--reporter=./packages/core/e2e/github-reporter.ts \
|
|
--outputFile=e2e-conformance-python.json
|
|
env:
|
|
NODE_OPTIONS: "--enable-source-maps"
|
|
APP_NAME: "python"
|
|
WORKBENCH_APP_PATH: ${{ github.workspace }}/workbench/python
|
|
DEPLOYMENT_URL: "http://localhost:3000"
|
|
|
|
- name: Print Python app logs
|
|
if: always()
|
|
run: cat "$GITHUB_WORKSPACE/python-app.log" || true
|
|
|
|
- name: Generate E2E summary
|
|
if: always()
|
|
run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Python Conformance" >> $GITHUB_STEP_SUMMARY || true
|
|
|
|
- name: Upload E2E results
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: e2e-results-conformance-python
|
|
path: |
|
|
e2e-conformance-python.json
|
|
e2e-flaky-python-local.json
|
|
e2e-infra-python-local.json
|
|
e2e-diagnostics-python-local.json
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|
|
|
|
e2e-windows:
|
|
name: E2E Windows Tests (${{ matrix.vm }})
|
|
runs-on: windows-latest
|
|
timeout-minutes: 30
|
|
needs: ci-scope
|
|
if: ${{ needs.ci-scope.outputs.fast-path != 'true' && !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
# Workflow VM engines: node:vm (default) and the opt-in QuickJS
|
|
# WASM engine (WORKFLOW_VM=quickjs).
|
|
vm: [node, quickjs]
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
WORKFLOW_PUBLIC_MANIFEST: '1'
|
|
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup Rust
|
|
uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1
|
|
with:
|
|
toolchain: stable
|
|
target: wasm32-unknown-unknown
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
|
|
|
- name: Setup Node.js 22.x
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22.x
|
|
cache: "pnpm"
|
|
|
|
- name: Install Dependencies
|
|
run: pnpm install --frozen-lockfile --ignore-scripts
|
|
|
|
- name: Run Initial Build
|
|
run: pnpm turbo run build --filter='!./workbench/*' --filter='!./docs'
|
|
|
|
- name: Run E2E Tests (Next.js)
|
|
run: |
|
|
cd workbench/nextjs-turbopack
|
|
$logFile = "$env:GITHUB_WORKSPACE/nextjs-server.log"
|
|
$env:DEV_SERVER_LOG_PATH = $logFile
|
|
# `$using:` only resolves PowerShell variables, not env vars, so
|
|
# copy MATRIX_VM into a session variable before Start-Job.
|
|
$matrixVm = $env:MATRIX_VM
|
|
$job = Start-Job -ScriptBlock { Set-Location $using:PWD; $env:WORKFLOW_VM = $using:matrixVm; pnpm dev *>&1 | Tee-Object -FilePath $using:logFile }
|
|
Start-Sleep -Seconds 15
|
|
cd ../..
|
|
|
|
# Fail-fast guards: if the Next.js dev server gets into a corrupted
|
|
# state during HMR (a recurring Turbopack-on-Windows issue where
|
|
# `packages/core/dist/*.js` becomes "MODULE_UNPARSABLE" and every
|
|
# subsequent request returns 500), the e2e suite would otherwise burn
|
|
# the full 30-minute job window polling stuck workflow runs before
|
|
# finally being cancelled. Below we (1) bail out if `dev.test.ts`
|
|
# fails, since that's the first thing to start hitting 500s, and
|
|
# (2) probe the server before the long e2e run to catch any other
|
|
# silent breakage.
|
|
#
|
|
# Get-DevServerStatus returns the HTTP status code of GET /api/chat,
|
|
# or 0 if the request never produced a response. /api/chat is a
|
|
# POST-only endpoint, so a healthy dev server responds with 405
|
|
# Method Not Allowed; we only treat 5xx (and no-response) as
|
|
# unhealthy.
|
|
function Get-DevServerStatus {
|
|
try {
|
|
$resp = Invoke-WebRequest -Uri "http://localhost:3000/api/chat" -UseBasicParsing -TimeoutSec 15 -ErrorAction Stop
|
|
return [int]$resp.StatusCode
|
|
} catch {
|
|
if ($_.Exception.Response) {
|
|
try { return [int]$_.Exception.Response.StatusCode } catch { return 0 }
|
|
}
|
|
return 0
|
|
}
|
|
}
|
|
|
|
$devExit = 0
|
|
pnpm vitest run packages/core/e2e/dev.test.ts
|
|
$devExit = $LASTEXITCODE
|
|
if ($devExit -ne 0) {
|
|
Write-Host "::warning title=dev.test.ts failed::dev.test.ts exited with code $devExit. Skipping the remainder of the e2e suite to avoid waiting for the 30-minute job timeout."
|
|
Stop-Job $job -ErrorAction SilentlyContinue
|
|
exit $devExit
|
|
}
|
|
|
|
# Sanity-check the server before kicking off the long e2e suite. This
|
|
# catches the case where Turbopack quietly went sideways (e.g. lost
|
|
# track of files in `packages/core/dist` after HMR on Windows) and is
|
|
# returning 500 for every request. Without this check the e2e suite
|
|
# would burn the full 30-minute job window polling stuck workflow
|
|
# runs before getting cancelled.
|
|
$status = Get-DevServerStatus
|
|
Write-Host "[health-check:pre-e2e] GET /api/chat -> $status"
|
|
if ($status -eq 0 -or $status -ge 500) {
|
|
Write-Host "::error title=Next.js dev server unhealthy::Dev server returned $status before e2e tests. Aborting to avoid the 30-minute job timeout. See the 'Print Next.js server logs' step for the underlying Turbopack error."
|
|
Stop-Job $job -ErrorAction SilentlyContinue
|
|
exit 1
|
|
}
|
|
|
|
pnpm run test:e2e --reporter=verbose --reporter=json --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-windows-nextjs-turbopack-$env:MATRIX_VM.json
|
|
$e2eExit = $LASTEXITCODE
|
|
Stop-Job $job -ErrorAction SilentlyContinue
|
|
exit $e2eExit
|
|
shell: powershell
|
|
env:
|
|
NODE_OPTIONS: "--enable-source-maps"
|
|
APP_NAME: "nextjs-turbopack"
|
|
DEPLOYMENT_URL: "http://localhost:3000"
|
|
# Use a real workflow file for the Windows HMR test. Most of this
|
|
# workbench's workflows are symlinks into workbench/example, and
|
|
# Windows/Turbopack can rebuild from the symlink path while still
|
|
# serving the previous module contents from the realpath cache.
|
|
DEV_TEST_CONFIG: '{"generatedStepRegistrationPath":"app/.well-known/workflow/v1/flow/__step_registrations.js","generatedWorkflowPath":"app/.well-known/workflow/v1/flow/route.js","apiFilePath":"app/api/chat/route.ts","apiFileImportPath":"../../..","port":3000,"testWorkflowFile":"96_many_steps.ts"}'
|
|
DEV_SERVER_LOG_PATH: "${{ github.workspace }}/nextjs-server.log"
|
|
WORKFLOW_DEV_HMR_LOGS: "1"
|
|
WORKFLOW_VM: ${{ matrix.vm }}
|
|
MATRIX_VM: ${{ matrix.vm }}
|
|
|
|
- name: Print Next.js server logs
|
|
if: always()
|
|
shell: powershell
|
|
run: |
|
|
$logFile = "$env:GITHUB_WORKSPACE/nextjs-server.log"
|
|
if (Test-Path $logFile) {
|
|
Write-Host "=== Next.js Server Logs ===" -ForegroundColor Cyan
|
|
Get-Content $logFile
|
|
Write-Host "=== End of Server Logs ===" -ForegroundColor Cyan
|
|
} else {
|
|
Write-Host "No server log file found at $logFile" -ForegroundColor Yellow
|
|
}
|
|
|
|
- name: Generate E2E summary
|
|
if: always()
|
|
shell: bash
|
|
env:
|
|
MATRIX_VM: ${{ matrix.vm }}
|
|
run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Windows (nextjs-turbopack - $MATRIX_VM)" >> $GITHUB_STEP_SUMMARY || true
|
|
|
|
- name: Upload E2E results
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: e2e-results-windows-nextjs-turbopack-${{ matrix.vm }}
|
|
path: |
|
|
e2e-windows-nextjs-turbopack-${{ matrix.vm }}.json
|
|
e2e-flaky-nextjs-turbopack-local.json
|
|
e2e-infra-nextjs-turbopack-local.json
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|
|
|
|
- name: Upload Next.js server logs
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: nextjs-server-logs-windows-${{ matrix.vm }}
|
|
path: nextjs-server.log
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|
|
|
|
# Community World E2E Tests (dynamically generated from worlds-manifest.json)
|
|
# Disabled on main: community worlds target spec v2 and do not yet support the
|
|
# CBOR queue transport used by world-vercel (see #1627 / #1658). They continue
|
|
# to run on `stable`, where spec v2 is still the primary compat target. Once
|
|
# the community worlds (mizzle-dev/workflow-worlds) ship CBOR support we can
|
|
# re-enable these jobs here.
|
|
getCommunityWorldsMatrix:
|
|
name: Get Community Worlds Matrix
|
|
runs-on: ubuntu-latest
|
|
if: false
|
|
outputs:
|
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
install-dependencies: 'false'
|
|
build-packages: 'false'
|
|
cache-pnpm: 'false'
|
|
|
|
- id: set-matrix
|
|
run: |
|
|
MATRIX_JSON="$(node ./scripts/create-community-worlds-matrix.mjs | jq -c .)"
|
|
echo "matrix=$MATRIX_JSON" >> "$GITHUB_OUTPUT"
|
|
|
|
e2e-community:
|
|
name: E2E Community World (${{ matrix.world.name }})
|
|
if: false
|
|
needs: getCommunityWorldsMatrix
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.getCommunityWorldsMatrix.outputs.matrix) }}
|
|
uses: ./.github/workflows/e2e-community-world.yml
|
|
with:
|
|
world-id: ${{ matrix.world.id }}
|
|
world-name: ${{ matrix.world.name }}
|
|
world-package: ${{ matrix.world.package }}
|
|
package-version: ${{ matrix.world.version }}
|
|
service-type: ${{ matrix.world.service-type }}
|
|
env-vars: ${{ matrix.world.env-vars }}
|
|
services: ${{ matrix.world.services }}
|
|
secrets: inherit
|
|
|
|
# Final job: Aggregate all E2E results and update PR comment
|
|
summary:
|
|
name: E2E Summary
|
|
runs-on: ubuntu-latest
|
|
needs: [ci-scope, e2e-vercel-prod, e2e-vercel-ws-transport, e2e-vercel-http-transport, e2e-local-dev, e2e-local-prod, e2e-local-postgres, e2e-python, e2e-windows]
|
|
if: always() && !cancelled() && needs.ci-scope.outputs.fast-path != 'true'
|
|
timeout-minutes: 10
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Download all E2E artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: e2e-results-*
|
|
path: e2e-results
|
|
merge-multiple: true
|
|
|
|
- name: List downloaded files
|
|
run: find e2e-results -type f -name "*.json" | sort || echo "No files found"
|
|
|
|
- name: Aggregate E2E results
|
|
id: aggregate
|
|
run: |
|
|
node .github/scripts/aggregate-e2e-results.js e2e-results --mode aggregate --run-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | tee e2e-summary.md >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Update PR comment with results
|
|
if: github.event_name == 'pull_request'
|
|
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
|
|
with:
|
|
header: e2e-test-results
|
|
path: e2e-summary.md
|
|
|
|
# Final required check: passes only when unit + all E2E jobs succeed.
|
|
# Community worlds are intentionally excluded — they are disabled on main.
|
|
e2e-required-check:
|
|
name: E2E Required Check
|
|
runs-on: ubuntu-latest
|
|
needs: [ci-scope, unit, e2e-package-build, e2e-vercel-prod, e2e-vercel-ws-transport, e2e-vercel-http-transport, e2e-local-dev, e2e-local-prod, e2e-local-postgres, e2e-python, e2e-windows]
|
|
if: always()
|
|
timeout-minutes: 5
|
|
|
|
steps:
|
|
- name: Validate E2E job statuses
|
|
env:
|
|
UNIT_STATUS: ${{ needs.unit.result }}
|
|
BUILD_STATUS: ${{ needs.e2e-package-build.result }}
|
|
VERCEL_STATUS: ${{ needs.e2e-vercel-prod.result }}
|
|
VERCEL_WS_STATUS: ${{ needs.e2e-vercel-ws-transport.result }}
|
|
VERCEL_HTTP_STATUS: ${{ needs.e2e-vercel-http-transport.result }}
|
|
LOCAL_DEV_STATUS: ${{ needs.e2e-local-dev.result }}
|
|
LOCAL_PROD_STATUS: ${{ needs.e2e-local-prod.result }}
|
|
POSTGRES_STATUS: ${{ needs.e2e-local-postgres.result }}
|
|
PYTHON_STATUS: ${{ needs.e2e-python.result }}
|
|
WINDOWS_STATUS: ${{ needs.e2e-windows.result }}
|
|
FAST_PATH: ${{ needs.ci-scope.outputs.fast-path }}
|
|
VALIDATION_FAST_PATH: ${{ needs.ci-scope.outputs.validation-fast-path }}
|
|
HAS_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }}
|
|
run: |
|
|
FAILED_JOBS=()
|
|
|
|
if [[ "$FAST_PATH" == "true" ]]; then
|
|
if [[ "$VALIDATION_FAST_PATH" != "true" && "$HAS_LABEL" != "true" && "$UNIT_STATUS" != "success" ]]; then
|
|
echo "UI-only fast path still requires unit tests; unit status was $UNIT_STATUS."
|
|
exit 1
|
|
fi
|
|
echo "Docs/UI-only change detected - no runtime E2E lanes are required."
|
|
exit 0
|
|
fi
|
|
|
|
# When workflow-server-test label is present, only check vercel-prod (others should be skipped)
|
|
# Otherwise, all jobs must succeed
|
|
if [[ "$HAS_LABEL" == "true" ]]; then
|
|
echo "workflow-server-test label detected - only checking required jobs"
|
|
[[ "$VERCEL_STATUS" == "success" ]] || FAILED_JOBS+=("e2e-vercel-prod ($VERCEL_STATUS)")
|
|
# This label is itself opt-in for the WS lane (see that job's `if`),
|
|
# so it runs here and is required.
|
|
[[ "$VERCEL_WS_STATUS" == "success" ]] || FAILED_JOBS+=("e2e-vercel-ws-transport ($VERCEL_WS_STATUS)")
|
|
# Unconditional, exactly like e2e-vercel-prod above: the HTTP
|
|
# lane has no opt-in label, so there is no case in which it is
|
|
# legitimately skipped while the rest of this branch runs.
|
|
[[ "$VERCEL_HTTP_STATUS" == "success" ]] || FAILED_JOBS+=("e2e-vercel-http-transport ($VERCEL_HTTP_STATUS)")
|
|
|
|
# Verify other jobs were actually skipped
|
|
[[ "$UNIT_STATUS" == "skipped" ]] || echo "Warning: unit was not skipped ($UNIT_STATUS)"
|
|
[[ "$LOCAL_DEV_STATUS" == "skipped" ]] || echo "Warning: e2e-local-dev was not skipped ($LOCAL_DEV_STATUS)"
|
|
[[ "$LOCAL_PROD_STATUS" == "skipped" ]] || echo "Warning: e2e-local-prod was not skipped ($LOCAL_PROD_STATUS)"
|
|
[[ "$POSTGRES_STATUS" == "skipped" ]] || echo "Warning: e2e-local-postgres was not skipped ($POSTGRES_STATUS)"
|
|
[[ "$PYTHON_STATUS" == "skipped" ]] || echo "Warning: e2e-python was not skipped ($PYTHON_STATUS)"
|
|
[[ "$WINDOWS_STATUS" == "skipped" ]] || echo "Warning: e2e-windows was not skipped ($WINDOWS_STATUS)"
|
|
else
|
|
echo "Standard PR - checking all jobs"
|
|
[[ "$UNIT_STATUS" == "success" ]] || FAILED_JOBS+=("unit ($UNIT_STATUS)")
|
|
[[ "$BUILD_STATUS" == "success" ]] || FAILED_JOBS+=("e2e-package-build ($BUILD_STATUS)")
|
|
[[ "$VERCEL_STATUS" == "success" ]] || FAILED_JOBS+=("e2e-vercel-prod ($VERCEL_STATUS)")
|
|
[[ "$VERCEL_WS_STATUS" == "success" ]] || FAILED_JOBS+=("e2e-vercel-ws-transport ($VERCEL_WS_STATUS)")
|
|
[[ "$VERCEL_HTTP_STATUS" == "success" ]] || FAILED_JOBS+=("e2e-vercel-http-transport ($VERCEL_HTTP_STATUS)")
|
|
[[ "$LOCAL_DEV_STATUS" == "success" ]] || FAILED_JOBS+=("e2e-local-dev ($LOCAL_DEV_STATUS)")
|
|
[[ "$LOCAL_PROD_STATUS" == "success" ]] || FAILED_JOBS+=("e2e-local-prod ($LOCAL_PROD_STATUS)")
|
|
[[ "$POSTGRES_STATUS" == "success" ]] || FAILED_JOBS+=("e2e-local-postgres ($POSTGRES_STATUS)")
|
|
[[ "$PYTHON_STATUS" == "success" ]] || FAILED_JOBS+=("e2e-python ($PYTHON_STATUS)")
|
|
[[ "$WINDOWS_STATUS" == "success" ]] || FAILED_JOBS+=("e2e-windows ($WINDOWS_STATUS)")
|
|
fi
|
|
|
|
if [[ ${#FAILED_JOBS[@]} -gt 0 ]]; then
|
|
echo "The following E2E jobs did not succeed:"
|
|
printf '%s\n' "${FAILED_JOBS[@]}"
|
|
exit 1
|
|
fi
|
|
|
|
echo "All required E2E jobs succeeded."
|
|
|
|
# Publish E2E results to GitHub Pages (main branch only)
|
|
publish-results:
|
|
name: Publish E2E Results
|
|
runs-on: ubuntu-latest
|
|
needs: [summary]
|
|
# Use always() because summary uses always() - without it this job gets skipped when any E2E test fails
|
|
if: always() && !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
timeout-minutes: 5
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
- name: Download all E2E artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: e2e-results-*
|
|
path: e2e-results
|
|
merge-multiple: true
|
|
|
|
- name: Generate docs data
|
|
run: |
|
|
mkdir -p docs-data
|
|
node .github/scripts/generate-docs-data.js \
|
|
--type e2e \
|
|
--results-dir e2e-results \
|
|
--output docs-data/e2e-results.json \
|
|
--commit "${{ github.sha }}" \
|
|
--branch "${{ github.ref_name }}"
|
|
|
|
- name: Deploy to GitHub Pages
|
|
uses: ./.github/actions/publish-to-gh-pages
|
|
with:
|
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
source-dir: ./docs-data
|
|
destination-dir: ci
|