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/framing-capability-cutoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/core": patch
---

Correct the byte-stream framing capability cutoff so framed byte streams are never written to deployments that cannot decode them
28 changes: 22 additions & 6 deletions packages/core/src/capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,26 +77,42 @@ describe('getRunCapabilities', () => {

it.each([
// pre-cutoff: encryption introduced in 4.2.0-beta.64; framing ships
// in the stable 4.5.0 release, so any earlier 4.x version is too old
// in the stable 4.6.0 release, so any earlier 4.x version is too old.
// 4.5.0 in particular was published before the framing backport
// merged, so it silently pipes framed bytes through to the user.
'4.2.0-beta.64',
'4.2.0',
'4.4.0',
'4.5.0',
'4.5.1',
// betas published before framing shipped must read as raw —
// a false positive here means framed writes to a consumer that
// cannot unframe them
'4.5.0-beta.14',
'4.6.0-beta.14',
// 5.0.0 betas below beta.15 predate framing but compare above every
// 4.x version, so they specifically must not match a plain >=4.6.0
// check (runs created on those deployments record these versions)
'5.0.0-beta.0',
'5.0.0-beta.14',
])('is false for pre-framing version %s', (version) => {
expect(getRunCapabilities(version).framedByteStreams).toBe(false);
});

it('is true at the exact cutoff version (4.5.0)', () => {
expect(getRunCapabilities('4.5.0').framedByteStreams).toBe(true);
it('is true at the exact stable cutoff version (4.6.0)', () => {
expect(getRunCapabilities('4.6.0').framedByteStreams).toBe(true);
});

it('is true at the exact beta cutoff version (5.0.0-beta.15)', () => {
expect(getRunCapabilities('5.0.0-beta.15').framedByteStreams).toBe(true);
});

it.each([
'4.5.1',
'4.6.0',
'4.6.1',
'4.7.0',
'5.0.0-beta.16',
'5.0.0',
// future prereleases above the cutoff must be recognized as capable
'5.1.0-beta.0',
'6.0.0',
])('is true for post-framing version %s', (version) => {
expect(getRunCapabilities(version).framedByteStreams).toBe(true);
Expand Down
41 changes: 30 additions & 11 deletions packages/core/src/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
* - `encr` (AES-256-GCM encryption): added in `4.2.0-beta.64`
* Commit: 7618ac36 "Wire AES-GCM encryption into serialization layer (#1251)"
* https://github.com/vercel/workflow/commit/7618ac36
* - `framedByteStreams` (wire-level chunk framing for byte streams): added in `4.5.0`
* - `framedByteStreams` (wire-level chunk framing for byte streams): added in
* `4.6.0` on the stable line and `5.0.0-beta.15` on the beta line
* Commit: f2ad726 "Add wire-level framing for byte streams (#1853)"
* https://github.com/vercel/workflow/commit/f2ad726
*/

import semver from 'semver';
Expand Down Expand Up @@ -75,17 +78,31 @@ const FORMAT_VERSION_TABLE: ReadonlyArray<{

/**
* Maps non-format capability flags (booleans on `RunCapabilities`) to the
* minimum `@workflow/core` version that introduced support for them.
* semver range of `@workflow/core` versions that support them.
*
* These are ranges rather than simple minimum versions because published
* release lines interleave: every `5.0.0-beta.x` compares above every
* `4.x` version, but betas below `5.0.0-beta.15` predate byte-stream
* framing. A plain `>= 4.6.0` check would classify those betas as
* framing-capable and write framed bytes to a consumer that cannot
* unframe them (silent corruption). Classifying a capable version as
* incapable is always safe — it merely falls back to the legacy format.
*
* Ranges are evaluated with `includePrerelease: true` so that future
* prereleases above a cutoff (e.g. `5.1.0-beta.0`) are recognized as
* capable.
*/
const CAPABILITY_VERSION_TABLE: ReadonlyArray<{
capability: keyof Omit<RunCapabilities, 'supportedFormats'>;
minVersion: string;
// TODO(release): verify this matches the actual version that ships byte-stream
// framing. If a "Version Packages (beta)" PR merges before this change, bump
// to the next beta. A too-low cutoff makes new producers write framed bytes to
// consumers that cannot unframe them (silent corruption); too-high merely
// delays the optimization (safe).
}> = [{ capability: 'framedByteStreams', minVersion: '4.5.0' }];
range: string;
}> = [
{
capability: 'framedByteStreams',
// Stable line: shipped in 4.6.0. Beta line: shipped in 5.0.0-beta.15;
// 5.0.0-beta.0 through 5.0.0-beta.14 must read as raw.
range: '>=4.6.0 <5.0.0-0 || >=5.0.0-beta.15',
},
];

/**
* The set of formats supported by all specVersion 2 runs, regardless of
Expand Down Expand Up @@ -128,8 +145,10 @@ export function getRunCapabilities(
framedByteStreams: false,
};

for (const { capability, minVersion } of CAPABILITY_VERSION_TABLE) {
if (semver.gte(workflowCoreVersion, minVersion)) {
for (const { capability, range } of CAPABILITY_VERSION_TABLE) {
if (
semver.satisfies(workflowCoreVersion, range, { includePrerelease: true })
) {
result[capability] = true;
}
}
Expand Down
Loading