Skip to content

Commit c089357

Browse files
Merge branch 'main' into peter/issue-1179
2 parents 5d3e7e6 + ff336a0 commit c089357

64 files changed

Lines changed: 3594 additions & 441 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/fix-tarballs-build.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
Fix the `tarballs` Vercel project so workspace dependencies are built before packing — without this, every preview tarball ships with an empty `dist/` directory and downstream installs fail to resolve `dist/*` entry points.

.changeset/friendlier-errors.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@workflow/core": patch
3+
"@workflow/errors": patch
4+
"@workflow/builders": patch
5+
"@workflow/utils": patch
6+
---
7+
8+
Friendlier workflow error messages. New `SerializationError`, `WorkflowBuildError`, and structured context-violation classes (e.g. `NotInWorkflowContextError`) with actionable hints and docs links applied to user-facing throw sites; `FatalError.is()` recognizes any error with `fatal: true` so context violations and serialization failures now fail fast instead of burning retry attempts. Runtime logs are namespaced under `[workflow-sdk]` and gain `errorAttribution` (`user` vs `sdk`) plus class-aware hints
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

.changeset/pretty-log-format.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@workflow/core": patch
3+
"@workflow/errors": patch
4+
---
5+
6+
Replace `util.inspect`'s default object dump for runtime structured-log metadata with an opinionated, workflow-aware formatter. The runtime logger uses color-coded metadata blocks.

