diff --git a/.changeset/wire-workflow-version.md b/.changeset/wire-workflow-version.md new file mode 100644 index 0000000000..4e7dcc4e7c --- /dev/null +++ b/.changeset/wire-workflow-version.md @@ -0,0 +1,6 @@ +--- +"@workflow/core": patch +"@workflow/web-shared": patch +--- + +Add SDK version to workflow run executionContext for observability diff --git a/packages/cli/bin/run.js b/packages/cli/bin/run.js index 51c9ec9e1f..a4ac37838c 100755 --- a/packages/cli/bin/run.js +++ b/packages/cli/bin/run.js @@ -10,7 +10,9 @@ const envPath = resolve(process.cwd(), '.env'); if (existsSync(envPath)) { const envResult = config({ path: envPath }); if (envResult.error && envResult.error.code !== 'ENOENT') { - console.warn(`Warning: Failed to load .env file: ${envResult.error.message}`); + console.warn( + `Warning: Failed to load .env file: ${envResult.error.message}` + ); } } @@ -19,7 +21,9 @@ const envLocalPath = resolve(process.cwd(), '.env.local'); if (existsSync(envLocalPath)) { const envLocalResult = config({ path: envLocalPath, override: true }); if (envLocalResult.error && envLocalResult.error.code !== 'ENOENT') { - console.warn(`Warning: Failed to load .env.local file: ${envLocalResult.error.message}`); + console.warn( + `Warning: Failed to load .env.local file: ${envLocalResult.error.message}` + ); } } diff --git a/packages/core/.gitignore b/packages/core/.gitignore new file mode 100644 index 0000000000..7b6d0b4576 --- /dev/null +++ b/packages/core/.gitignore @@ -0,0 +1,2 @@ +# Auto-generated version file +src/version.ts diff --git a/packages/core/package.json b/packages/core/package.json index 60820a83fb..72fe0dc0be 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -35,12 +35,12 @@ "./_workflow": "./dist/workflow/index.js" }, "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "clean": "tsc --build --clean && rm -r dist ||:", + "build": "genversion --es6 src/version.ts && tsc", + "dev": "genversion --es6 src/version.ts && tsc --watch", + "clean": "tsc --build --clean && rm -rf dist src/version.ts", "test": "cross-env WORKFLOW_TARGET_WORLD=local vitest run src", "test:e2e": "vitest run e2e", - "typecheck": "tsc --noEmit" + "typecheck": "genversion --es6 src/version.ts && tsc --noEmit" }, "dependencies": { "@aws-sdk/credential-provider-web-identity": "3.609.0", @@ -68,7 +68,8 @@ "@types/node": "catalog:", "@types/seedrandom": "3.0.8", "@workflow/tsconfig": "workspace:*", - "cross-env": "10.1.0" + "cross-env": "10.1.0", + "genversion": "3.2.0" }, "peerDependencies": { "@opentelemetry/api": "1" diff --git a/packages/core/src/observability.ts b/packages/core/src/observability.ts index 0d024be576..38c733b96d 100644 --- a/packages/core/src/observability.ts +++ b/packages/core/src/observability.ts @@ -344,7 +344,17 @@ export const hydrateResourceIO = < hydrated = hydrateWorkflowIO(resource); } if ('executionContext' in hydrated) { - const { executionContext: _, ...rest } = hydrated; + const { executionContext, ...rest } = hydrated; + // Preserve workflowCoreVersion from executionContext for observability + const workflowCoreVersion = + executionContext && + typeof executionContext === 'object' && + 'workflowCoreVersion' in executionContext + ? executionContext.workflowCoreVersion + : undefined; + if (workflowCoreVersion) { + return { ...rest, workflowCoreVersion } as unknown as T; + } return rest as T; } return hydrated; diff --git a/packages/core/src/runtime/start.ts b/packages/core/src/runtime/start.ts index 2db3940742..051a667914 100644 --- a/packages/core/src/runtime/start.ts +++ b/packages/core/src/runtime/start.ts @@ -9,6 +9,7 @@ import { dehydrateWorkflowArguments } from '../serialization.js'; import * as Attribute from '../telemetry/semantic-conventions.js'; import { serializeTraceCarrier, trace } from '../telemetry.js'; import { waitedUntil } from '../util.js'; +import { version as workflowCoreVersion } from '../version.js'; import { getWorld } from './world.js'; export interface StartOptions { @@ -117,7 +118,7 @@ export async function start( deploymentId: deploymentId, workflowName: workflowName, input: workflowArguments, - executionContext: { traceCarrier }, + executionContext: { traceCarrier, workflowCoreVersion }, }, }); diff --git a/packages/core/turbo.json b/packages/core/turbo.json new file mode 100644 index 0000000000..e503fb6757 --- /dev/null +++ b/packages/core/turbo.json @@ -0,0 +1,9 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist", "src/version.ts"] + } + } +} diff --git a/packages/next/src/builder.ts b/packages/next/src/builder.ts index ebe0156345..c0c168974c 100644 --- a/packages/next/src/builder.ts +++ b/packages/next/src/builder.ts @@ -353,7 +353,11 @@ export async function getNextBuilder() { return inputFiles.filter((item) => { // Match App Router entrypoints: route.ts, page.ts, layout.ts in app/ or src/app/ directories // Matches: /app/page.ts, /app/dashboard/page.ts, /src/app/route.ts, etc. - if (item.match(/(^|.*[/\\])(app|src[/\\]app)([/\\](route|page|layout)\.|[/\\].*[/\\](route|page|layout)\.)/)) { + if ( + item.match( + /(^|.*[/\\])(app|src[/\\]app)([/\\](route|page|layout)\.|[/\\].*[/\\](route|page|layout)\.)/ + ) + ) { return true; } // Match Pages Router entrypoints: files in pages/ or src/pages/ diff --git a/packages/web-shared/src/sidebar/attribute-panel.tsx b/packages/web-shared/src/sidebar/attribute-panel.tsx index 7a8b23c8f6..0ddfb886b9 100644 --- a/packages/web-shared/src/sidebar/attribute-panel.tsx +++ b/packages/web-shared/src/sidebar/attribute-panel.tsx @@ -481,7 +481,8 @@ type AttributeKey = | keyof Event | 'eventData' | 'resumeAt' - | 'expiredAt'; + | 'expiredAt' + | 'workflowCoreVersion'; const attributeOrder: AttributeKey[] = [ 'workflowName', @@ -497,6 +498,7 @@ const attributeOrder: AttributeKey[] = [ 'eventType', 'deploymentId', 'specVersion', + 'workflowCoreVersion', 'ownerId', 'projectId', 'environment', @@ -521,6 +523,20 @@ const sortByAttributeOrder = (a: string, b: string): number => { return aIndex - bIndex; }; +/** + * Display names for attributes that should render differently from their key. + */ +const attributeDisplayNames: Partial> = { + workflowCoreVersion: '@workflow/core version', +}; + +/** + * Get the display name for an attribute key. + */ +const getAttributeDisplayName = (attribute: string): string => { + return attributeDisplayNames[attribute as AttributeKey] ?? attribute; +}; + export const localMillisecondTime = (value: unknown): string => { let date: Date; if (value instanceof Date) { @@ -573,6 +589,7 @@ const attributeToDisplayFn: Record< // Project details deploymentId: (value: unknown) => String(value), specVersion: (value: unknown) => String(value), + workflowCoreVersion: (value: unknown) => String(value), // Tenancy (we don't show these) ownerId: (_value: unknown) => null, projectId: (_value: unknown) => null, @@ -820,7 +837,17 @@ export const AttributePanel = ({ /** Callback when a stream reference is clicked */ onStreamClick?: (streamId: string) => void; }) => { - const displayData = data; + // Extract workflowCoreVersion from executionContext for display + const displayData = useMemo(() => { + const result = { ...data }; + const execCtx = data.executionContext as + | Record + | undefined; + if (execCtx?.workflowCoreVersion) { + result.workflowCoreVersion = execCtx.workflowCoreVersion; + } + return result; + }, [data]); const hasExpired = expiredAt != null && new Date(expiredAt) < new Date(); const basicAttributes = Object.keys(displayData) .filter((key) => !resolvableAttributes.includes(key)) @@ -872,7 +899,7 @@ export const AttributePanel = ({ className="text-[11px] font-medium" style={{ color: 'var(--ds-gray-700)' }} > - {attribute} + {getAttributeDisplayName(attribute)}