feat: establish Lyra CLI with pinned Aster engine and remote CI
Verify Lyra CLI / verify (push) Failing after 1s

This commit is contained in:
2026-10-20 01:10:02 +08:00
commit af5c5f7a25
27 changed files with 1164 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Lyra 验证脚本
| 文件 | 职责 |
| --- | --- |
| check.sh | 远端格式、CLI 进程测试、Clippy 和报告脚本回归 |
| endor_examples.py | 构建真实 lyra,执行固定 Aster 的 cases.json,保存实际输出 |
| test_endor_examples.py | 远端报告失败/超时/输出不匹配检查 |
| ci-ces-control.sh | 专用检查容器,复制报告和 Linux 二进制,清理容器 |
| README.md | 本目录入口 |
报告不冒充整体 CI 结果,失败时保留已有诊断。无数据库或生产部署。返回 [项目入口](../README.md)。
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
# Run only in the designated remote validation environment.
set -eu
cargo fmt --all --check
cargo test --workspace --locked
cargo clippy --workspace --all-targets --locked -- -D warnings
python3 scripts/test_endor_examples.py
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
# Remote CI only. No production deployment or resources.
set -eu
cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
release_sha=${LYRA_RELEASE_SHA:?LYRA_RELEASE_SHA is required}
case "$release_sha" in *[!0-9a-f]*|'') exit 1;; esac
test "${#release_sha}" -eq 40
image="endorphin-lyra-cli-ci:$release_sha"
checks="endorphin-lyra-cli-checks-${GITHUB_RUN_ID:-manual}-$$"
mkdir -p ci-output
printf '# Lyra Endor examples\n\nStatus: NOT_RUN; inspect CI logs.\n' > ci-output/endor-examples.md
printf '{"status":"NOT_RUN","cases":[]}\n' > ci-output/endor-examples.json
cleanup() { docker rm -f "$checks" >/dev/null 2>&1 || true; }
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
docker build -f Dockerfile.ci -t "$image" .
status=0
docker run --name "$checks" --init --env LYRA_RELEASE_SHA="$release_sha" "$image" sh -eu -c '
checks=0
sh scripts/check.sh || checks=$?
examples=0
python3 scripts/endor_examples.py --output-dir /build/ci-output || examples=$?
test "$checks" -eq 0
test "$examples" -eq 0
cargo build --release --locked -p cli
cp target/release/lyra ci-output/lyra
' || status=$?
copy_status=0
docker cp "$checks:/build/ci-output/endor-examples.md" ci-output/endor-examples.md || copy_status=$?
docker cp "$checks:/build/ci-output/endor-examples.json" ci-output/endor-examples.json || copy_status=$?
if [ "$status" -eq 0 ]; then
docker cp "$checks:/build/ci-output/lyra" ci-output/lyra || copy_status=$?
fi
if [ "$status" -ne 0 ]; then exit "$status"; fi
exit "$copy_status"
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Run Endor example files and retain actual output on the designated CI host.
Never run locally: this builds the CLI and executes language code.
"""
import argparse
from datetime import datetime, timezone
import json
import os
from pathlib import Path
import re
import shlex
import subprocess
ROOT = Path(__file__).resolve().parents[1]
EXAMPLES = ROOT / "vendor/aster/crates/compiler/examples"
def decoded(value):
return value.decode("utf-8", "replace") if isinstance(value, bytes) else value or ""
def invoke(command, timeout):
"""Keep stdout, stderr and failure state even for launch errors/timeouts."""
record = {"command": command, "exit_code": None, "stdout": "", "stderr": "", "error": None}
try:
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=timeout)
record.update(exit_code=result.returncode, stdout=result.stdout, stderr=result.stderr)
except subprocess.TimeoutExpired as error:
record.update(stdout=decoded(error.stdout), stderr=decoded(error.stderr),
error=f"timed out after {timeout} seconds")
except OSError as error:
record["error"] = str(error)
return record
def load_cases():
manifest = json.loads((EXAMPLES / "cases.json").read_text(encoding="utf-8"))
if manifest.get("format_version") != 1:
raise ValueError("unsupported example manifest version")
cases = manifest["cases"]
names, sources = set(), set()
for case in cases:
if not re.fullmatch(r"[a-z][a-z0-9_-]*", case["name"]) or case["name"] in names:
raise ValueError("invalid or duplicate example name")
source = EXAMPLES / case["source"]
if source.resolve().parent != EXAMPLES.resolve() or source.suffix != ".endor":
raise ValueError("example source must be a direct .endor file")
if case["mode"] not in ("run", "eval") or not isinstance(case["expected_stdout"], str):
raise ValueError("invalid example mode or expected output")
if case["source"] in sources:
raise ValueError("duplicate example source")
case["source_text"] = source.read_text(encoding="utf-8")
names.add(case["name"])
sources.add(case["source"])
if not cases or sources != {path.name for path in EXAMPLES.glob("*.endor")}:
raise ValueError("manifest must cover every .endor example exactly once")
return cases
def fence(text, language="text"):
# Keep source/output literal even if future examples print Markdown fences.
longest = max((len(match[0]) for match in re.finditer(r"`+", text)), default=0)
delimiter = "`" * max(3, longest + 1)
return f"{delimiter}{language}\n{text}" + ("" if text.endswith("\n") else "\n") + delimiter
def markdown(report):
lines = ["# Endor 示例实际运行结果", "", f"提交:`{report['commit']}`",
f"状态:**{report['status']}**", f"生成时间(UTC):{report['generated_at']}",
"", "本报告仅覆盖 Endor 示例;整个 CI/CD 的状态以工作流结果为准。"]
if report.get("error"):
lines += ["", "报告阶段错误:", fence(report["error"])]
for stage in report["preparation"]:
lines += ["", "## 准备:" + shlex.join(stage["command"]),
f"退出码:{stage['exit_code']};错误:{stage['error'] or '无'}"]
if stage["exit_code"] != 0 or stage["error"]:
lines += ["", "stdout:", fence(stage["stdout"]), "", "stderr:", fence(stage["stderr"])]
for case in report["cases"]:
lines += ["", f"## {case['name']} — {case['status']}", "",
"命令:", fence(shlex.join(case["command"]), "sh"), "", "源码:",
fence(case["source_text"], "endor"), "", "预期 stdout:",
fence(case["expected_stdout"]), "", "实际 stdout:", fence(case["stdout"]),
"", "实际 stderr:", fence(case["stderr"]), "",
f"预期退出码:0;实际退出码:{case['exit_code']}",
f"错误:{case['error'] or '无'}"]
return "\n".join(lines) + "\n"
def collect(report):
for case in load_cases():
report["cases"].append({**case, "status": "NOT_RUN", "exit_code": None,
"stdout": "", "stderr": "", "error": None,
"expected_stderr": "", "expected_exit_code": 0,
"command": ["lyra", case["mode"], "vendor/aster/crates/compiler/examples/" + case["source"]]})
build = invoke(["cargo", "build", "-p", "cli", "--locked"], timeout=600)
report["preparation"].append(build)
if build["exit_code"] != 0 or build["error"]:
report.update(status="NOT_RUN", error="CLI build failed; examples were not executed.")
return
metadata = invoke(["cargo", "metadata", "--locked", "--no-deps", "--format-version", "1"], timeout=60)
report["preparation"].append(metadata)
if metadata["exit_code"] != 0 or metadata["error"]:
report.update(status="NOT_RUN", error="Cannot locate the compiled CLI.")
return
binary = Path(json.loads(metadata["stdout"])["target_directory"]) / "debug" / "lyra"
for case in report["cases"]:
command = [str(binary), *case["command"][1:]]
actual = invoke(command, timeout=30)
case.update({key: actual[key] for key in ["exit_code", "stdout", "stderr", "error"]})
case["binary"] = str(binary)
case["status"] = "PASS" if (
case["exit_code"] == case["expected_exit_code"] and not case["error"]
and case["stdout"] == case["expected_stdout"]
and case["stderr"] == case["expected_stderr"]
) else "FAIL"
report["status"] = "PASS" if all(case["status"] == "PASS" for case in report["cases"]) else "FAIL"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output-dir", type=Path, required=True)
args = parser.parse_args()
args.output_dir.mkdir(parents=True, exist_ok=True)
report = {"format_version": 1, "commit": os.environ.get("LYRA_RELEASE_SHA", "unknown"),
"generated_at": datetime.now(timezone.utc).isoformat(), "status": "NOT_RUN",
"error": None, "preparation": [], "cases": []}
try:
collect(report)
except Exception as error:
report.update(status="FAIL", error=str(error))
# Always persist diagnostics before returning a failing CI exit code.
args.output_dir.joinpath("endor-examples.json").write_text(
json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
rendered = markdown(report)
args.output_dir.joinpath("endor-examples.md").write_text(rendered, encoding="utf-8")
print(rendered)
return 0 if report["status"] == "PASS" else 1
if __name__ == "__main__":
raise SystemExit(main())
+80
View File
@@ -0,0 +1,80 @@
"""Remote-only regression checks for example output capture and failure reports."""
import contextlib
import io
import json
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest.mock import patch
import endor_examples as examples
def result(stdout="", code=0):
return {"command": ["mock"], "exit_code": code, "stdout": stdout, "stderr": "", "error": None}
def report():
return {"format_version": 1, "commit": "test", "generated_at": "test", "status": "NOT_RUN",
"preparation": [], "cases": [], "error": None}
class ExampleReportTests(unittest.TestCase):
def test_manifest_runs_every_source_and_captures_exact_output(self):
cases = examples.load_cases()
responses = [result(), result(json.dumps({"target_directory": "/tmp/ci-target"}))]
responses += [result(case["expected_stdout"]) for case in cases]
collected = report()
with patch.object(examples, "invoke", side_effect=responses) as invoke:
examples.collect(collected)
self.assertEqual(collected["status"], "PASS")
self.assertEqual(invoke.call_count, len(cases) + 2)
for actual, expected in zip(collected["cases"], cases):
self.assertEqual(actual["stdout"], expected["expected_stdout"])
self.assertEqual(actual["status"], "PASS")
self.assertEqual(actual["binary"], "/tmp/ci-target/debug/lyra")
def test_output_mismatch_fails_and_build_failure_never_claims_examples_ran(self):
cases = examples.load_cases()
responses = [result(), result(json.dumps({"target_directory": "/tmp/target"}))]
responses += [result("wrong\n")] + [result(case["expected_stdout"]) for case in cases[1:]]
collected = report()
with patch.object(examples, "invoke", side_effect=responses):
examples.collect(collected)
self.assertEqual(collected["status"], "FAIL")
self.assertEqual(collected["cases"][0]["stdout"], "wrong\n")
self.assertEqual(collected["cases"][0]["status"], "FAIL")
self.assertIn("wrong", examples.markdown(collected))
collected = report()
with patch.object(examples, "invoke", return_value=result(code=1)) as invoke:
examples.collect(collected)
self.assertEqual(invoke.call_count, 1)
self.assertEqual(collected["status"], "NOT_RUN")
self.assertTrue(all(case["status"] == "NOT_RUN" for case in collected["cases"]))
def test_timeout_keeps_partial_utf8_stdout_and_stderr(self):
failure = subprocess.TimeoutExpired(["cli"], 1, output="日".encode(), stderr=b"partial error")
with patch.object(examples.subprocess, "run", side_effect=failure):
captured = examples.invoke(["cli"], 1)
self.assertIsNone(captured["exit_code"])
self.assertEqual(captured["stdout"], "日")
self.assertEqual(captured["stderr"], "partial error")
self.assertIn("timed out", captured["error"])
def test_unexpected_failure_still_writes_both_artifacts(self):
with tempfile.TemporaryDirectory() as directory:
with patch("sys.argv", ["endor_examples.py", "--output-dir", directory]), \
patch.object(examples, "collect", side_effect=ValueError("broken manifest")), \
contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(examples.main(), 1)
payload = json.loads(Path(directory, "endor-examples.json").read_text(encoding="utf-8"))
self.assertEqual(payload["status"], "FAIL")
self.assertEqual(payload["error"], "broken manifest")
self.assertIn("broken manifest", Path(directory, "endor-examples.md").read_text(encoding="utf-8"))
self.assertTrue(examples.fence("```\n日").startswith("````text\n"))
if __name__ == "__main__":
unittest.main()