.github/workflows/benchmarks.yml

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ jobs:
3939
if: steps.find-comment.outputs.comment-id != ''
4040
id: get-comment
4141
uses: actions/github-script@v7
42+
env:
43+
STARTED_AT: ${{ github.run_started_at }}
4244
with:
4345
script: |
46+
const fs = require('fs');
4447
const comment = await github.rest.issues.getComment({
4548
owner: context.repo.owner,
4649
repo: context.repo.repo,
@@ -49,7 +52,7 @@ jobs:
4952
// Extract the results section (everything after the header and running message)
5053
const body = comment.data.body;
5154
// Remove any existing stale warning and running message
52-
let resultsSection = body
55+
const resultsSection = body
5356
.replace(/<!-- benchmark-results -->\n## 📊 Benchmark Results\n\n> ⚠️ \*\*Results below are stale\*\*[^\n]*\n\n/g, '')
5457
.replace(/<!-- benchmark-results -->\n## 📊 Benchmark Results\n\n/g, '')
5558
.replace(/⏳ \*\*Benchmarks are running\.\.\.\*\*\n\n---\n_Started at:[^_]*_\n\n---\n\n/g, '')
@@ -58,8 +61,25 @@ jobs:
5861
5962
// If there's actual content left (benchmark tables), save it
6063
if (resultsSection && resultsSection.includes('|')) {
64+
// Write the full stale-banner message to disk and pass the
65+
// path to the sticky-pull-request-comment action below.
66+
// Inlining the previous results via `message:` blew past
67+
// ARG_MAX once the benchmark tables grew large enough.
68+
const startedAt = process.env.STARTED_AT;
69+
const message =
70+
'<!-- benchmark-results -->\n' +
71+
'## 📊 Benchmark Results\n\n' +
72+
'> ⚠️ **Results below are stale** and not from the latest commit. This comment will be updated when CI completes on the latest run.\n\n' +
73+
'⏳ **Benchmarks are running...**\n\n' +
74+
'---\n' +
75+
`_Started at: ${startedAt}_\n\n` +
76+
'---\n\n' +
77+
resultsSection +
78+
'\n';
79+
const path = `${process.env.RUNNER_TEMP}/stale-comment.md`;
80+
fs.writeFileSync(path, message);
6181
core.setOutput('has-results', 'true');
62-
core.setOutput('previous-results', resultsSection);
82+
core.setOutput('stale-comment-path', path);
6383
} else {
6484
core.setOutput('has-results', 'false');
6585
}
@@ -78,27 +98,14 @@ jobs:
7898
This comment will be updated with the results when the benchmarks complete.
7999
80100
---
81-
_Started at: ${{ github.event.pull_request.updated_at }}_
101+
_Started at: ${{ github.run_started_at }}_
82102
83103
- name: Update existing benchmark comment with stale warning
84104
if: steps.find-comment.outputs.comment-id != '' && steps.get-comment.outputs.has-results == 'true'
85105
uses: marocchino/sticky-pull-request-comment@v2
86106
with:
87107
header: benchmark-results
88-
message: |
89-
<!-- benchmark-results -->
90-
## 📊 Benchmark Results
91-
92-
> ⚠️ **Results below are stale** and not from the latest commit. This comment will be updated when CI completes on the latest run.
93-
94-
⏳ **Benchmarks are running...**
95-
96-
---
97-
_Started at: ${{ github.event.pull_request.updated_at }}_
98-
99-
---
100-
101-
${{ steps.get-comment.outputs.previous-results }}
108+
path: ${{ steps.get-comment.outputs.stale-comment-path }}
102109

103110
- name: Update existing benchmark comment without results
104111
if: steps.find-comment.outputs.comment-id != '' && steps.get-comment.outputs.has-results != 'true'
@@ -114,7 +121,7 @@ jobs:
114121
This comment will be updated with the results when the benchmarks complete.
115122
116123
---
117-
_Started at: ${{ github.event.pull_request.updated_at }}_
124+
_Started at: ${{ github.run_started_at }}_
118125
119126
# Phase 1: Build all packages (not workbenches)
120127
build:
@@ -449,6 +456,12 @@ jobs:
449456
with:
450457
install-dependencies: 'false'
451458
build-packages: 'false'
459+
# This job never runs `pnpm install`, so the pnpm store path
460+
# never exists. The post-job `actions/setup-node@v4` cache-save
461+
# then fails with "Path Validation Error" and red-X's the job.
462+
# Disable the cache to keep the matrix step the only failure
463+
# surface.
464+
cache-pnpm: 'false'
452465

453466
- id: set-matrix
454467
run: |

.github/workflows/tests.yml

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ jobs:
3838
if: steps.find-comment.outputs.comment-id != ''
3939
id: get-comment
4040
uses: actions/github-script@v7
41+
env:
42+
STARTED_AT: ${{ github.run_started_at }}
4143
with:
4244
script: |
45+
const fs = require('fs');
4346
const comment = await github.rest.issues.getComment({
4447
owner: context.repo.owner,
4548
repo: context.repo.repo,
@@ -49,15 +52,32 @@ jobs:
4952
// Check if there are actual results (tables)
5053
if (body.includes('|') && body.includes('Passed')) {
5154
// Extract results section (everything after header)
52-
let resultsSection = body
55+
const resultsSection = body
5356
.replace(/<!-- e2e-test-results -->\n## 🧪 E2E Test Results\n\n> ⚠️ \*\*Results below are stale\*\*[^\n]*\n\n/g, '')
5457
.replace(/<!-- e2e-test-results -->\n## 🧪 E2E Test Results\n\n/g, '')
5558
.replace(/⏳ \*\*Tests are running\.\.\.\*\*\n\n---\n_Started at:[^_]*_\n\n---\n\n/g, '')
5659
.replace(/⏳ \*\*Tests are running\.\.\.\*\*\n\n---\n_Started at:[^_]*_/g, '')
5760
.trim();
5861
if (resultsSection && resultsSection.includes('|')) {
62+
// Write the full stale-banner message to disk and pass the
63+
// path to the sticky-pull-request-comment action below.
64+
// Inlining the previous results via `message:` blew past
65+
// ARG_MAX once the matrix doubled (snapshot + replay).
66+
const startedAt = process.env.STARTED_AT;
67+
const message =
68+
'<!-- e2e-test-results -->\n' +
69+
'## 🧪 E2E Test Results\n\n' +
70+
'> ⚠️ **Results below are stale** and not from the latest commit. This comment will be updated when CI completes on the latest run.\n\n' +
71+
'⏳ **Tests are running...**\n\n' +
72+
'---\n' +
73+
`_Started at: ${startedAt}_\n\n` +
74+
'---\n\n' +
75+
resultsSection +
76+
'\n';
77+
const path = `${process.env.RUNNER_TEMP}/stale-comment.md`;
78+
fs.writeFileSync(path, message);
5979
core.setOutput('has-results', 'true');
60-
core.setOutput('previous-results', resultsSection);
80+
core.setOutput('stale-comment-path', path);
6181
} else {
6282
core.setOutput('has-results', 'false');
6383
}
@@ -79,27 +99,14 @@ jobs:
7999
This comment will be updated with the results when the tests complete.
80100
81101
---
82-
_Started at: ${{ github.event.pull_request.updated_at }}_
102+
_Started at: ${{ github.run_started_at }}_
83103
84104
- name: Update existing test comment with stale warning
85105
if: steps.find-comment.outputs.comment-id != '' && steps.get-comment.outputs.has-results == 'true'
86106
uses: marocchino/sticky-pull-request-comment@v2
87107
with:
88108
header: e2e-test-results
89-
message: |
90-
<!-- e2e-test-results -->
91-
## 🧪 E2E Test Results
92-
93-
> ⚠️ **Results below are stale** and not from the latest commit. This comment will be updated when CI completes on the latest run.
94-
95-
⏳ **Tests are running...**
96-
97-
---
98-
_Started at: ${{ github.event.pull_request.updated_at }}_
99-
100-
---
101-
102-
${{ steps.get-comment.outputs.previous-results }}
109+
path: ${{ steps.get-comment.outputs.stale-comment-path }}
103110

104111
- name: Update existing test comment without results
105112
if: steps.find-comment.outputs.comment-id != '' && steps.get-comment.outputs.has-results != 'true'
@@ -115,7 +122,7 @@ jobs:
115122
This comment will be updated with the results when the tests complete.
116123
117124
---
118-
_Started at: ${{ github.event.pull_request.updated_at }}_
125+
_Started at: ${{ github.run_started_at }}_
119126
120127
unit:
121128
name: Unit Tests (${{ matrix.os }})

packages/builders/src/base-builder.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
22
import { mkdir, readFile, realpath, rename, writeFile } from 'node:fs/promises';
33
import { basename, dirname, join, relative, resolve } from 'node:path';
44
import { promisify } from 'node:util';
5+
import { WorkflowBuildError } from '@workflow/errors';
56
import { pluralize } from '@workflow/utils';
67
import chalk from 'chalk';
78
import enhancedResolveOriginal from 'enhanced-resolve';
@@ -343,8 +344,11 @@ export abstract class BaseBuilder {
343344
}
344345

345346
if (throwOnError) {
346-
throw new Error(
347-
`Build failed during ${phase}:\n${errorMessages.join('\n')}`
347+
throw new WorkflowBuildError(
348+
`Build failed during ${phase}:\n${errorMessages.join('\n')}`,
349+
{
350+
hint: `Review the esbuild errors above — they come from the ${phase} bundle. Fix the offending source files and re-run the build.`,
351+
}
348352
);
349353
}
350354
}
@@ -421,13 +425,12 @@ export abstract class BaseBuilder {
421425
dirname(outfile),
422426
'workflow/internal/builtins'
423427
).catch((err) => {
424-
throw new Error(
425-
[
426-
chalk.red('Failed to resolve built-in steps sources.'),
427-
`${chalk.yellow.bold('hint:')} run \`${chalk.cyan.italic('npm install workflow')}\` to resolve this issue.`,
428-
'',
429-
`Caused by: ${chalk.red(String(err))}`,
430-
].join('\n')
428+
throw new WorkflowBuildError(
429+
`Failed to resolve built-in steps sources.\n\nCaused by: ${String(err)}`,
430+
{
431+
hint: 'run `pnpm install workflow` to resolve this issue.',
432+
cause: err,
433+
}
431434
);
432435
});
433436

@@ -856,7 +859,9 @@ export abstract class BaseBuilder {
856859
!interimBundle.outputFiles ||
857860
interimBundle.outputFiles.length === 0
858861
) {
859-
throw new Error('No output files generated from esbuild');
862+
throw new WorkflowBuildError('No output files generated from esbuild', {
863+
hint: 'This usually indicates a misconfigured entry point or an empty workflow directory. Check that your workflow files contain a `"use workflow"` or `"use step"` directive.',
864+
});
860865
}
861866

862867
// Serde compliance warnings: check if workflow bundle has Node.js imports

packages/core/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@
6464
"types": "./dist/encryption.d.ts",
6565
"default": "./dist/encryption.js"
6666
},
67+
"./describe-error": {
68+
"types": "./dist/describe-error.d.ts",
69+
"default": "./dist/describe-error.js"
70+
},
6771
"./_workflow": "./dist/workflow/index.js"
6872
},
6973
"scripts": {

packages/core/src/capture-stack.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* V8-only (Node, Bun, Chrome, Deno). Rewrites `err.stack` so the top frame is
3+
* the caller of `stackStartFn` instead of the framework function that threw.
4+
* Without this, terminal overlays (Next.js, Turbopack, VS Code) render the
5+
* code frame at our `throw` site inside `@workflow/core`, which is useless
6+
* to the user.
7+
*
8+
* No-op on engines that don't expose `Error.captureStackTrace` — the stack
9+
* degrades gracefully to the default behavior.
10+
*
11+
* Kept in its own tiny module so callers that can't participate in the
12+
* `context-errors.ts` ↔ `workflow/get-workflow-metadata.ts` import cycle can
13+
* still pull in the helper without pulling in the full error classes.
14+
*/
15+
export function redirectStackToCaller(
16+
err: Error,
17+
// biome-ignore lint/complexity/noBannedTypes: signature matches Error.captureStackTrace
18+
stackStartFn: Function
19+
): void {
20+
const capture = (
21+
Error as unknown as {
22+
captureStackTrace?: (target: object, fn: Function) => void;
23+
}
24+
).captureStackTrace;
25+
capture?.(err, stackStartFn);
26+
}

0 commit comments

Comments
 (0)