mirror of
https://github.com/sentimony/skills.git
synced 2026-09-19 07:12:05 +08:00
17ea97d943
Release six updated skills with audit and feedback fixes. Harden Vitest auto-selected script execution and its runtime trust boundary, add regression coverage, improve web-debug checkpoint safety, and strengthen CI validation.
137 lines
6.2 KiB
YAML
137 lines
6.2 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
|
|
jobs:
|
|
validate:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- uses: actions/setup-python@v7
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Validate SKILL.md frontmatter
|
|
run: |
|
|
python - <<'EOF'
|
|
import pathlib, re, sys
|
|
|
|
errors = []
|
|
for skill_md in sorted(pathlib.Path("skills").glob("*/SKILL.md")):
|
|
text = skill_md.read_text(encoding="utf-8")
|
|
m = re.match(r"^---\n(.*?)\n---\n", text, re.DOTALL)
|
|
if not m:
|
|
errors.append(f"{skill_md}: missing YAML frontmatter")
|
|
continue
|
|
fm = m.group(1)
|
|
name = re.search(r"^name:\s*(\S+)\s*$", fm, re.MULTILINE)
|
|
if not name:
|
|
errors.append(f"{skill_md}: missing 'name'")
|
|
elif name.group(1) != skill_md.parent.name:
|
|
errors.append(f"{skill_md}: name '{name.group(1)}' != directory '{skill_md.parent.name}'")
|
|
if not re.search(r"^description:\s*\S", fm, re.MULTILINE):
|
|
errors.append(f"{skill_md}: missing 'description'")
|
|
if not re.search(r'^\s*version:\s*"\d+\.\d+\.\d+"\s*$', fm, re.MULTILINE):
|
|
errors.append(f"{skill_md}: metadata.version must be plain semver X.Y.Z")
|
|
|
|
for err in errors:
|
|
print(f"::error::{err}")
|
|
sys.exit(1 if errors else 0)
|
|
EOF
|
|
|
|
- name: Compile Python scripts and examples
|
|
run: |
|
|
python -m compileall -q skills/*/scripts skills/*/examples
|
|
|
|
- name: Run skill test suites
|
|
run: |
|
|
set -e
|
|
for test in $(find skills -name 'test_*.py' | sort); do
|
|
echo "::group::$test"
|
|
python "$test"
|
|
echo "::endgroup::"
|
|
done
|
|
|
|
- name: Check for hidden/bidirectional Unicode
|
|
run: |
|
|
# The forbidden set is defined once, as INVISIBLE_CODEPOINT_CLASS in
|
|
# skills/vitest/scripts/run_vitest.py, and read from there rather than spelled
|
|
# again: a second copy is a copy that drifts, which is how U+061C ARABIC LETTER
|
|
# MARK ended up in one list and not the others. It is read as source text rather
|
|
# than imported, so this step executes none of the code it is checking. What that
|
|
# class must contain is asserted separately, against unicodedata, in
|
|
# test_run_vitest.py.
|
|
python - "$RUNNER_TEMP" <<'EOF'
|
|
import ast, pathlib, sys
|
|
|
|
# Read the assignment, do not import the module: importing runs whatever the
|
|
# runner does at import time, and the scanner that decides whether this
|
|
# repository is clean should not depend on the file it is scanning executing
|
|
# first. ast.literal_eval accepts the string literal and nothing else.
|
|
source = pathlib.Path("skills/vitest/scripts/run_vitest.py").read_text(encoding="utf-8")
|
|
forbidden = None
|
|
for node in ast.parse(source).body:
|
|
if isinstance(node, ast.Assign) and any(
|
|
isinstance(target, ast.Name) and target.id == "INVISIBLE_CODEPOINT_CLASS"
|
|
for target in node.targets
|
|
):
|
|
forbidden = ast.literal_eval(node.value)
|
|
if not isinstance(forbidden, str) or not forbidden:
|
|
print("::error::INVISIBLE_CODEPOINT_CLASS is not a string literal in run_vitest.py")
|
|
sys.exit(1)
|
|
|
|
codepoints, index = [], 0
|
|
while index < len(forbidden):
|
|
if index + 2 < len(forbidden) and forbidden[index + 1] == "-":
|
|
codepoints.extend(range(ord(forbidden[index]), ord(forbidden[index + 2]) + 1))
|
|
index += 3
|
|
else:
|
|
codepoints.append(ord(forbidden[index]))
|
|
index += 1
|
|
|
|
temp = pathlib.Path(sys.argv[1])
|
|
pattern = "[" + "".join(f"\\x{{{point:04X}}}" for point in codepoints) + "]"
|
|
temp.joinpath("bidi-pattern").write_text(pattern, encoding="utf-8")
|
|
temp.joinpath("bidi-expected").write_text(str(len(codepoints)), encoding="utf-8")
|
|
temp.joinpath("bidi-control").write_text(
|
|
"".join(f"control {point:04X} before{chr(point)}after\n" for point in codepoints),
|
|
encoding="utf-8",
|
|
)
|
|
EOF
|
|
pattern=$(cat "$RUNNER_TEMP/bidi-pattern")
|
|
expected=$(cat "$RUNNER_TEMP/bidi-expected")
|
|
|
|
# Positive control first, one line per forbidden codepoint. This scan passes by
|
|
# finding nothing, so a pattern or a flag that stops matching makes it silently
|
|
# vacuous rather than noisy - the -I and --exclude-dir below were added
|
|
# reactively and could have done exactly that. Every codepoint has to be matched
|
|
# individually: a control that exercises one of them proves nothing about the
|
|
# rest. The control file lives outside the checkout so the real scan never sees it.
|
|
matched=$(grep -cPI "$pattern" "$RUNNER_TEMP/bidi-control" || true)
|
|
if [ "$matched" != "$expected" ]; then
|
|
echo "::error::hidden-Unicode scan matched $matched of $expected codepoints in its own positive control"
|
|
exit 1
|
|
fi
|
|
|
|
# -I and --exclude-dir skip build artifacts: compiling and running the
|
|
# suites leaves .pyc files in which a source-level \uXXXX escape appears
|
|
# as the literal codepoint. The check is about source, not bytecode.
|
|
#
|
|
# The exit codes are separated rather than negated with `! grep`, which reports
|
|
# success for both "found nothing" (1) and "the scanner failed" (2) - an
|
|
# unreadable file or a pattern grep rejects would have passed as clean.
|
|
status=0
|
|
grep -rnPI --exclude-dir=__pycache__ "$pattern" skills README.md AGENTS.md CHANGELOG.md || status=$?
|
|
if [ "$status" -eq 0 ]; then
|
|
echo "::error::hidden or bidirectional Unicode found in the files listed above"
|
|
exit 1
|
|
fi
|
|
if [ "$status" -ne 1 ]; then
|
|
echo "::error::hidden-Unicode scan failed with exit status $status"
|
|
exit 1
|
|
fi
|