Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
103 changes: 59 additions & 44 deletions skill-runtime/package_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
import zipfile
from pathlib import Path

from skill_profiles import PROFILES, SkillProfile, render_profile, skill_profile

ROOT = Path(__file__).resolve().parents[1]
SKILL_SOURCE = ROOT / "skills/iac-code"
PUBLIC_ORIGIN = "https://ros-public-tools.oss-cn-beijing.aliyuncs.com"
PRODUCT_PREFIX = "github-releases/aliyun/iac-code"
RUNTIME_PYTHON = "cp312"
SKILL_FILES = ("SKILL.md", "agents/openai.yaml", "scripts/iac_code.py")
_SEMVER_PATTERN = re.compile(r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)")
_TAG_PATTERN = re.compile(r"v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)")
_CANDIDATE_PATTERN = re.compile(r"candidate-[0-9]{8}T[0-9]{6}Z-([0-9a-f]{12})")
Expand All @@ -44,26 +44,36 @@ def _candidate(value: str | None, source_commit: str, name: str) -> str | None:


def _validate_args(args: argparse.Namespace) -> None:
profile = skill_profile(args.profile)
for name in ("source_commit", "publisher_commit"):
if _COMMIT_PATTERN.fullmatch(getattr(args, name)) is None:
raise SystemExit("--{} must be a lowercase 40-character Git commit".format(name.replace("_", "-")))
if _RFC3339_PATTERN.fullmatch(args.published_at) is None:
raise SystemExit("--published-at must use UTC RFC3339 form YYYY-MM-DDTHH:MM:SSZ")
if _DIGEST_PATTERN.fullmatch(args.manifest_sha256) is None or set(args.manifest_sha256) == {"0"}:
if args.skill_version and _SEMVER_PATTERN.fullmatch(args.skill_version) is None:
raise SystemExit("--skill-version must use canonical X.Y.Z")
_candidate(args.skill_candidate_id, args.source_commit, "--skill-candidate-id")
if not profile.requires_runtime:
runtime_values = (args.runtime_tag, args.runtime_candidate_id, args.iac_code_version, args.manifest_sha256)
if any(value is not None for value in runtime_values) or args.manifest_size is not None:
raise SystemExit("{} does not accept Runtime arguments".format(profile.name))
return
if args.manifest_sha256 is None or _DIGEST_PATTERN.fullmatch(args.manifest_sha256) is None:
raise SystemExit("--manifest-sha256 must be a non-placeholder lowercase SHA-256")
if set(args.manifest_sha256) == {"0"}:
raise SystemExit("--manifest-sha256 must be a non-placeholder lowercase SHA-256")
if args.manifest_size <= 0:
if args.manifest_size is None or args.manifest_size <= 0:
raise SystemExit("--manifest-size must be positive")
if args.runtime_python != RUNTIME_PYTHON:
raise SystemExit("the initial Skill contract requires runtime Python cp312")
if args.skill_version and _SEMVER_PATTERN.fullmatch(args.skill_version) is None:
raise SystemExit("--skill-version must use canonical X.Y.Z")
if args.runtime_tag:
match = _TAG_PATTERN.fullmatch(args.runtime_tag)
if match is None or args.runtime_tag != "v{}".format(args.iac_code_version):
raise SystemExit("--runtime-tag must be the canonical tag for --iac-code-version")
if _SEMVER_PATTERN.fullmatch(args.iac_code_version) is None:
if args.runtime_tag is None and args.runtime_candidate_id is None:
raise SystemExit("runtime-backed Skill requires --runtime-tag or --runtime-candidate-id")
if args.iac_code_version is None or _SEMVER_PATTERN.fullmatch(args.iac_code_version) is None:
raise SystemExit("--iac-code-version must use canonical X.Y.Z")
_candidate(args.skill_candidate_id, args.source_commit, "--skill-candidate-id")
runtime_source = args.runtime_source_commit or args.source_commit
if _COMMIT_PATTERN.fullmatch(runtime_source) is None:
raise SystemExit("--runtime-source-commit must be a lowercase 40-character Git commit")
Expand All @@ -80,11 +90,20 @@ def runtime_manifest_url(args: argparse.Namespace) -> str:
return "/".join((PUBLIC_ORIGIN, PRODUCT_PREFIX, suffix))


