Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/core-cross-environment-start-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflow/core': minor
---

Stamp the creating client's environment into the queue message's `runInput`, and refuse a queue delivery whose run was created in a different environment than the consuming deployment runs in, so one run ID can no longer be forked into two environments.
9 changes: 9 additions & 0 deletions .changeset/event-count-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'workflow': minor
'@workflow/core': minor
'@workflow/world-vercel': minor
'@workflow/world': minor
'@workflow/errors': minor
---

Strengthen the event-creation precondition guard: replay-context writes now also send the number of loaded events, so a snapshot that is missing an event is rejected instead of committing a divergent event log, and a rejection restarts the replay in-process (consuming the events a world may attach to the rejection) rather than re-committing the rejected payload or re-invoking over the queue. `@workflow/world-local` and `@workflow/world-postgres` do not implement the check.
7 changes: 7 additions & 0 deletions .changeset/report-replay-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@workflow/core': patch
'@workflow/world': patch
'@workflow/world-vercel': patch
---

Report replay-divergence counts on event writes that recover or exhaust replay retries.
7 changes: 7 additions & 0 deletions .changeset/resilient-resume-hook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@workflow/core": minor
"workflow": minor
"@workflow/world": minor
---

Make `resumeHook()` resilient to transient `hook_received` event write failures (429/5xx) by carrying the payload on the queue message for the runtime to materialize. Returned `Hook` gets a new `resilientResume: true` flag when this fallback path is taken.
4 changes: 4 additions & 0 deletions .changeset/skip-changeset-release-deploys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Stop deploying the `changeset-release/main` branch, and run that PR's Vercel e2e lanes against main's production deployment instead.
5 changes: 5 additions & 0 deletions .changeset/world-get-environment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflow/world': minor
---

Add an optional `World.getEnvironment()` hook and an optional `environment` field on `RunInputSchema`, so the environment a run was created in can travel with its queue message.
5 changes: 5 additions & 0 deletions .changeset/world-local-bounded-deliveries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflow/world-local': patch
---

Bound stalled local queue deliveries and redeliver their durable messages instead of waiting indefinitely.
5 changes: 5 additions & 0 deletions .changeset/world-vercel-get-environment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflow/world-vercel': minor
---

