working on workflows
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# RUST_MIN_STACK Fix & Workflow File Metadata Separation
|
||||
|
||||
**Date**: 2026-02-05
|
||||
|
||||
## Summary
|
||||
|
||||
Three related changes: (1) fixed `rustc` SIGSEGV crashes during Docker release builds by increasing the compiler stack size, (2) enforced the separation of concerns between action YAML and workflow YAML files across the parser, loaders, and registrars, and (3) updated the workflow builder UI and API save endpoints to produce the correct two-file layout.
|
||||
|
||||
## Problem 1: rustc SIGSEGV in Docker Builds
|
||||
|
||||
Docker Compose builds were failing with `rustc interrupted by SIGSEGV` during release compilation. The error message suggested increasing `RUST_MIN_STACK` to 16 MiB.
|
||||
|
||||
### Fix
|
||||
|
||||
Added `ENV RUST_MIN_STACK=16777216` to the build stage of all 7 Rust Dockerfiles:
|
||||
|
||||
- `docker/Dockerfile` (both build stages)
|
||||
- `docker/Dockerfile.optimized`
|
||||
- `docker/Dockerfile.worker`
|
||||
- `docker/Dockerfile.worker.optimized`
|
||||
- `docker/Dockerfile.sensor.optimized`
|
||||
- `docker/Dockerfile.pack-binaries`
|
||||
- `docker/Dockerfile.pack-builder`
|
||||
|
||||
Also added `export RUST_MIN_STACK := 16777216` to the `Makefile` for local builds.
|
||||
|
||||
## Problem 2: Workflow File Metadata Duplication
|
||||
|
||||
The `timeline_demo.yaml` workflow file (in `actions/workflows/`) redundantly defined `ref`, `label`, `description`, `parameters`, `output`, and `tags` — all of which are action-level concerns that belong exclusively in the companion action YAML (`actions/timeline_demo.yaml`). This violated the design principle that action YAML owns the interface and workflow YAML owns the execution graph.
|
||||
|
||||
The root cause was that `WorkflowDefinition` required `ref` and `label` as mandatory fields, forcing even action-linked workflow files to include them.
|
||||
|
||||
### Backend Parser & Loader Changes
|
||||
|
||||
**`crates/common/src/workflow/parser.rs`**:
|
||||
- Made `ref` and `label` optional with `#[serde(default)]` and removed `min = 1` validators
|
||||
- Added two new tests: `test_parse_action_linked_workflow_without_ref_and_label` and `test_parse_standalone_workflow_still_works_with_ref_and_label`
|
||||
|
||||
**`crates/common/src/pack_registry/loader.rs`**:
|
||||
- `load_workflow_for_action()` now fills in `ref`/`label`/`description`/`tags` from the action YAML when the workflow file omits them (action YAML is authoritative)
|
||||
|
||||
**`crates/common/src/workflow/registrar.rs`** and **`crates/executor/src/workflow/registrar.rs`**:
|
||||
- Added `effective_ref()` and `effective_label()` helper methods that fall back to `WorkflowFile.ref_name` / `WorkflowFile.name` (derived from filename) when the workflow YAML omits them
|
||||
- Threaded effective values through `create_workflow`, `update_workflow`, `create_companion_action`, and `ensure_companion_action`
|
||||
|
||||
**`scripts/load_core_pack.py`**:
|
||||
- `upsert_workflow_definition()` now derives `ref`/`label`/`description`/`tags` from the action YAML when the workflow file omits them
|
||||
|
||||
**`packs.external/python_example/actions/workflows/timeline_demo.yaml`**:
|
||||
- Stripped `ref`, `label`, `description`, `parameters`, `output`, and `tags` — file now contains only `version`, `vars`, `tasks`, and `output_map`
|
||||
|
||||
## Problem 3: Workflow Builder Wrote Full Definition to Disk
|
||||
|
||||
The visual workflow builder's save endpoints (`POST /api/v1/packs/{pack_ref}/workflow-files` and `PUT /api/v1/workflows/{ref}/file`) were writing the full `WorkflowYamlDefinition` — including action-level metadata — to the `.workflow.yaml` file on disk. The YAML viewer also showed a single monolithic preview.
|
||||
|
||||
### API Save Endpoint Changes
|
||||
|
||||
**`crates/api/src/routes/workflows.rs`** — `write_workflow_yaml()`:
|
||||
- Now writes **two files** per save:
|
||||
1. **Workflow YAML** (`actions/workflows/{name}.workflow.yaml`) — graph-only via `strip_action_level_fields()` which removes `ref`, `label`, `description`, `parameters`, `output`, `tags`
|
||||
2. **Action YAML** (`actions/{name}.yaml`) — action-level metadata via `build_action_yaml()` which produces `ref`, `label`, `description`, `enabled`, `workflow_file`, `parameters`, `output`, `tags`
|
||||
- Added `strip_action_level_fields()` helper — extracts only `version`, `vars`, `tasks`, `output_map` from the definition JSON
|
||||
- Added `build_action_yaml()` helper — constructs the companion action YAML with proper formatting and comments
|
||||
|
||||
### Frontend Changes
|
||||
|
||||
**`web/src/types/workflow.ts`**:
|
||||
- Added `WorkflowGraphDefinition` interface (graph-only: `version`, `vars`, `tasks`, `output_map`)
|
||||
- Added `ActionYamlDefinition` interface (action metadata: `ref`, `label`, `description`, `enabled`, `workflow_file`, `parameters`, `output`, `tags`)
|
||||
- Added `builderStateToGraph()` — extracts graph-only definition from builder state
|
||||
- Added `builderStateToActionYaml()` — extracts action metadata from builder state
|
||||
- Refactored `builderStateToDefinition()` to delegate to `builderStateToGraph()` internally
|
||||
|
||||
**`web/src/pages/actions/WorkflowBuilderPage.tsx`**:
|
||||
- YAML viewer now shows **two side-by-side panels** instead of a single preview:
|
||||
- **Left panel (blue, 2/5 width)**: Action YAML — shows `actions/{name}.yaml` content with ref, label, parameters, workflow_file reference
|
||||
- **Right panel (green, 3/5 width)**: Workflow YAML — shows `actions/workflows/{name}.workflow.yaml` with graph-only content (version, vars, tasks)
|
||||
- Each panel has its own copy button, filename label, and description bar explaining the file's role
|
||||
- Separate `actionYamlPreview` and `workflowYamlPreview` memos replace the old `yamlPreview`
|
||||
|
||||
## Design: Two Valid Workflow File Conventions
|
||||
|
||||
1. **Standalone workflows** (`workflows/*.yaml`) — no companion action YAML, so they carry their own `ref`, `label`, `parameters`, etc. Loaded by `WorkflowLoader.sync_pack_workflows()`.
|
||||
|
||||
2. **Action-linked workflows** (`actions/workflows/*.yaml`) — referenced via `workflow_file` from an action YAML. The action YAML is the single authoritative source for `ref`, `label`, `description`, `parameters`, `output`, and `tags`. The workflow file contains only the execution graph: `version`, `vars`, `tasks`, `output_map`.
|
||||
|
||||
The visual workflow builder and API save endpoints now produce the action-linked layout (convention 2) with properly separated files.
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `docker/Dockerfile` | Added `RUST_MIN_STACK=16777216` (both stages) |
|
||||
| `docker/Dockerfile.optimized` | Added `RUST_MIN_STACK=16777216` |
|
||||
| `docker/Dockerfile.worker` | Added `RUST_MIN_STACK=16777216` |
|
||||
| `docker/Dockerfile.worker.optimized` | Added `RUST_MIN_STACK=16777216` |
|
||||
| `docker/Dockerfile.sensor.optimized` | Added `RUST_MIN_STACK=16777216` |
|
||||
| `docker/Dockerfile.pack-binaries` | Added `RUST_MIN_STACK=16777216` |
|
||||
| `docker/Dockerfile.pack-builder` | Added `RUST_MIN_STACK=16777216` |
|
||||
| `Makefile` | Added `export RUST_MIN_STACK` |
|
||||
| `crates/common/src/workflow/parser.rs` | Optional `ref`/`label`, 2 new tests |
|
||||
| `crates/common/src/pack_registry/loader.rs` | Action YAML fallback for metadata |
|
||||
| `crates/common/src/workflow/registrar.rs` | `effective_ref()`/`effective_label()` |
|
||||
| `crates/executor/src/workflow/registrar.rs` | `effective_ref()`/`effective_label()` |
|
||||
| `scripts/load_core_pack.py` | Action YAML fallback for metadata |
|
||||
| `crates/api/src/routes/workflows.rs` | Two-file write, `strip_action_level_fields()`, `build_action_yaml()` |
|
||||
| `web/src/types/workflow.ts` | `WorkflowGraphDefinition`, `ActionYamlDefinition`, `builderStateToGraph()`, `builderStateToActionYaml()` |
|
||||
| `web/src/pages/actions/WorkflowBuilderPage.tsx` | Two-panel YAML viewer |
|
||||
| `packs.external/python_example/actions/workflows/timeline_demo.yaml` | Stripped action-level metadata |
|
||||
| `AGENTS.md` | Updated Workflow File Storage, YAML viewer, Docker Build Optimization sections |
|
||||
|
||||
## Test Results
|
||||
|
||||
- All 23 parser tests pass (including 2 new)
|
||||
- All 9 loader tests pass
|
||||
- All 2 registrar tests pass
|
||||
- All 598 workspace lib tests pass
|
||||
- Zero TypeScript errors
|
||||
- Zero compiler warnings
|
||||
- Zero build errors
|
||||
56
work-summary/2026-03-04-cli-workflow-upload.md
Normal file
56
work-summary/2026-03-04-cli-workflow-upload.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# CLI Workflow Upload Command
|
||||
|
||||
**Date**: 2026-03-04
|
||||
|
||||
## Summary
|
||||
|
||||
Added a `workflow` subcommand group to the Attune CLI, enabling users to upload individual workflow actions to existing packs without requiring a full pack upload. Also fixed a pre-existing `-y` short flag conflict across multiple CLI subcommands.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New File: `crates/cli/src/commands/workflow.rs`
|
||||
|
||||
New CLI subcommand module with four commands:
|
||||
|
||||
- **`attune workflow upload <action-yaml-path>`** — Reads a local action YAML file, extracts the `workflow_file` field to locate the companion workflow YAML, determines the pack from the action ref (e.g., `mypack.deploy` → pack `mypack`), and uploads both files to the API via `POST /api/v1/packs/{pack_ref}/workflow-files`. On 409 Conflict, fails unless `--force` is passed, which triggers a `PUT /api/v1/workflows/{ref}/file` update instead.
|
||||
- **`attune workflow list`** — Lists workflows with optional `--pack`, `--tags`, and `--search` filters.
|
||||
- **`attune workflow show <ref>`** — Shows workflow details including a task summary table (name, action, transition count).
|
||||
- **`attune workflow delete <ref>`** — Deletes a workflow with `--yes` confirmation bypass.
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `crates/cli/src/commands/mod.rs` | Added `pub mod workflow` |
|
||||
| `crates/cli/src/main.rs` | Added `Workflow` variant to `Commands` enum, import, and dispatch |
|
||||
| `crates/cli/src/commands/action.rs` | Fixed `-y` short flag conflict on `Delete.yes` |
|
||||
| `crates/cli/src/commands/trigger.rs` | Fixed `-y` short flag conflict on `Delete.yes` |
|
||||
| `crates/cli/src/commands/pack.rs` | Fixed `-y` short flag conflict on `Uninstall.yes` |
|
||||
| `AGENTS.md` | Added workflow CLI documentation to CLI Tool section |
|
||||
|
||||
### New Test File: `crates/cli/tests/test_workflows.rs`
|
||||
|
||||
21 integration tests covering:
|
||||
- List (authenticated, by pack, JSON/YAML output, empty, unauthenticated)
|
||||
- Show (table, JSON, not found)
|
||||
- Delete (with `--yes`, JSON output)
|
||||
- Upload (success, JSON output, conflict without force, conflict with force, missing action file, missing workflow file, non-workflow action, invalid YAML)
|
||||
- Help text (workflow help, upload help)
|
||||
|
||||
### Bug Fix: `-y` Short Flag Conflict
|
||||
|
||||
The global `--yaml` flag uses `-y` as its short form. Three existing subcommands (`action delete`, `trigger delete`, `pack uninstall`) also defined `-y` as a short flag for `--yes`. This caused a clap runtime panic when both flags were in scope (e.g., `attune --yaml action delete ref --yes`). Fixed by removing the short flag from all `yes` arguments — they now only accept `--yes` (long form).
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **Reuses existing API endpoints** — No new server-side code needed. The CLI constructs a `SaveWorkflowFileRequest` JSON payload from the two local YAML files and posts to the existing workflow-file endpoints.
|
||||
- **Pack determined from action ref** — The pack ref is extracted from the action's `ref` field using the last-dot convention (e.g., `org.infra.deploy` → pack `org.infra`, name `deploy`).
|
||||
- **Workflow path resolution** — The `workflow_file` value is resolved relative to the action YAML's parent directory, matching how the pack loader resolves it relative to the `actions/` directory.
|
||||
- **Create-or-update pattern** — Upload attempts create first; on 409 with `--force`, falls back to update. This matches the `pack upload --force` UX pattern.
|
||||
|
||||
## Test Results
|
||||
|
||||
- **Unit tests**: 6 new (split_action_ref, resolve_workflow_path variants)
|
||||
- **Integration tests**: 21 new
|
||||
- **Total CLI tests**: 160 passed, 0 failed, 1 ignored (pre-existing)
|
||||
- **Compiler warnings**: 0
|
||||
82
work-summary/2026-03-04-typed-publish-directives.md
Normal file
82
work-summary/2026-03-04-typed-publish-directives.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Typed Publish Directives in Workflow Definitions
|
||||
|
||||
**Date**: 2026-03-04
|
||||
|
||||
## Problem
|
||||
|
||||
The `python_example.timeline_demo` workflow action failed to execute with:
|
||||
|
||||
```
|
||||
Runtime not found: No runtime found for action: python_example.timeline_demo
|
||||
(available: node.js, python, shell)
|
||||
```
|
||||
|
||||
This error was misleading — the real issue was that the workflow definition YAML
|
||||
failed to parse during pack registration, so the `workflow_definition` record was
|
||||
never created and the action's `workflow_def` FK remained NULL. Without a linked
|
||||
workflow definition, the executor treated it as a regular action and dispatched it
|
||||
to a worker, which couldn't find a runtime for a workflow action.
|
||||
|
||||
### Root Cause
|
||||
|
||||
The YAML parsing error was:
|
||||
|
||||
```
|
||||
tasks[7].next[0].publish: data did not match any variant of untagged enum
|
||||
PublishDirective at line 234 column 11
|
||||
```
|
||||
|
||||
The `PublishDirective::Simple` variant was defined as `HashMap<String, String>`,
|
||||
but the workflow YAML contained non-string publish values:
|
||||
|
||||
```yaml
|
||||
publish:
|
||||
- validation_passed: true # boolean, not a string
|
||||
- validation_passed: false # boolean, not a string
|
||||
```
|
||||
|
||||
YAML parses `true`/`false` as booleans, which couldn't deserialize into `String`.
|
||||
|
||||
## Solution
|
||||
|
||||
Changed `PublishDirective::Simple` from `HashMap<String, String>` to
|
||||
`HashMap<String, serde_json::Value>` so publish directives can carry any
|
||||
JSON-compatible type: strings (including template expressions), booleans,
|
||||
numbers, arrays, objects, and null.
|
||||
|
||||
### Files Modified
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `crates/common/src/workflow/parser.rs` | `PublishDirective::Simple` value type → `serde_json::Value` |
|
||||
| `crates/executor/src/workflow/parser.rs` | Same change (executor's local copy) |
|
||||
| `crates/executor/src/workflow/graph.rs` | Renamed `PublishVar.expression: String` → `PublishVar.value: JsonValue` with `#[serde(alias = "expression")]` for backward compat with stored task graphs; imported `serde_json::Value` |
|
||||
| `crates/executor/src/scheduler.rs` | Updated publish map from `HashMap<String, String>` to `HashMap<String, JsonValue>` |
|
||||
| `crates/executor/src/workflow/context.rs` | `publish_from_result` accepts `HashMap<String, JsonValue>`, passes values directly to `render_json` (strings get template-rendered, non-strings pass through unchanged) |
|
||||
| `crates/common/src/workflow/expression_validator.rs` | Only validates string values as templates; non-string literals are skipped |
|
||||
| `packs.external/python_example/actions/workflows/timeline_demo.yaml` | Fixed `result().items` → `result().data.items` (secondary bug in workflow definition) |
|
||||
|
||||
### Type Preservation
|
||||
|
||||
The rendering pipeline now correctly preserves types end-to-end:
|
||||
|
||||
- **String values** (e.g., `"{{ result().data }}"`) → rendered through expression engine with type preservation
|
||||
- **Boolean values** (e.g., `true`) → stored as `JsonValue::Bool(true)`, pass through `render_json` unchanged
|
||||
- **Numeric values** (e.g., `42`, `3.14`) → stored as `JsonValue::Number`, pass through unchanged
|
||||
- **Null** → stored as `JsonValue::Null`, passes through unchanged
|
||||
- **Arrays/Objects** → stored as-is, with any nested string templates rendered recursively
|
||||
|
||||
### Tests Added
|
||||
|
||||
- `parser::tests::test_typed_publish_values_in_transitions` — verifies YAML parsing of booleans, numbers, strings, templates, and null in publish directives
|
||||
- `graph::tests::test_typed_publish_values` — verifies typed values survive graph construction
|
||||
- `context::tests::test_publish_typed_values` — verifies typed values pass through `publish_from_result` with correct types (boolean stays boolean, not string "true")
|
||||
|
||||
## Verification
|
||||
|
||||
After deploying the fix:
|
||||
|
||||
1. Re-registered `python_example` pack — workflow definition created successfully (ID: 2)
|
||||
2. Action `python_example.timeline_demo` linked to `workflow_def = 2`
|
||||
3. Executed the workflow — executor correctly identified it as a workflow action and orchestrated 15 child task executions through all stages: initialize → parallel fan-out (build/lint/scan) → merge join → generate items → with_items(×3) → validate → finalize
|
||||
4. Workflow variables confirmed type preservation: `validation_passed: true` (boolean), `items_processed: 3` (integer), `number_list: [1, 2, 3]` (array)
|
||||
55
work-summary/2026-03-04-with-items-race-condition-fix.md
Normal file
55
work-summary/2026-03-04-with-items-race-condition-fix.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Fix: with_items Race Condition Causing Duplicate Task Dispatches
|
||||
|
||||
**Date**: 2026-03-04
|
||||
**Component**: Executor service (`crates/executor/src/scheduler.rs`)
|
||||
**Issue**: Workflow tasks downstream of `with_items` tasks were being dispatched multiple times
|
||||
|
||||
## Problem
|
||||
|
||||
When a `with_items` task (e.g., `process_items` with `concurrency: 3`) had multiple items completing nearly simultaneously, the downstream successor task (e.g., `validate`) would be dispatched once per concurrently-completing item instead of once total.
|
||||
|
||||
**Root cause**: Workers update execution status in the database to `Completed` *before* publishing the `ExecutionCompleted` MQ message. The completion listener processes MQ messages sequentially, but by the time it processes item N's completion message, items N+1, N+2, etc. may already be marked `Completed` in the database. This means the `siblings_remaining` query (which checks DB status) returns 0 for multiple items, and each one falls through to transition evaluation and dispatches the successor task.
|
||||
|
||||
### Concrete Scenario
|
||||
|
||||
With `process_items` (5 items, `concurrency: 3`) → `validate`:
|
||||
|
||||
1. Items 3 and 4 finish on separate workers nearly simultaneously
|
||||
2. Worker for item 3 updates DB: status = Completed, then publishes MQ message
|
||||
3. Worker for item 4 updates DB: status = Completed, then publishes MQ message
|
||||
4. Completion listener processes item 3's message:
|
||||
- `siblings_remaining` query: item 4 is already Completed in DB → **0 remaining**
|
||||
- Falls through → dispatches `validate` ✓
|
||||
5. Completion listener processes item 4's message:
|
||||
- `siblings_remaining` query: all items completed → **0 remaining**
|
||||
- Falls through → dispatches `validate` **again** ✗
|
||||
|
||||
With `concurrency: 3` and tasks of equal duration, up to 3 items could complete simultaneously, causing the successor to be dispatched 3 times.
|
||||
|
||||
## Fix
|
||||
|
||||
Two-layer defense added to `advance_workflow()`:
|
||||
|
||||
### Layer 1: Persisted state check (with_items early return)
|
||||
|
||||
After the `siblings_remaining` check passes (all items done), but before evaluating transitions, the fix checks whether `task_name` is already present in the *persisted* `completed_tasks` or `failed_tasks` from the `workflow_execution` record. If so, a previous `advance_workflow` invocation already handled this task's final completion — return early.
|
||||
|
||||
This is efficient because it uses data already loaded at the top of the function.
|
||||
|
||||
### Layer 2: Already-dispatched DB check (all successor tasks)
|
||||
|
||||
Before dispatching ANY successor task, the fix queries the `execution` table for existing child executions with the same `workflow_execution` ID and `task_name`. If any exist, the successor has already been dispatched by a prior call — skip it.
|
||||
|
||||
This belt-and-suspenders guard catches edge cases regardless of how the race manifests, including scenarios where the persisted `completed_tasks` list hasn't been updated yet.
|
||||
|
||||
## Files Changed
|
||||
|
||||
- `crates/executor/src/scheduler.rs` — Added two guards in `advance_workflow()`:
|
||||
1. Lines ~1035-1066: Early return for with_items tasks already in persisted completed/failed lists
|
||||
2. Lines ~1220-1250: DB existence check before dispatching any successor task
|
||||
|
||||
## Testing
|
||||
|
||||
- All 601 unit tests pass across the workspace (0 failures, 8 intentionally ignored)
|
||||
- Zero compiler warnings
|
||||
- The fix is defensive and backward-compatible — no changes to data models, APIs, or MQ protocols
|
||||
104
work-summary/2026-03-04-workflow-file-action-field.md
Normal file
104
work-summary/2026-03-04-workflow-file-action-field.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Workflow Action `workflow_file` Field & Timeline Demo Workflow
|
||||
|
||||
**Date**: 2026-03-04
|
||||
**Scope**: Pack loading architecture, workflow file discovery, demo workflow
|
||||
|
||||
## Summary
|
||||
|
||||
Introduced a `workflow_file` field for action YAML definitions that separates action-level metadata from workflow graph definitions. This enables a clean conceptual divide: the action YAML controls ref, label, parameters, policies, and tags, while the workflow file contains the execution graph (tasks, transitions, variables). Multiple actions can reference the same workflow file with different configurations, which has implications for policies and parameter mapping.
|
||||
|
||||
Also created a comprehensive demo workflow in the `python_example` pack that exercises the Workflow Timeline DAG visualizer.
|
||||
|
||||
## Architecture Change
|
||||
|
||||
### Before
|
||||
|
||||
Workflows could be registered two ways, each with limitations:
|
||||
|
||||
1. **`workflows/` directory** (pack root) — scanned by `WorkflowLoader`, registered by `WorkflowRegistrar` which auto-creates a companion action. No separation of action metadata from workflow definition.
|
||||
2. **API endpoints** (`POST /api/v1/packs/{ref}/workflow-files`) — writes to `actions/workflows/`, creates both `workflow_definition` and companion `action` records. Only available via the visual builder, not during pack file loading.
|
||||
|
||||
The `PackComponentLoader` had no awareness of workflow files at all — it only loaded actions, triggers, runtimes, and sensors from their respective directories.
|
||||
|
||||
### After
|
||||
|
||||
A third path is now supported, bridging both worlds:
|
||||
|
||||
3. **Action YAML with `workflow_file` field** — an action YAML in `actions/*.yaml` can include `workflow_file: workflows/timeline_demo.yaml` (path relative to `actions/`). During pack loading, the `PackComponentLoader`:
|
||||
- Reads and parses the referenced workflow YAML
|
||||
- Creates/updates a `workflow_definition` record
|
||||
- Creates the action record with `workflow_def` FK linked
|
||||
- Skips runtime resolution (workflow actions have no runner_type)
|
||||
- Uses the workflow file path as the entrypoint
|
||||
|
||||
This preserves the clean separation the visual builder already uses (action metadata in one place, workflow graph in another) while making it work with the pack file loading pipeline.
|
||||
|
||||
### Dual-Directory Workflow Scanning
|
||||
|
||||
The `WorkflowLoader` now scans **two** directories:
|
||||
|
||||
1. `{pack_dir}/workflows/` — legacy standalone workflow files
|
||||
2. `{pack_dir}/actions/workflows/` — visual-builder and action-linked workflow files
|
||||
|
||||
Files with `.workflow.yaml` suffix have the `.workflow` portion stripped when deriving the name/ref (e.g., `deploy.workflow.yaml` → name `deploy`, ref `pack.deploy`). If the same ref appears in both directories, `actions/workflows/` wins. The `reload_workflow` method searches `actions/workflows/` first with all extension variants.
|
||||
|
||||
## Files Changed
|
||||
|
||||
### Rust (`crates/common/src/pack_registry/loader.rs`)
|
||||
|
||||
- Added imports for `WorkflowDefinitionRepository`, `CreateWorkflowDefinitionInput`, `UpdateWorkflowDefinitionInput`, and `parse_workflow_yaml`
|
||||
- **`load_actions()`**: When action YAML contains `workflow_file`, calls `load_workflow_for_action()` to create/update the workflow definition, sets entrypoint to the workflow file path, skips runtime resolution, and links the action to the workflow definition after creation/update
|
||||
- **`load_workflow_for_action()`** (new): Reads and parses the workflow YAML, creates or updates the `workflow_definition` record, respects action YAML schema overrides (action's `parameters`/`output` take precedence over the workflow file's own schemas)
|
||||
|
||||
### Rust (`crates/common/src/workflow/loader.rs`)
|
||||
|
||||
- **`load_pack_workflows()`**: Now scans both `workflows/` and `actions/workflows/`, with the latter taking precedence on ref collision
|
||||
- **`reload_workflow()`**: Searches `actions/workflows/` first, trying `.workflow.yaml`, `.yaml`, `.workflow.yml`, and `.yml` extensions before falling back to `workflows/`
|
||||
- **`scan_workflow_files()`**: Strips `.workflow` suffix from filenames (e.g., `deploy.workflow.yaml` → name `deploy`)
|
||||
- **3 new tests**: `test_scan_workflow_files_strips_workflow_suffix`, `test_load_pack_workflows_scans_both_directories`, `test_reload_workflow_finds_actions_workflows_dir`
|
||||
|
||||
### Python (`scripts/load_core_pack.py`)
|
||||
|
||||
- **`upsert_workflow_definition()`** (new): Reads a workflow YAML file, upserts into `workflow_definition` table, returns the ID
|
||||
- **`upsert_actions()`**: Detects `workflow_file` field, calls `upsert_workflow_definition()`, sets entrypoint to workflow file path, skips runtime resolution for workflow actions, links action to workflow definition via `UPDATE action SET workflow_def = ...`
|
||||
|
||||
### Demo Pack Files (`packs.external/python_example/`)
|
||||
|
||||
- **`actions/simulate_work.py`** + **`actions/simulate_work.yaml`**: New action that simulates a unit of work with configurable duration, optional failure simulation, and structured JSON output
|
||||
- **`actions/timeline_demo.yaml`**: Action YAML with `workflow_file: workflows/timeline_demo.yaml` — controls action-level metadata
|
||||
- **`actions/workflows/timeline_demo.yaml`**: Workflow definition with 11 tasks, 18 transition edges, exercising parallel fan-out/fan-in, `with_items` + concurrency, failure paths, retries, timeouts, publish directives, and custom edge styling via `__chart_meta__`
|
||||
|
||||
### Documentation
|
||||
|
||||
- **`AGENTS.md`**: Updated Pack Component Loading Order, added Workflow Action YAML (`workflow_file` field) section, added Workflow File Discovery (dual-directory scanning) section, added pitfall #7 (never put workflow content directly in action YAML), renumbered subsequent items
|
||||
- **`packs.external/python_example/README.md`**: Added docs for `simulate_work`, `timeline_demo` workflow, and usage examples
|
||||
|
||||
## Test Results
|
||||
|
||||
- **596 unit tests passing**, 0 failures
|
||||
- **0 compiler warnings** across the workspace
|
||||
- 3 new tests for the workflow loader changes, all passing
|
||||
- Integration tests require `attune_test` database (pre-existing infrastructure issue, unrelated)
|
||||
|
||||
## Timeline Demo Workflow Features
|
||||
|
||||
The `python_example.timeline_demo` workflow creates this execution shape:
|
||||
|
||||
```
|
||||
initialize ─┬─► build_artifacts(6s) ────────────────┐
|
||||
├─► run_linter(3s) ─────┐ ├─► merge_results ─► generate_items ─► process_items(×5, 3∥) ─► validate ─┬─► finalize_success
|
||||
└─► security_scan(4s) ──┘ │ └─► handle_failure ─► finalize_failure
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
| Feature | How Exercised |
|
||||
|---------|--------------|
|
||||
| Parallel fan-out | `initialize` → 3 branches with different durations |
|
||||
| Fan-in / join | `merge_results` with `join: 3` |
|
||||
| `with_items` + concurrency | `process_items` expands to N items, `concurrency: 3` |
|
||||
| Failure paths | Every task has `{{ failed() }}` transitions |
|
||||
| Timeout handling | `security_scan` has `timeout: 30` + `{{ timed_out() }}` |
|
||||
| Retries | `build_artifacts` and `validate` with retry configs |
|
||||
| Publish directives | Results passed between stages |
|
||||
| Custom edge colors/labels | Via `__chart_meta__` on transitions |
|
||||
| Configurable failure | `fail_validation=true` exercises the error path |
|
||||
83
work-summary/workflow-timeline-dag.md
Normal file
83
work-summary/workflow-timeline-dag.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Workflow Timeline DAG Visualization
|
||||
|
||||
**Date**: 2026-02-05
|
||||
**Component**: `web/src/components/executions/workflow-timeline/`
|
||||
**Integration**: `web/src/pages/executions/ExecutionDetailPage.tsx`
|
||||
|
||||
## Overview
|
||||
|
||||
Added a Prefect-style workflow run timeline DAG visualization to the execution detail page for workflow executions. The component renders child task executions as horizontal duration bars on a time axis, connected by curved dependency edges that reflect the actual workflow definition transitions.
|
||||
|
||||
## Architecture
|
||||
|
||||
The implementation is a pure SVG renderer with no additional dependencies — it uses React, TypeScript, and inline SVG only (no D3, no React Flow, no new npm packages).
|
||||
|
||||
### Module Structure
|
||||
|
||||
```
|
||||
web/src/components/executions/workflow-timeline/
|
||||
├── index.ts # Barrel exports
|
||||
├── types.ts # Type definitions, color constants, layout config
|
||||
├── data.ts # Data transformation (executions → timeline structures)
|
||||
├── layout.ts # Layout engine (lane assignment, time scaling, edge paths)
|
||||
├── TimelineRenderer.tsx # SVG renderer with interactions
|
||||
└── WorkflowTimelineDAG.tsx # Orchestrator component (data fetching + layout + render)
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **WorkflowTimelineDAG** (orchestrator) fetches child executions via `useChildExecutions` and the workflow definition via `useWorkflow(actionRef)`.
|
||||
2. **data.ts** transforms `ExecutionSummary[]` + `WorkflowDefinition` into `TimelineTask[]`, `TimelineEdge[]`, and `TimelineMilestone[]`.
|
||||
3. **layout.ts** computes lane assignments (greedy packing), time→pixel scale, node positions, grid lines, and cubic Bezier edge paths.
|
||||
4. **TimelineRenderer** renders everything as SVG with interactive features.
|
||||
|
||||
## Key Features
|
||||
|
||||
### Visualization
|
||||
- **Task bars**: Horizontal rounded rectangles colored by state (green=completed, blue=running, red=failed, gray=pending, orange=timeout). Left accent bar indicates state. Running tasks pulse.
|
||||
- **Milestones**: Synthetic start/end diamond nodes plus merge/fork junctions inserted when fan-in/fan-out exceeds 3 tasks.
|
||||
- **Edges**: Curved cubic Bezier dependency lines with transition-aware coloring and labels derived from the workflow definition (`succeeded`, `failed`, `timed out`, custom expressions). Failure edges are dashed, timeout edges use dash-dot pattern.
|
||||
- **Time axis**: Vertical gridlines at "nice" intervals with timestamp labels along the top.
|
||||
- **Lane packing**: Greedy algorithm assigns tasks to non-overlapping y-lanes, with optional lane reordering to cluster tasks with shared upstream dependencies.
|
||||
|
||||
### Workflow Metadata Integration
|
||||
- Fetches the workflow definition to extract the `next` transition array from each task definition.
|
||||
- Maps definition task names to execution IDs (handles `with_items` expansions with multiple executions per task name).
|
||||
- Classifies `when` expressions (`{{ succeeded() }}`, `{{ failed() }}`, `{{ timed_out() }}`) into edge kinds with appropriate colors.
|
||||
- Reads `__chart_meta__` labels and custom colors from workflow definition transitions.
|
||||
- Falls back to timing-based heuristic edge inference when no workflow definition is available.
|
||||
|
||||
### Interactions
|
||||
- **Hover tooltip**: Shows task name, state, action ref, start/end times, duration, retry info, upstream/downstream counts.
|
||||
- **Click selection**: Clicking a task highlights its full upstream/downstream path (BFS traversal) and dims unrelated nodes/edges.
|
||||
- **Double-click navigation**: Navigates to the child execution's detail page.
|
||||
- **Horizontal zoom**: Mouse wheel zooms the x-axis while keeping y-lanes stable. Zoom anchors to cursor position.
|
||||
- **Pan**: Alt+drag or middle-mouse-drag pans horizontally via native scroll.
|
||||
- **Expand/compact toggle**: Expand button widens the chart for complex workflows.
|
||||
|
||||
### Performance
|
||||
- Edge paths are memoized per layout computation.
|
||||
- Node lookups use a `Map<string, TimelineNode>` for O(1) access.
|
||||
- Grid lines and highlighted paths are memoized with stable dependency arrays.
|
||||
- ResizeObserver tracks container width for responsive layout without polling.
|
||||
- No additional npm dependencies; SVG rendering handles 300+ tasks efficiently.
|
||||
|
||||
## Integration Point
|
||||
|
||||
The `WorkflowTimelineDAG` component is rendered on the execution detail page (`ExecutionDetailPage.tsx`) above the existing `WorkflowTasksPanel`, conditioned on `isWorkflow` (action has `workflow_def`).
|
||||
|
||||
Both components share a single TanStack Query cache entry for child executions (`["executions", { parent: id }]`) and both subscribe to WebSocket execution streams for real-time updates.
|
||||
|
||||
The `WorkflowTimelineDAG` accepts a `ParentExecutionInfo` interface (satisfied by both `ExecutionResponse` and `ExecutionSummary`) to avoid type casting at the integration point.
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `web/src/components/executions/workflow-timeline/types.ts` | New — type definitions |
|
||||
| `web/src/components/executions/workflow-timeline/data.ts` | New — data transformation |
|
||||
| `web/src/components/executions/workflow-timeline/layout.ts` | New — layout engine |
|
||||
| `web/src/components/executions/workflow-timeline/TimelineRenderer.tsx` | New — SVG renderer |
|
||||
| `web/src/components/executions/workflow-timeline/WorkflowTimelineDAG.tsx` | New — orchestrator |
|
||||
| `web/src/components/executions/workflow-timeline/index.ts` | New — barrel exports |
|
||||
| `web/src/pages/executions/ExecutionDetailPage.tsx` | Modified — import + render WorkflowTimelineDAG |
|
||||
Reference in New Issue
Block a user