def skill_public_url(args: argparse.Namespace) -> str:
if args.skill_version:
suffix = "skill/releases/{}/iac-code-skill-{}.zip".format(args.skill_version, args.skill_version)
def skill_public_url(args: argparse.Namespace, profile: SkillProfile) -> str:
if profile.name == "iac-code":
if args.skill_version:
suffix = "skill/releases/{}/iac-code-skill-{}.zip".format(args.skill_version, args.skill_version)
else:
suffix = "skill/candidates/{}/iac-code-skill.zip".format(args.skill_candidate_id)
elif args.skill_version:
suffix = "skills/{}/releases/{}/{}-skill-{}.zip".format(
profile.name, args.skill_version, profile.name, args.skill_version
)
else:
suffix = "skill/candidates/{}/iac-code-skill.zip".format(args.skill_candidate_id)
suffix = "skills/{}/candidates/{}/{}-skill.zip".format(
profile.name, args.skill_candidate_id, profile.name
)
return "/".join((PUBLIC_ORIGIN, PRODUCT_PREFIX, suffix))


Expand All @@ -98,15 +117,11 @@ def _replace_constant(source: str, name: str, value: str) -> str:


def _stage(args: argparse.Namespace, root: Path) -> Path:
skill_root = root / "iac-code"
for relative in SKILL_FILES:
source = SKILL_SOURCE / relative
if not source.is_file():
raise SystemExit("Skill source is missing {}".format(relative))
destination = skill_root / relative
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(source.read_bytes())
bridge_path = skill_root / "scripts/iac_code.py"
profile = skill_profile(args.profile)
skill_root = render_profile(profile.name, root / profile.archive_root)
if not profile.requires_runtime:
return skill_root
bridge_path = skill_root / profile.bridge_path
bridge = bridge_path.read_text(encoding="utf-8")
runtime_identity = args.runtime_tag or args.runtime_candidate_id
skill_identity = args.skill_version or args.skill_candidate_id
Expand All @@ -122,47 +137,45 @@ def _stage(args: argparse.Namespace, root: Path) -> Path:
bridge = _replace_constant(bridge, name, value)
ast.parse(bridge, filename=str(bridge_path), feature_version=(3, 8))
bridge_path.write_text(bridge, encoding="utf-8", newline="\n")
actual = sorted(path.relative_to(skill_root).as_posix() for path in skill_root.rglob("*") if path.is_file())
if actual != sorted(SKILL_FILES):
raise SystemExit("Skill staging contains files outside the package whitelist")
return skill_root


def deterministic_zip(skill_root: Path, output: Path) -> None:
def deterministic_zip(skill_root: Path, profile: SkillProfile, output: Path) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for relative in SKILL_FILES:
for relative in profile.output_files:
source = skill_root / relative
info = zipfile.ZipInfo("iac-code/" + relative, date_time=(1980, 1, 1, 0, 0, 0))
info = zipfile.ZipInfo(profile.archive_root + "/" + relative, date_time=(1980, 1, 1, 0, 0, 0))
info.create_system = 3
mode = 0o755 if relative == "scripts/iac_code.py" else 0o644
info.external_attr = (0o100000 | mode) << 16
info.compress_type = zipfile.ZIP_DEFLATED
archive.writestr(info, source.read_bytes(), compress_type=zipfile.ZIP_DEFLATED, compresslevel=9)


