|
|
|
@ -9,7 +9,7 @@ import sys
|
|
|
|
import tomllib
|
|
|
|
import tomllib
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from pathlib import Path
|
|
|
|
from pathlib import Path
|
|
|
|
from typing import Any, Sequence
|
|
|
|
from typing import Any, Mapping, Sequence
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_FRAMEWORK_GIT = os.environ.get(
|
|
|
|
DEFAULT_FRAMEWORK_GIT = os.environ.get(
|
|
|
|
"ITICLI_FRAMEWORK_GIT",
|
|
|
|
"ITICLI_FRAMEWORK_GIT",
|
|
|
|
@ -20,10 +20,6 @@ DEFAULT_SYSTEM_GIT = os.environ.get(
|
|
|
|
"https://git.noahlan.cn/iti-framework/iTi-System.git",
|
|
|
|
"https://git.noahlan.cn/iti-framework/iTi-System.git",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
DEFAULT_COPIER_REF = os.environ.get("ITICLI_COPIER_REF", "HEAD")
|
|
|
|
DEFAULT_COPIER_REF = os.environ.get("ITICLI_COPIER_REF", "HEAD")
|
|
|
|
UPDATE_PACKAGES = {
|
|
|
|
|
|
|
|
"framework": ("iti-flask", DEFAULT_FRAMEWORK_GIT),
|
|
|
|
|
|
|
|
"system": ("iti-system", DEFAULT_SYSTEM_GIT),
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ENV_NAMES = {"dev", "test", "prod", "default"}
|
|
|
|
ENV_NAMES = {"dev", "test", "prod", "default"}
|
|
|
|
|
|
|
|
|
|
|
|
@ -251,35 +247,49 @@ def handle_release(args: argparse.Namespace) -> int:
|
|
|
|
def handle_update(args: argparse.Namespace) -> int:
|
|
|
|
def handle_update(args: argparse.Namespace) -> int:
|
|
|
|
if args.target == "self":
|
|
|
|
if args.target == "self":
|
|
|
|
return update_self()
|
|
|
|
return update_self()
|
|
|
|
if args.target == "all":
|
|
|
|
|
|
|
|
ctx = require_project({"business"})
|
|
|
|
ctx = require_project({"business", "system"})
|
|
|
|
code = update_package("framework", args.tag)
|
|
|
|
updates = resolve_update_packages(ctx, args.target, args.tag)
|
|
|
|
|
|
|
|
update_pyproject_git_tags(ctx.root, updates)
|
|
|
|
|
|
|
|
if ctx.kind == "business" and ctx.has_system:
|
|
|
|
|
|
|
|
ensure_uv_no_sources_package(ctx.root, "iti-flask")
|
|
|
|
|
|
|
|
code = run(update_sync_cmd(ctx, updates), ctx.root)
|
|
|
|
if code:
|
|
|
|
if code:
|
|
|
|
return code
|
|
|
|
return code
|
|
|
|
if ctx.has_system:
|
|
|
|
if "iti-system" in updates:
|
|
|
|
return update_package("system", args.tag)
|
|
|
|
return sync_system_migrations(ctx)
|
|
|
|
return 0
|
|
|
|
return 0
|
|
|
|
return update_package(args.target, args.tag)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_package(target: str, tag_arg: str | None) -> int:
|
|
|
|
def resolve_update_packages(ctx: ProjectContext, target: str, tag_arg: str | None) -> dict[str, str]:
|
|
|
|
package, git_url = UPDATE_PACKAGES[target]
|
|
|
|
if ctx.kind == "system":
|
|
|
|
ctx = require_project({"business", "system"})
|
|
|
|
if target not in {"framework", "all"}:
|
|
|
|
if package == "iti-system":
|
|
|
|
|
|
|
|
if ctx.kind != "business":
|
|
|
|
|
|
|
|
raise CliError("system package can only be updated from a business project")
|
|
|
|
raise CliError("system package can only be updated from a business project")
|
|
|
|
if not ctx.has_system:
|
|
|
|
return {"iti-flask": normalize_git_tag(tag_arg) if tag_arg else latest_git_tag(DEFAULT_FRAMEWORK_GIT)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if target == "system" and not ctx.has_system:
|
|
|
|
raise CliError("current business project does not depend on iti-system")
|
|
|
|
raise CliError("current business project does not depend on iti-system")
|
|
|
|
|
|
|
|
|
|
|
|
tag = normalize_git_tag(tag_arg) if tag_arg else latest_git_tag(git_url)
|
|
|
|
if target == "framework":
|
|
|
|
update_pyproject_git_tag(ctx.root, package, tag)
|
|
|
|
return {"iti-flask": normalize_git_tag(tag_arg) if tag_arg else latest_git_tag(DEFAULT_FRAMEWORK_GIT)}
|
|
|
|
|
|
|
|
if target == "system":
|
|
|
|
|
|
|
|
return {"iti-system": normalize_git_tag(tag_arg) if tag_arg else latest_git_tag(DEFAULT_SYSTEM_GIT)}
|
|
|
|
|
|
|
|
if target == "all":
|
|
|
|
|
|
|
|
updates = {"iti-flask": normalize_git_tag(tag_arg) if tag_arg else latest_git_tag(DEFAULT_FRAMEWORK_GIT)}
|
|
|
|
|
|
|
|
if ctx.has_system:
|
|
|
|
|
|
|
|
updates["iti-system"] = normalize_git_tag(tag_arg) if tag_arg else latest_git_tag(DEFAULT_SYSTEM_GIT)
|
|
|
|
|
|
|
|
return updates
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
raise CliError(f"unknown update target: {target}")
|
|
|
|
|
|
|
|
|
|
|
|
code = run(["uv", "sync", "--extra", "dev", "--upgrade-package", package], ctx.root)
|
|
|
|
|
|
|
|
if code:
|
|
|
|
def update_sync_cmd(ctx: ProjectContext, updates: Mapping[str, str]) -> list[str]:
|
|
|
|
return code
|
|
|
|
cmd = ["uv", "sync", "--extra", "dev"]
|
|
|
|
if package == "iti-system":
|
|
|
|
if ctx.kind == "business" and ctx.has_system:
|
|
|
|
return sync_system_migrations(ctx)
|
|
|
|
cmd.extend(["--no-sources-package", "iti-flask"])
|
|
|
|
return 0
|
|
|
|
for package in updates:
|
|
|
|
|
|
|
|
cmd.extend(["--upgrade-package", package])
|
|
|
|
|
|
|
|
return cmd
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def handle_template(args: argparse.Namespace) -> int:
|
|
|
|
def handle_template(args: argparse.Namespace) -> int:
|
|
|
|
@ -640,45 +650,101 @@ def latest_git_tag(git_url: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def latest_git_tag_from_refs(refs: str, git_url: str = "remote") -> str:
|
|
|
|
def latest_git_tag_from_refs(refs: str, git_url: str = "remote") -> str:
|
|
|
|
tags: list[tuple[tuple[int, int, int], str]] = []
|
|
|
|
tags = git_tags_from_refs(refs)
|
|
|
|
|
|
|
|
if not tags:
|
|
|
|
|
|
|
|
raise CliError(f"no semver tags found in {git_url}")
|
|
|
|
|
|
|
|
return max(tags, key=parse_version)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def git_tags_from_refs(refs: str) -> list[str]:
|
|
|
|
|
|
|
|
tags: list[str] = []
|
|
|
|
|
|
|
|
seen: set[str] = set()
|
|
|
|
for line in refs.splitlines():
|
|
|
|
for line in refs.splitlines():
|
|
|
|
parts = line.split()
|
|
|
|
parts = line.split()
|
|
|
|
if len(parts) != 2 or parts[1].endswith("^{}"):
|
|
|
|
if len(parts) != 2 or parts[1].endswith("^{}"):
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
tag = parts[1].rsplit("/", 1)[-1]
|
|
|
|
tag = parts[1].rsplit("/", 1)[-1]
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
tags.append((parse_version(tag), tag))
|
|
|
|
parse_version(tag)
|
|
|
|
except CliError:
|
|
|
|
except CliError:
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
if not tags:
|
|
|
|
if tag not in seen:
|
|
|
|
raise CliError(f"no semver tags found in {git_url}")
|
|
|
|
tags.append(tag)
|
|
|
|
return max(tags, key=lambda item: item[0])[1]
|
|
|
|
seen.add(tag)
|
|
|
|
|
|
|
|
return tags
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def normalize_git_tag(raw: str) -> str:
|
|
|
|
def normalize_git_tag(raw: str) -> str:
|
|
|
|
return f"v{normalize_version(raw)}"
|
|
|
|
return f"v{normalize_version(raw)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_pyproject_git_tag(root: Path, package: str, tag: str) -> None:
|
|
|
|
def update_pyproject_git_tags(root: Path, updates: Mapping[str, str]) -> None:
|
|
|
|
path = root / "pyproject.toml"
|
|
|
|
path = root / "pyproject.toml"
|
|
|
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
|
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
|
|
updated = False
|
|
|
|
updated = {package: False for package in updates}
|
|
|
|
|
|
|
|
|
|
|
|
for index, line in enumerate(lines):
|
|
|
|
for index, line in enumerate(lines):
|
|
|
|
|
|
|
|
for package, tag in updates.items():
|
|
|
|
dependency_line = rewrite_dependency_git_line(line, package, tag)
|
|
|
|
dependency_line = rewrite_dependency_git_line(line, package, tag)
|
|
|
|
if dependency_line is not None:
|
|
|
|
if dependency_line is not None:
|
|
|
|
lines[index] = dependency_line
|
|
|
|
lines[index] = dependency_line
|
|
|
|
updated = True
|
|
|
|
updated[package] = True
|
|
|
|
continue
|
|
|
|
break
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
for package, tag in updates.items():
|
|
|
|
source_line = rewrite_uv_source_git_line(line, package, tag)
|
|
|
|
source_line = rewrite_uv_source_git_line(line, package, tag)
|
|
|
|
if source_line is not None:
|
|
|
|
if source_line is not None:
|
|
|
|
lines[index] = source_line
|
|
|
|
lines[index] = source_line
|
|
|
|
updated = True
|
|
|
|
updated[package] = True
|
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
missing = [package for package, is_updated in updated.items() if not is_updated]
|
|
|
|
|
|
|
|
if missing:
|
|
|
|
|
|
|
|
raise CliError(f"{', '.join(missing)} git dependency not found in {path}")
|
|
|
|
|
|
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
if not updated:
|
|
|
|
|
|
|
|
raise CliError(f"{package} git dependency not found in {path}")
|
|
|
|
def ensure_uv_no_sources_package(root: Path, package: str) -> None:
|
|
|
|
|
|
|
|
path = root / "pyproject.toml"
|
|
|
|
|
|
|
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
|
|
|
|
|
|
section_start = find_toml_section(lines, "[tool.uv]")
|
|
|
|
|
|
|
|
if section_start is None:
|
|
|
|
|
|
|
|
source_start = find_toml_section(lines, "[tool.uv.sources]")
|
|
|
|
|
|
|
|
insert_at = source_start if source_start is not None else len(lines)
|
|
|
|
|
|
|
|
block = ["[tool.uv]", f'no-sources-package = ["{package}"]', ""]
|
|
|
|
|
|
|
|
lines[insert_at:insert_at] = block
|
|
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
section_end = next_toml_section(lines, section_start + 1)
|
|
|
|
|
|
|
|
for index in range(section_start + 1, section_end):
|
|
|
|
|
|
|
|
line = lines[index].strip()
|
|
|
|
|
|
|
|
if not line.startswith("no-sources-package"):
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
if re.search(rf'"{re.escape(package)}"|\'{re.escape(package)}\'', line):
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
if line.endswith("]"):
|
|
|
|
|
|
|
|
lines[index] = f'{lines[index][:-1]}, "{package}"]'
|
|
|
|
|
|
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
raise CliError("cannot update multi-line tool.uv.no-sources-package")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lines.insert(section_end, f'no-sources-package = ["{package}"]')
|
|
|
|
|
|
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def find_toml_section(lines: Sequence[str], section: str) -> int | None:
|
|
|
|
|
|
|
|
for index, line in enumerate(lines):
|
|
|
|
|
|
|
|
if line.strip() == section:
|
|
|
|
|
|
|
|
return index
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def next_toml_section(lines: Sequence[str], start: int) -> int:
|
|
|
|
|
|
|
|
for index in range(start, len(lines)):
|
|
|
|
|
|
|
|
if re.match(r"\s*\[.+]\s*$", lines[index]):
|
|
|
|
|
|
|
|
return index
|
|
|
|
|
|
|
|
return len(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rewrite_dependency_git_line(line: str, package: str, tag: str) -> str | None:
|
|
|
|
def rewrite_dependency_git_line(line: str, package: str, tag: str) -> str | None:
|
|
|
|
|