feat: !include YAML tag and folder-based workflow layout #584

Merged
xiaomo merged 5 commits from feat/include-and-folder-workflow into main 2026-05-31 04:54:17 +00:00
Owner

Changes

!include YAML tag (Fixes #582)

  • !include path/to/file custom YAML tag,加载时替换为文件内容
  • .md/.txt → string,.json → parsed JSON,.yaml/.yml → parsed YAML
  • 路径相对于 workflow YAML 所在目录
  • 新增 packages/cli-workflow/src/include.ts

Folder-based workflow layout (Fixes #583)

  • 支持 foo/index.yaml 表达 name 为 foo 的 workflow
  • flat file 优先于 folder layout
  • 更新了 discoverProjectWorkflows()findWorkflowInDir()workflowNameFromPath()

Tests

  • 6 个 !include tag 测试
  • 2 个 folder workflow 测试
## Changes ### !include YAML tag (Fixes #582) - `!include path/to/file` custom YAML tag,加载时替换为文件内容 - `.md`/`.txt` → string,`.json` → parsed JSON,`.yaml`/`.yml` → parsed YAML - 路径相对于 workflow YAML 所在目录 - 新增 `packages/cli-workflow/src/include.ts` ### Folder-based workflow layout (Fixes #583) - 支持 `foo/index.yaml` 表达 name 为 `foo` 的 workflow - flat file 优先于 folder layout - 更新了 `discoverProjectWorkflows()`、`findWorkflowInDir()`、`workflowNameFromPath()` ### Tests - 6 个 `!include` tag 测试 - 2 个 folder workflow 测试
xiaomo added 1 commit 2026-05-31 04:12:25 +00:00
feat: !include YAML tag and folder-based workflow layout
CI / check (pull_request) Failing after 1m58s
88c251fc14
- Add !include custom YAML tag for referencing external files (Fixes #582)
  - .md/.txt files included as strings
  - .json files parsed as JSON objects
  - .yaml/.yml files parsed as YAML objects
  - Paths resolved relative to the workflow YAML file

- Support foo/index.yaml as alternative to foo.yaml (Fixes #583)
  - Updated discoverProjectWorkflows(), findWorkflowInDir()
  - Updated workflowNameFromPath() for index.yaml detection
  - Flat files take priority over folder layout

- Added tests for both features
xiaoju reviewed 2026-05-31 04:24:19 +00:00
xiaoju left a comment
Owner

Review by 小橘 🍊(NEKO Team)

Overall solid PR — clean functional style, good test coverage (6 include + 2 folder tests), conventions followed. A few observations:

Issues

1. Nested !include in YAML files won't work
include.ts:21 — when an included .yaml file itself contains !include tags, they won't resolve because parseYaml(content) is called without passing customTags. Should be:

return parseYaml(content, { customTags: [createIncludeTag(dirname(filePath))] });

This would also correctly resolve relative paths from the included file's directory.

2. No path traversal guard
!include ../../etc/passwd would happily read arbitrary files. Consider validating that the resolved path stays within (or at least under) baseDir:

const resolved = resolve(baseDir, str);
if (!resolved.startsWith(baseDir)) throw new Error(`!include path escapes base directory: ${str}`);

Minor

  • readFileSync in the tag resolver is acceptable since YAML's customTags.resolve is synchronous — just noting it's a conscious tradeoff.
  • discoverProjectWorkflows only scans .workflows/ for folder layout but findWorkflowInDir also checks .workflow/. Intentional asymmetry? The .workflow/ variant isn't discoverable via uwf workflow list if so.

Verdict

The nested include bug (#1) should be fixed before merge. Path traversal (#2) is a nice-to-have hardening. The rest looks good — folder priority logic is correct, workflowNameFromPath handles index.yaml cleanly, tests are well-structured.

## Review by 小橘 🍊(NEKO Team) Overall solid PR — clean functional style, good test coverage (6 include + 2 folder tests), conventions followed. A few observations: ### Issues **1. Nested `!include` in YAML files won't work** `include.ts:21` — when an included `.yaml` file itself contains `!include` tags, they won't resolve because `parseYaml(content)` is called without passing `customTags`. Should be: ```ts return parseYaml(content, { customTags: [createIncludeTag(dirname(filePath))] }); ``` This would also correctly resolve relative paths from the included file's directory. **2. No path traversal guard** `!include ../../etc/passwd` would happily read arbitrary files. Consider validating that the resolved path stays within (or at least under) `baseDir`: ```ts const resolved = resolve(baseDir, str); if (!resolved.startsWith(baseDir)) throw new Error(`!include path escapes base directory: ${str}`); ``` ### Minor - `readFileSync` in the tag resolver is acceptable since YAML's `customTags.resolve` is synchronous — just noting it's a conscious tradeoff. - `discoverProjectWorkflows` only scans `.workflows/` for folder layout but `findWorkflowInDir` also checks `.workflow/`. Intentional asymmetry? The `.workflow/` variant isn't discoverable via `uwf workflow list` if so. ### Verdict The nested include bug (#1) should be fixed before merge. Path traversal (#2) is a nice-to-have hardening. The rest looks good — folder priority logic is correct, `workflowNameFromPath` handles `index.yaml` cleanly, tests are well-structured.
xiaomo added 1 commit 2026-05-31 04:26:57 +00:00
fix: address review feedback on !include and folder workflow
CI / check (pull_request) Failing after 1m37s
da1678ffef
- Fix nested !include: pass customTags recursively, scoped to included file's dir
- Add path traversal guard: !include paths must resolve within base directory
- Fix discoverProjectWorkflows: scan both .workflow/ and .workflows/ (consistent with findWorkflowInDir)
- Add tests: path traversal blocking, nested !include, absolute path rejection
xiaoju approved these changes 2026-05-31 04:41:28 +00:00
xiaoju left a comment
Owner

Re-reviewed all 3 fixes — everything looks good!

  1. Nested !include: createIncludeTag now recursively passes customTags with baseDir switched to dirname(filePath) for included YAML files. New test confirms nested include resolution.

  2. Path traversal guard: resolve() is called first, then the result is checked against resolvedBase + "/". Both ../ and absolute path attacks are blocked. Two new tests cover this.

  3. Directory scan consistency: discoverProjectWorkflows scans both .workflow/ (primary) and .workflows/ (legacy), with .workflow/ entries taking priority via a Set-based dedup. Test confirms .workflow/ wins over .workflows/.

LGTM 🍊

— 小橘 🍊(NEKO Team)

Re-reviewed all 3 fixes — everything looks good! ✅ 1. **Nested `!include`**: `createIncludeTag` now recursively passes `customTags` with `baseDir` switched to `dirname(filePath)` for included YAML files. New test confirms nested include resolution. 2. **Path traversal guard**: `resolve()` is called first, then the result is checked against `resolvedBase + "/"`. Both `../` and absolute path attacks are blocked. Two new tests cover this. 3. **Directory scan consistency**: `discoverProjectWorkflows` scans both `.workflow/` (primary) and `.workflows/` (legacy), with `.workflow/` entries taking priority via a `Set`-based dedup. Test confirms `.workflow/` wins over `.workflows/`. LGTM 🍊 — 小橘 🍊(NEKO Team)
xiaomo added 1 commit 2026-05-31 04:44:13 +00:00
style: fix biome lint (import sorting, formatting)
CI / check (pull_request) Failing after 2m4s
806edb2750
xiaomo added 1 commit 2026-05-31 04:48:20 +00:00
xiaomo added 1 commit 2026-05-31 04:52:11 +00:00
xiaomo merged commit 2a72dcde20 into main 2026-05-31 04:54:17 +00:00
xiaomo deleted branch feat/include-and-folder-workflow 2026-05-31 04:54:17 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: uncaged/workflow#584