feat(builder): Android/iOS 构建与 copy_artifact 发布
新增 APK/IPA 轨道和 copy_artifact.sh(本地目录或 container:/dir)。 push 必须委托该脚本,禁止在 makefile 里内联 docker cp。
This commit is contained in:
@@ -109,6 +109,9 @@ class BuilderCheckTests(unittest.TestCase):
|
||||
self.assertIn("ARCHES", readme)
|
||||
self.assertIn("amd64 arm64", readme)
|
||||
self.assertIn("多架构", readme)
|
||||
self.assertIn("copy_artifact.sh", skill)
|
||||
self.assertIn("android", skill.lower())
|
||||
self.assertIn("COPY_DEST", contract)
|
||||
|
||||
def test_no_makefile_without_ready_is_usage_error(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
@@ -406,6 +409,61 @@ class BuilderCheckTests(unittest.TestCase):
|
||||
self.assertIn("Platform: linux/amd64", overridden.stdout)
|
||||
self.assertNotIn("linux/arm64", overridden.stdout)
|
||||
|
||||
def test_android_copy_makefile_skips_missing_copy_dest(self) -> None:
|
||||
template = REPO_ROOT / "skills" / "builder" / "templates" / "makefile.android.builder"
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
project = Path(temp)
|
||||
init_repo(project)
|
||||
text = template.read_text(encoding="utf-8")
|
||||
text = text.replace(
|
||||
"include $(HOME)/.pouch/skills/builder/scripts/version.mk",
|
||||
f"include {SCRIPTS / 'version.mk'}",
|
||||
)
|
||||
(project / "makefile.builder").write_text(text, encoding="utf-8")
|
||||
code, out = run_check(project, "--ready")
|
||||
self.assertEqual(code, 0, out)
|
||||
self.assertIn("[PASS] 12. android 目标产物形状与纯构建", out)
|
||||
self.assertIn("[SKIP] 13. ios 目标产物形状与纯构建", out)
|
||||
self.assertIn("COPY_DEST: MISSING", out)
|
||||
self.assertIn("blocks publish, not build", out)
|
||||
|
||||
def test_inline_docker_cp_in_push_fails(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
project = Path(temp)
|
||||
init_repo(project)
|
||||
(project / "makefile.builder").write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"ARCHES ?= amd64",
|
||||
"ARCH ?= amd64",
|
||||
"ifneq ($(filter $(ARCH),$(ARCHES)),$(ARCH))",
|
||||
"$(error ARCH must be one of: $(ARCHES))",
|
||||
"endif",
|
||||
"DIST_DIR ?= dist",
|
||||
f"include {SCRIPTS / 'version.mk'}",
|
||||
".PHONY: help version arches clean build push",
|
||||
"help:",
|
||||
"\t@echo app $(VERSION)",
|
||||
"version:",
|
||||
"\t@echo $(VERSION)",
|
||||
"arches:",
|
||||
"\t@echo $(ARCHES)",
|
||||
"clean:",
|
||||
"\trm -rf $(DIST_DIR)",
|
||||
"build:",
|
||||
"\tmkdir -p $(DIST_DIR)/$(ARCH).apk",
|
||||
"push:",
|
||||
"\tdocker cp dist/app.apk file-server:/srv/app.apk",
|
||||
"",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
code, out = run_check(project, "--ready")
|
||||
self.assertEqual(code, 1, out)
|
||||
self.assertIn("[FAIL] 7. push 仅委托 builder 脚本", out)
|
||||
self.assertIn("docker cp", out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = REPO_ROOT / "skills" / "builder" / "scripts" / "copy_artifact.sh"
|
||||
|
||||
|
||||
def git(cwd: Path, *args: str) -> None:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
cwd=cwd,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
env={
|
||||
**os.environ,
|
||||
"GIT_AUTHOR_NAME": "Test",
|
||||
"GIT_AUTHOR_EMAIL": "test@example.com",
|
||||
"GIT_COMMITTER_NAME": "Test",
|
||||
"GIT_COMMITTER_EMAIL": "test@example.com",
|
||||
},
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise AssertionError(f"git {args} failed: {result.stderr}")
|
||||
|
||||
|
||||
class CopyArtifactTests(unittest.TestCase):
|
||||
def test_local_copy_and_alias(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
git(root, "init", "-b", "main")
|
||||
git(root, "config", "user.email", "test@example.com")
|
||||
git(root, "config", "user.name", "Test")
|
||||
src = root / "app.apk"
|
||||
src.write_bytes(b"apk")
|
||||
git(root, "add", "app.apk")
|
||||
git(root, "commit", "-m", "apk")
|
||||
dest = root / "out"
|
||||
result = subprocess.run(
|
||||
[str(SCRIPT), "-d", str(dest), "-a", "latest.apk", str(src)],
|
||||
cwd=root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertTrue((dest / "app.apk").is_file())
|
||||
self.assertTrue((dest / "latest.apk").is_file())
|
||||
self.assertEqual((dest / "latest.apk").read_bytes(), b"apk")
|
||||
self.assertIn("copy_artifact: 1 file(s)", result.stdout)
|
||||
|
||||
def test_dirty_tree_requires_allow(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
git(root, "init", "-b", "main")
|
||||
git(root, "config", "user.email", "test@example.com")
|
||||
git(root, "config", "user.name", "Test")
|
||||
src = root / "app.apk"
|
||||
src.write_bytes(b"apk")
|
||||
git(root, "add", "app.apk")
|
||||
git(root, "commit", "-m", "apk")
|
||||
src.write_bytes(b"changed")
|
||||
dest = root / "out"
|
||||
blocked = subprocess.run(
|
||||
[str(SCRIPT), "-d", str(dest), str(src)],
|
||||
cwd=root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(blocked.returncode, 3, blocked.stderr)
|
||||
allowed = subprocess.run(
|
||||
[str(SCRIPT), "-d", str(dest), str(src)],
|
||||
cwd=root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
env={**os.environ, "ALLOW_UNCOMMITTED": "1"},
|
||||
)
|
||||
self.assertEqual(allowed.returncode, 0, allowed.stdout + allowed.stderr)
|
||||
|
||||
def test_dry_run_docker_dest_prints_docker_cp(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
git(root, "init", "-b", "main")
|
||||
git(root, "config", "user.email", "test@example.com")
|
||||
git(root, "config", "user.name", "Test")
|
||||
src = root / "app.apk"
|
||||
src.write_bytes(b"apk")
|
||||
git(root, "add", "app.apk")
|
||||
git(root, "commit", "-m", "apk")
|
||||
result = subprocess.run(
|
||||
[
|
||||
str(SCRIPT),
|
||||
"--dry-run",
|
||||
"-d",
|
||||
"file-server:/srv",
|
||||
"-a",
|
||||
"latest.apk",
|
||||
str(src),
|
||||
],
|
||||
cwd=root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("docker cp", result.stdout)
|
||||
self.assertIn("file-server:/srv/app.apk", result.stdout)
|
||||
self.assertIn("file-server:/srv/latest.apk", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user