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
3 changes: 3 additions & 0 deletions .github/workflows/skill-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ jobs:
- name: Run parser regression test
run: ruby scripts/validate_skills.rb --self-test

- name: Run upstream sync regression test
run: ruby scripts/sync_upstreams.rb --self-test

- name: Validate skill tree
run: ruby scripts/validate_skills.rb
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ author: anthropics

```shell
ruby scripts/validate_skills.rb --self-test
ruby scripts/sync_upstreams.rb --self-test
ruby scripts/validate_skills.rb
ruby scripts/sync_upstreams.rb
```
Expand Down
26 changes: 26 additions & 0 deletions scripts/sync_upstreams.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ def binary_bytes?(bytes)
end

def safe_join(root, relative)
# `upstreamPath: .` is a valid root. Normalize it before comparing the
# expanded child path; otherwise a root ending in `/.` makes every file
# look like it escaped the repository (for example `.gitattributes`).
root = File.expand_path(root)
path = File.expand_path(relative, root)
unless path == root || path.start_with?("#{root}#{File::SEPARATOR}")
raise SyncError, "refusing path outside repository: #{relative.inspect}"
Expand Down Expand Up @@ -136,6 +140,28 @@ def write_github_outputs(changed, conflicts)
end
end

def run_self_test
Dir.mktmpdir("easycode-skill-sync-self-test-") do |root|
root_with_dot = File.join(root, ".")
expected = File.join(root, ".gitattributes")
actual = safe_join(root_with_dot, ".gitattributes")
raise "root-relative path was not normalized" unless actual == expected

begin
safe_join(root_with_dot, "../outside")
raise "path traversal was not rejected"
rescue SyncError
# Expected: safe_join must reject paths outside the repository root.
end
end
puts "[self-test] safe_join root normalization and traversal guard passed"
end

if ARGV.delete("--self-test")
run_self_test
exit 0
end

targets = []
Dir[File.join(SKILLS_ROOT, "*", "SKILL.md")].sort.each do |skill_path|
metadata = parse_frontmatter(skill_path)
Expand Down
Loading