Implement `getEnvironment()`, reporting the environment this client's writes are attributed to from the same resolution that produces the `x-vercel-environment` request header.
9 changes: 9 additions & 0 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ jobs:
needs: ci-scope
if: >-
github.event_name == 'pull_request' &&
!startsWith(github.head_ref, 'changeset-release/') &&
!contains(github.event.pull_request.labels.*.name, 'workflow-server-test') &&
needs.ci-scope.outputs.fast-path != 'true'
timeout-minutes: 5
Expand Down Expand Up @@ -103,7 +104,14 @@ jobs:
name: Benchmark (${{ matrix.target.world }}, ${{ matrix.target.app }})
runs-on: ubuntu-latest
needs: ci-scope
# `changeset-release/*` PRs are not deployed (every project's vercel.json
# sets `git.deploymentEnabled` false for that branch, because the branch is
# force-pushed to main's HEAD SHA and a same-SHA preview deployment
# clobbers the per-SHA Vercel commit status), so there is nothing to wait
# for below. Benchmarking them would also be meaningless: the code is
# main's, so it would only compare main's baseline against itself.
if: >-
!startsWith(github.head_ref, 'changeset-release/') &&
!contains(github.event.pull_request.labels.*.name, 'workflow-server-test') &&
(needs.ci-scope.outputs.fast-path != 'true' ||
github.event_name == 'workflow_dispatch')
Expand Down Expand Up @@ -185,6 +193,7 @@ jobs:
if: >-
always() && !cancelled() &&
github.event_name == 'pull_request' &&
!startsWith(github.head_ref, 'changeset-release/') &&
!contains(github.event.pull_request.labels.*.name, 'workflow-server-test') &&
needs.ci-scope.outputs.fast-path != 'true'
timeout-minutes: 10
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/docs-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ jobs:
docs-preview-smoke:
name: Docs Preview Smoke Checks
runs-on: ubuntu-latest
# `changeset-release/*` PRs are not deployed (every project's vercel.json
# sets `git.deploymentEnabled` false for that branch, because the branch is
# force-pushed to main's HEAD SHA and a same-SHA preview deployment
# clobbers the per-SHA Vercel commit status), so there is no preview to
# smoke-test. Their docs content is identical to main's, which this job
# already checked against production on the push that produced the bump.
if: ${{ !startsWith(github.head_ref, 'changeset-release/') }}
timeout-minutes: 5
permissions:
id-token: write
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/tarballs-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ jobs:
tarballs-preview-smoke:
name: Tarballs Preview Smoke Checks
runs-on: ubuntu-latest
# `changeset-release/*` PRs are not deployed (every project's vercel.json
# sets `git.deploymentEnabled` false for that branch, because the branch is
# force-pushed to main's HEAD SHA and a same-SHA preview deployment
# clobbers the per-SHA Vercel commit status), so there is no preview to
# smoke-test. The version-bumped tarballs get this same check on the push
# to main that follows the version PR merge.
if: ${{ !startsWith(github.head_ref, 'changeset-release/') }}
timeout-minutes: 10
permissions:
contents: read
Expand Down
75 changes: 62 additions & 13 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -379,15 +379,43 @@ jobs:
- 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"
Expand All @@ -396,10 +424,12 @@ jobs:
run: pnpm run test:e2e --reporter=verbose --reporter=json --reporter=./packages/core/e2e/github-reporter.ts "--outputFile=e2e-vercel-prod-$APP_NAME.json"
env:
NODE_OPTIONS: "--enable-source-maps"
DEPLOYMENT_URL: ${{ steps.waitForDeployment.outputs.deployment-url }}
VERCEL_DEPLOYMENT_ID: ${{ steps.waitForDeployment.outputs.deployment-id }}
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_VERCEL_ENV: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}
# 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 }}
Expand All @@ -413,8 +443,11 @@ jobs:
# 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.
VERCEL_WORKFLOW_SERVER_URL: ${{ github.ref != 'refs/heads/main' && secrets.VERCEL_WORKFLOW_SERVER_URL || '' }}
# 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()
Expand All @@ -423,8 +456,8 @@ jobs:
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' && 'production' || 'preview' }}
VERCEL_DEPLOYMENT_ID: ${{ steps.waitForDeployment.outputs.deployment-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

Expand Down Expand Up @@ -485,30 +518,46 @@ jobs:
- 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 }}
VERCEL_DEPLOYMENT_ID: ${{ steps.waitForDeployment.outputs.deployment-id }}
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"
WORKFLOW_VERCEL_ENV: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}
# 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' && secrets.VERCEL_WORKFLOW_SERVER_URL || '' }}
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()
Expand All @@ -517,8 +566,8 @@ jobs:
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' && 'production' || 'preview' }}
VERCEL_DEPLOYMENT_ID: ${{ steps.waitForDeployment.outputs.deployment-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 }}
run: node .github/scripts/fetch-e2e-runtime-logs.mjs

- name: Upload E2E results
Expand Down
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@ Both branches trigger the release workflow (`.github/workflows/release.yml`) on

When backporting changes to `stable`, any conflicts involving docs app files (outside of `docs/content/`) or `skills/` files should be resolved by keeping the `stable` branch version (discarding the incoming change from `main`). Conflicts in `docs/content/` should be resolved normally. The backport GitHub Action handles this automatically.

#### The `changeset-release/main` branch is never deployed

Every Vercel project rooted in this repo sets `git.deploymentEnabled` to `false` for `changeset-release/main` in its `vercel.json`. **When you add a new Vercel project, add that key to its `vercel.json` too.**

The changesets action force-pushes `changeset-release/main`, and it can point at exactly main's HEAD SHA. Vercel keeps one commit status per project per SHA, so a preview deployment of that branch overwrites the production deployment's status for the same commit — and `vercel/wait-for-deployment-action`, which reads the deployment ID out of that status, then hands a production e2e run a preview deployment ID, forking the run across environments.

