From b3d53ea93fa33f462ac2bf4a34d08b4aee453a67 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Mon, 11 May 2026 18:14:32 +0100 Subject: [PATCH 1/2] Emit schema_version: '4.0' in v2 workflow and project YAML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a top-level `schema_version` key after `id` and `name` in both the TypeScript workflow serializer and the Elixir workflow/project serializer. The v2 JSON schema is extended to permit the key so round-trip parsing still passes its own validation. Parsers remain lenient — files without the key continue to parse unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- assets/js/yaml/schema/workflow-spec-v2.json | 1 + assets/js/yaml/v2.ts | 2 ++ lib/lightning/workflows/yaml_format/v2.ex | 2 ++ test/fixtures/portability/v2/canonical_project.yaml | 1 + test/fixtures/portability/v2/canonical_workflow.yaml | 1 + 5 files changed, 7 insertions(+) diff --git a/assets/js/yaml/schema/workflow-spec-v2.json b/assets/js/yaml/schema/workflow-spec-v2.json index 5c6905e81d..6c863d9fdf 100644 --- a/assets/js/yaml/schema/workflow-spec-v2.json +++ b/assets/js/yaml/schema/workflow-spec-v2.json @@ -79,6 +79,7 @@ } }, "properties": { + "schema_version": { "type": "string" }, "id": { "type": "string" }, "name": { "type": ["string", "null"] }, "start": { "type": "string" }, diff --git a/assets/js/yaml/v2.ts b/assets/js/yaml/v2.ts index daf3d41956..1de63993e5 100644 --- a/assets/js/yaml/v2.ts +++ b/assets/js/yaml/v2.ts @@ -250,6 +250,7 @@ type CanonicalStep = CanonicalTriggerStep | CanonicalJobStep; interface CanonicalWorkflow { id: string; name: string; + schema_version: string; start?: string; steps: CanonicalStep[]; } @@ -344,6 +345,7 @@ const workflowStateToCanonical = (state: WorkflowState): CanonicalWorkflow => { return { id: hyphenate(state.name), name: state.name, + schema_version: '4.0', ...(start ? { start } : {}), // Trigger steps first, then job steps — matches Elixir's emit order. steps: [...triggerSteps, ...jobSteps], diff --git a/lib/lightning/workflows/yaml_format/v2.ex b/lib/lightning/workflows/yaml_format/v2.ex index 860210522f..5001461e86 100644 --- a/lib/lightning/workflows/yaml_format/v2.ex +++ b/lib/lightning/workflows/yaml_format/v2.ex @@ -457,6 +457,7 @@ defmodule Lightning.Workflows.YamlFormat.V2 do [ emit_scalar_field("id", Map.get(workflow_canonical, :id)), emit_scalar_field("name", Map.get(workflow_canonical, :name)), + emit_scalar_field("schema_version", "4.0"), emit_scalar_field("start", Map.get(workflow_canonical, :start)), emit_steps(triggers ++ jobs) ] @@ -772,6 +773,7 @@ defmodule Lightning.Workflows.YamlFormat.V2 do [ emit_top_scalar("id", Map.get(project_canonical, :id)), emit_top_scalar("name", Map.get(project_canonical, :name)), + emit_top_scalar("schema_version", "4.0"), emit_top_description(Map.get(project_canonical, :description)), emit_collections_array(Map.get(project_canonical, :collections, [])), emit_credentials_array(Map.get(project_canonical, :credentials, [])), diff --git a/test/fixtures/portability/v2/canonical_project.yaml b/test/fixtures/portability/v2/canonical_project.yaml index 5b1fd75a64..4cb588a895 100644 --- a/test/fixtures/portability/v2/canonical_project.yaml +++ b/test/fixtures/portability/v2/canonical_project.yaml @@ -1,5 +1,6 @@ id: a-test-project name: a-test-project +schema_version: '4.0' description: | This is only a test collections: diff --git a/test/fixtures/portability/v2/canonical_workflow.yaml b/test/fixtures/portability/v2/canonical_workflow.yaml index c635bfc898..7248bcc29d 100644 --- a/test/fixtures/portability/v2/canonical_workflow.yaml +++ b/test/fixtures/portability/v2/canonical_workflow.yaml @@ -1,5 +1,6 @@ id: canonical-workflow name: canonical workflow +schema_version: '4.0' start: cron steps: - id: cron From 429f4c8df38bdb68a555f89b1d20f77e709ed25b Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Mon, 11 May 2026 18:19:31 +0100 Subject: [PATCH 2/2] formatting on yaml output --- assets/js/yaml/v2.ts | 3 +++ lib/lightning/workflows/yaml_format/v2.ex | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/assets/js/yaml/v2.ts b/assets/js/yaml/v2.ts index 1de63993e5..3fd907b856 100644 --- a/assets/js/yaml/v2.ts +++ b/assets/js/yaml/v2.ts @@ -488,6 +488,9 @@ const RESERVED_YAML = new Set([ const needsQuoting = (s: string): boolean => { if (s === '') return true; if (RESERVED_YAML.has(s.toLowerCase())) return true; + // Quote strings that would otherwise parse as a YAML number on read + // (e.g. "4.0" must stay a string for schema_version). + if (/^-?(\d+\.?\d*|\.\d+)$/.test(s)) return true; // Mirrors `Lightning.Workflows.YamlFormat.V2.quote_if_needed/1`: // ^[A-Za-z0-9][A-Za-z0-9_\-@./> ]*[A-Za-z0-9]$ (and not reserved) return !/^[A-Za-z0-9][A-Za-z0-9_\-@./> ]*[A-Za-z0-9]$/.test(s); diff --git a/lib/lightning/workflows/yaml_format/v2.ex b/lib/lightning/workflows/yaml_format/v2.ex index 5001461e86..10b7b91827 100644 --- a/lib/lightning/workflows/yaml_format/v2.ex +++ b/lib/lightning/workflows/yaml_format/v2.ex @@ -658,6 +658,11 @@ defmodule Lightning.Workflows.YamlFormat.V2 do value == "" -> "''" + # Quote strings that would otherwise parse as a YAML number on read + # (e.g. "4.0" must stay a string for schema_version). + Regex.match?(~r/^-?(\d+\.?\d*|\.\d+)$/, value) -> + "'" <> value <> "'" + Regex.match?(~r/^[a-zA-Z0-9][a-zA-Z0-9_\-@\.\/> |]*[a-zA-Z0-9]$/, value) and not yaml_reserved?(value) -> value