def release_manifest(args: argparse.Namespace, archive: Path) -> dict[str, object]:
runtime_identity_name = "runtimeTag" if args.runtime_tag else "runtimeCandidateId"
runtime_identity = args.runtime_tag or args.runtime_candidate_id
def release_manifest(args: argparse.Namespace, profile: SkillProfile, archive: Path) -> dict[str, object]:
value: dict[str, object] = {
"schemaVersion": 1,
"kind": "iac-code-skill-release" if args.skill_version else "iac-code-skill-candidate",
"kind": "{}-skill-{}".format(profile.name, "release" if args.skill_version else "candidate"),
"skillName": profile.name,
"skillSourceCommit": args.source_commit,
"publisherCommit": args.publisher_commit,
"publishedAt": args.published_at,
runtime_identity_name: runtime_identity,
"runtimeManifest": {
"url": runtime_manifest_url(args),
"size": args.manifest_size,
"sha256": args.manifest_sha256,
},
"skill": {
"name": archive.name,
"url": skill_public_url(args),
"url": skill_public_url(args, profile),
"size": archive.stat().st_size,
"sha256": sha256(archive),
},
}
if profile.requires_runtime:
runtime_identity_name = "runtimeTag" if args.runtime_tag else "runtimeCandidateId"
value[runtime_identity_name] = args.runtime_tag or args.runtime_candidate_id
value["runtimeManifest"] = {
"url": runtime_manifest_url(args),
"size": args.manifest_size,
"sha256": args.manifest_sha256,
}
if args.skill_version:
value["skillVersion"] = args.skill_version
else:
Expand All @@ -172,16 +185,17 @@ def release_manifest(args: argparse.Namespace, archive: Path) -> dict[str, objec

def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--profile", choices=sorted(PROFILES), default="iac-code")
skill = parser.add_mutually_exclusive_group(required=True)
skill.add_argument("--skill-version")
skill.add_argument("--skill-candidate-id")
runtime = parser.add_mutually_exclusive_group(required=True)
runtime = parser.add_mutually_exclusive_group()
runtime.add_argument("--runtime-tag")
runtime.add_argument("--runtime-candidate-id")
parser.add_argument("--iac-code-version", required=True)
parser.add_argument("--iac-code-version")
parser.add_argument("--runtime-python", default=RUNTIME_PYTHON)
parser.add_argument("--manifest-sha256", required=True)
parser.add_argument("--manifest-size", type=int, required=True)
parser.add_argument("--manifest-sha256")
parser.add_argument("--manifest-size", type=int)
parser.add_argument("--source-commit", required=True)
parser.add_argument("--runtime-source-commit")
parser.add_argument("--publisher-commit", required=True)
Expand All @@ -194,10 +208,11 @@ def parse_args() -> argparse.Namespace:
def main() -> int:
args = parse_args()
_validate_args(args)
profile = skill_profile(args.profile)
with tempfile.TemporaryDirectory(prefix="iac-code-skill-package-") as temporary:
skill_root = _stage(args, Path(temporary))
deterministic_zip(skill_root, args.output)
manifest = release_manifest(args, args.output)
deterministic_zip(skill_root, profile, args.output)
manifest = release_manifest(args, profile, args.output)
args.manifest_output.parent.mkdir(parents=True, exist_ok=True)
encoded = json.dumps(manifest, indent=2, sort_keys=True) + "\n"
args.manifest_output.write_text(encoded, encoding="utf-8", newline="\n")
Expand Down
34 changes: 34 additions & 0 deletions skill-runtime/skill-package-contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,40 @@
"hostPythonMinimum": "3.8",
"kind": "iac-code-skill-package-contract",
"packageScript": "skill-runtime/package_skill.py",
"profileScript": "skill-runtime/skill_profiles.py",
"profiles": {
"alibabacloud-iac-code": {
"archiveRoot": "alibabacloud-iac-code",
"files": [
"SKILL.md",
"references/ram-policies.md",
"scripts/iac_code.py"
],
"requiresRuntime": true,
"sourceDirectory": "skills/iac-code"
},
"alibabacloud-ros-agent": {
"archiveRoot": "alibabacloud-ros-agent",
"files": [
"SKILL.md",
"references/ram-policies.md",
"scripts/requirements.txt",
"scripts/ros_agent.py"
],
"requiresRuntime": false,
"sourceDirectory": "skills/alicloud-ros-agent"
},
"iac-code": {
"archiveRoot": "iac-code",
"files": [
"SKILL.md",
"agents/openai.yaml",
"scripts/iac_code.py"
],
"requiresRuntime": true,
"sourceDirectory": "skills/iac-code"
}
},
"runtimePython": "cp312",
"schemaVersion": 1,
"sourceDirectory": "skills/iac-code"
Expand Down
Loading
Loading