Because those PRs have no deployment of their own, CI treats them specially: the Vercel e2e lanes in `tests.yml` run `vercel/wait-for-deployment-action` a second way — `environment: production` with `sha` pinned to the PR's base SHA — so they test main's production deployment and run as `production`. (The commit-status ID that action reads is unambiguous for main SHAs precisely because this repo no longer deploys `changeset-release/main`, the only branch that ever deployed a commit main also deployed.) The deployment-dependent jobs in `docs-checks.yml`, `tarballs-checks.yml`, and `benchmarks.yml` are skipped. Anything new that waits on a deployment needs the same treatment.

### Changesets

- `workflow` and `@workflow/core` use changesets' "fixed" versioning strategy — they always have the same version number
Expand Down
6 changes: 3 additions & 3 deletions docs/app/[lang]/(home)/components/cta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { Button } from '@vercel/geistdocs/components/button';
import Link from 'next/link';

export const CTA = () => (
<section className="px-8 sm:px-12 py-10 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<h2 className="font-semibold text-xl tracking-tight sm:text-2xl md:text-3xl lg:text-[40px]">
<section className="py-10 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<h2 className="text-heading-20 sm:text-heading-24 md:text-heading-32 lg:text-heading-40">
Create your first workflow today.
</h2>
<Button asChild size="lg" className="w-fit h-10">
<Button asChild size="lg" className="w-fit h-10 rounded-full">
<Link href="/docs/getting-started">Get started</Link>
</Button>
</section>
Expand Down
6 changes: 2 additions & 4 deletions docs/app/[lang]/(home)/components/features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@ const data = [
];

export const Features = () => (
<div className="px-4 py-8 sm:py-12 sm:px-12 grid md:grid-cols-3 gap-8">
<div className="py-8 sm:py-12 grid md:grid-cols-3 gap-8">
{data.map((item) => (
<div key={item.title}>
<h3 className="mb-2 font-semibold text-lg tracking-tight">
{item.title}
</h3>
<h3 className="mb-2 text-heading-20">{item.title}</h3>
<p className="text-muted-foreground">{item.description}</p>
</div>
))}
Expand Down
6 changes: 3 additions & 3 deletions docs/app/[lang]/(home)/components/frameworks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -719,16 +719,16 @@ export const Next = (props: ComponentProps<'svg'>) => (

export const Frameworks = () => {
return (
<div className="flex flex-col items-center gap-8 px-4 py-12 sm:py-16 sm:px-12 text-center">
<div className="flex flex-col items-start sm:items-center gap-8 py-12 sm:py-16 text-left sm:text-center">
<div className="flex flex-col gap-3 max-w-xl">
<h2 className="font-semibold text-xl tracking-tight sm:text-2xl md:text-3xl">
<h2 className="text-heading-20 sm:text-heading-24 md:text-heading-32 lg:text-heading-40">
Universally Compatible
</h2>
<p className="text-muted-foreground sm:text-lg">
Works with the frameworks you already use with more coming soon.
</p>
</div>
<div className="flex flex-wrap justify-center gap-6 items-center max-w-xl">
<div className="flex flex-wrap justify-start sm:justify-center gap-6 items-center max-w-xl">
<Link
href="/docs/getting-started/next"
className="hover:opacity-70 transition-opacity duration-200"
Expand Down
6 changes: 3 additions & 3 deletions docs/app/[lang]/(home)/components/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ type HeroProps = {

export const Hero = ({ title, description }: HeroProps) => {
return (
<section className="mt-[var(--fd-nav-height)] space-y-6 px-4 pt-24 sm:pt-32 pb-32 text-center">
<section className="mt-[var(--fd-nav-height)] space-y-6 pt-24 sm:pt-32 pb-32 text-center">
<div className="mx-auto w-full max-w-4xl space-y-5">
<h1 className="text-center font-semibold text-4xl leading-[1.1] tracking-tight lg:font-semibold sm:text-5xl! xl:text-6xl! text-balance">
<h1 className="text-center text-balance text-heading-40 sm:text-heading-48 xl:text-heading-64">
{title}
</h1>
<p className="text-balance max-w-3xl mx-auto text-muted-foreground sm:text-xl leading-relaxed">
<p className="text-balance max-w-3xl mx-auto text-muted-foreground text-copy-18 leading-relaxed">
<span className="font-mono text-base bg-accent inline-block px-2 py-0 rounded-sm border border-border">
use workflow
</span>{' '}
Expand Down
Loading
Loading