Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
98 changes: 9 additions & 89 deletions .rapidkit/release/promote-manifest.json
Original file line number Diff line number Diff line change
@@ -1,103 +1,23 @@
{
"excluded_commits": [
{
"reason": "protected workflow-only commit",
"sha": "968ac3613a89e9c9d94cde9789c989e56f21979d",
"subject": "fix: validate staging promote token before tagging"
},
{
"reason": "protected workflow-only commit",
"sha": "54d03752f2a08fab756099f91ff68dee3e56cf16",
"subject": "fix(ci): pass SOURCE_REF env to curated promote step"
},
{
"reason": "protected workflow-only commit",
"sha": "8b7c59fa2477b9dc56900116d806b9c22b3ea247",
"subject": "feat(ci): optional publish input for RC tag; pass SOURCE_REF"
},
{
"reason": "protected workflow-only commit",
"sha": "a9f3b6e7dfb6ee744e4345aa2a5fd41509e6ed20",
"subject": "fix(release): exclude protected paths at patch generation"
},
{
"reason": "protected workflow-only commit",
"sha": "7097d0e01290f438e98f23c84168af5da5bc63f1",
"subject": "fix(ci): always use promote tooling from main when promoting a tag"
},
{
"reason": "protected workflow-only commit",
"sha": "0035d1c252482c6058e3a09e8aeb6729011a3669",
"subject": "fix(release): skip engine-only modified/deleted paths in curated promote"
},
{
"reason": "protected workflow-only commit",
"sha": "3c00d4e007b4fdd9b0a6ae9be4a758e71b068d28",
"subject": "fix(release): exclude tier-owned files from promote"
},
{
"reason": "protected workflow-only commit",
"sha": "6b6e6840642ba76787fd23a0629cb9d1c7436565",
"subject": "fix(release): force-add promote manifest past target gitignore"
},
{
"reason": "empty after protected-path restore",
"sha": "fe17f029b51f4b1b97d41b3afcb6e021ee32b9e0",
"subject": "feat(release): redact engine repo and auto-snapshot changelog"
"sha": "306d5b8bccee82f33a988fb34bb5606e538592eb",
"subject": "feat(cli): add module identity util and harden module lifecycle commands"
}
],
"included_commits": [
{
"sha": "1e1613e1c7f29e4ad3c28965afa21106186746cf",
"subject": "fix: enforce module-owned generated paths"
},
{
"sha": "0c4b3ac52ab3c1322aacc9ead1de261d5188e7e3",
"subject": "fix: align module test artifacts with owned paths"
},
{
"sha": "f0db77ae0186a62c9431ec0f74b7be7a4e20173c",
"subject": "fix: curate staging promotion into public core"
},
{
"sha": "8245c11601b35d5274f76f3711a65441fa150b4d",
"subject": "chore: align module docs domain"
},
{
"sha": "bf5b2aad605f380074a22b58353cb4ea4b228531",
"subject": "chore: sync module metadata after docs domain update"
},
{
"sha": "e18b04679f64a3bff79148de76b2642b91493cca",
"subject": "chore: investigate free/ai/rag_pipeline stabilization (logs + tests)"
},
{
"sha": "23ecd477c4062128de6878e4d5fa8c12924b3622",
"subject": "chore: investigate free/ai/rag_pipeline stabilization (logs + tests)"
},
{
"sha": "d8ec650301393c4c04c0c2cae90d94c3a5ed07c5",
"subject": "chore: add generator contract tests and module metadata updates"
},
{
"sha": "16a23751d439bcdaef2409550185b4402d3292d0",
"subject": "chore: add generator contract tests and module metadata updates"
},
{
"sha": "bbe759dc17565ab39fd50be233a3564c5e639bc0",
"subject": "fix(release): persist promote manifest and harden stable tag"
},
{
"sha": "56e6b195fdf7644e982749e9127891d0b07757eb",
"subject": "fix(release): anchor promote chain to staging tags and source.json"
"sha": "83fedbfd4236f8ecf8407541b810dabd7ec04a4f",
"subject": "feat(cli): add module identity util and harden module lifecycle commands"
}
],
"promote_branch": "promote/v0.5.3",
"promoted_at": "2026-06-10T23:35:59.946914Z",
"promote_branch": "promote/v0.5.4",
"promoted_at": "2026-06-11T22:02:02.636849Z",
"schema_version": 1,
"source_base_sha": "2529eeabd3aeff807c2148cc7d60f9f2d94f206c",
"source_head_sha": "fe17f029b51f4b1b97d41b3afcb6e021ee32b9e0",
"source_base_sha": "fe17f029b51f4b1b97d41b3afcb6e021ee32b9e0",
"source_head_sha": "306d5b8bccee82f33a988fb34bb5606e538592eb",
"target_branch": "main",
"target_repo": "https://github.com/rapidkitlabs/rapidkit-core.git",
"version": "0.5.3"
"version": "0.5.4"
}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

<!-- auto:promote-changelog start -->

## [Snapshot 0.5.4] - 2026-06-11

> Auto-generated promote snapshot for this version.

### Promoted source commits

- feat(cli): add module identity util and harden module lifecycle commands (`83fedbfd4236`)

## [Snapshot 0.5.3] - 2026-06-10

> Auto-generated promote snapshot for this version.
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "rapidkit-core"
version = "0.5.3"
version = "0.5.4"
description = "Community-built rapid automation toolkit for makers who ship with heart"
readme = "README.md"
requires-python = ">=3.10,<4.0"
Expand Down Expand Up @@ -73,7 +73,7 @@ Documentation = "https://www.getrapidkit.com/docs"

[tool.poetry]
name = "rapidkit-core"
version = "0.5.3"
version = "0.5.4"
description = "Community-built rapid automation toolkit for makers who ship with heart"
authors = ["Baziar <baziar@live.com>"]
readme = "README.md"
Expand Down
7 changes: 6 additions & 1 deletion src/cli/commands/checkpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from ..ui.printer import print_error, print_info, print_success, print_warning
from ..utils.filesystem import find_project_root
from ..utils.module_identity import module_identity_matches

checkpoint_app = typer.Typer(help="Create rollback checkpoints for a module's files")

Expand Down Expand Up @@ -55,7 +56,11 @@ def checkpoint_module(
manifest = load_manifest_or_none(MODULES_PATH, name)
registry = load_hashes(project_root)
files_meta: Dict[str, Dict[str, Any]] = registry.get("files", {})
targets = {p: meta for p, meta in files_meta.items() if meta.get("module") == name}
targets = {
p: meta
for p, meta in files_meta.items()
if module_identity_matches(meta.get("module"), name)
}

updated = []
skipped = []
Expand Down
7 changes: 2 additions & 5 deletions src/cli/commands/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

from ..ui.printer import console, print_error, print_info, print_success, print_warning
from ..utils.filesystem import find_project_root
from ..utils.module_identity import module_identity_matches
from ..utils.module_structure_cli import (
DEFAULT_MODULES_ROOT,
collect_validation_results,
Expand Down Expand Up @@ -233,14 +234,10 @@ def _discover_free_manifest_paths(modules_root: Path) -> List[Path]:
def _module_matches_filter(recorded: Optional[str], requested: Optional[str]) -> bool:
if not requested:
return True
if not recorded:
return False
requested_norm = requested.strip()
if not requested_norm:
return True
if recorded == requested_norm:
return True
return recorded.endswith(f"/{requested_norm}")
return module_identity_matches(recorded, requested_norm)


def _registry_lookup_by_slug(registry: Any, slug: str) -> Optional[Dict[str, Any]]:
Expand Down
7 changes: 6 additions & 1 deletion src/cli/commands/rollback.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from ..ui.printer import print_error, print_info, print_success, print_warning
from ..utils.filesystem import find_project_root
from ..utils.module_identity import module_identity_matches

rollback_app = typer.Typer(help="Rollback generated files to previous_hash snapshot")

Expand Down Expand Up @@ -182,7 +183,11 @@ def rollback_module(
template_lookup[str(Path(root_path) / rel if root_path else Path(rel))] = tpl
except (OSError, json.JSONDecodeError):
template_lookup = None
targets = [p for p, meta in registry.get("files", {}).items() if meta.get("module") == name]
targets = [
p
for p, meta in registry.get("files", {}).items()
if module_identity_matches(meta.get("module"), name)
]
results = [
_rollback_file(
project_root,
Expand Down
7 changes: 6 additions & 1 deletion src/cli/commands/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from ..ui.printer import print_error, print_info, print_success, print_warning
from ..utils.filesystem import find_project_root
from ..utils.module_identity import module_identity_matches

snapshot_app = typer.Typer(help="Snapshot utilities (backfill missing snapshots)")

Expand Down Expand Up @@ -97,7 +98,11 @@ def snapshot_backfill(
registry = load_hashes(project_root)
files_meta: Dict[str, Dict[str, Any]] = registry.get("files", {})
targets = (
{p: meta for p, meta in files_meta.items() if meta.get("module") == module}
{
p: meta
for p, meta in files_meta.items()
if module_identity_matches(meta.get("module"), module)
}
if module
else files_meta
)
Expand Down
10 changes: 9 additions & 1 deletion src/cli/commands/uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from ..ui.printer import print_error, print_info, print_success, print_warning
from ..utils.filesystem import find_project_root
from ..utils.module_identity import module_identity_matches
from ..utils.registry import remove_from_registry

uninstall_app = typer.Typer(help="Uninstall (remove) generated files of a module")

Expand Down Expand Up @@ -41,7 +43,11 @@ def uninstall_module(
}
# ensure registry points to cleaned mapping (so removals persist)
registry["files"] = files_meta
targets = {p: meta for p, meta in files_meta.items() if meta.get("module") == name}
targets = {
p: meta
for p, meta in files_meta.items()
if module_identity_matches(meta.get("module"), name)
}
removed: List[str] = []
skipped: List[Dict[str, str]] = []
for rel, meta in targets.items():
Expand Down Expand Up @@ -72,6 +78,8 @@ def uninstall_module(

if not dry_run:
save_hashes(project_root, registry)
if removed:
remove_from_registry(name, project_root)

summary = {
"module": name,
Expand Down
26 changes: 26 additions & 0 deletions src/cli/utils/module_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations

from typing import Optional


def module_identity_matches(recorded: Optional[str], requested: Optional[str]) -> bool:
"""Match hash-registry module labels against CLI module slugs.

Hash registries may store short names (``agent_runtime``) while the CLI accepts
fully-qualified slugs (``free/ai/agent_runtime``).
"""

if not recorded or not requested:
return False

rec = str(recorded).strip().strip("/")
req = str(requested).strip().strip("/")
if not rec or not req:
return False

if rec == req:
return True
if rec.endswith(f"/{req}") or req.endswith(f"/{rec}"):
return True

return rec.split("/")[-1] == req.split("/")[-1]
27 changes: 27 additions & 0 deletions src/cli/utils/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any, Dict, List

from ..ui.printer import print_success, print_warning
from .module_identity import module_identity_matches


def _normalise_registry_entries(raw_entries: Any) -> List[Dict[str, Any]]:
Expand Down Expand Up @@ -72,3 +73,29 @@ def update_registry(
print_success(f"🔄 Refreshed registry metadata for module: {module_name}")
except (OSError, PermissionError) as e:
print_warning(f"⚠️ Could not update registry: {e}")


def remove_from_registry(module_name: str, project_root: Path) -> bool:
"""Remove a module entry from registry.json when its slug matches."""

registry_path = project_root / "registry.json"
if not registry_path.exists():
return False

try:
data = json.loads(registry_path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
return False

entries = _normalise_registry_entries(data.get("installed_modules", []))
filtered = [
entry for entry in entries if not module_identity_matches(entry.get("slug"), module_name)
]

if len(filtered) == len(entries):
return False

data["installed_modules"] = filtered
registry_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
print_success(f"✅ Removed module from registry: {module_name}")
return True
2 changes: 1 addition & 1 deletion src/core/config/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from packaging import version as packaging_version

CURRENT_VERSION = "0.5.3"
CURRENT_VERSION = "0.5.4"


def check_min_version(min_version: str, current_version: str) -> None:
Expand Down
20 changes: 20 additions & 0 deletions tests/cli/utils/test_module_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import pytest

from cli.utils.module_identity import module_identity_matches


@pytest.mark.parametrize(
("recorded", "requested", "expected"),
[
("agent_runtime", "free/ai/agent_runtime", True),
("llm_gateway", "free/ai/llm_gateway", True),
("free/ai/agent_runtime", "free/ai/agent_runtime", True),
("settings", "free/essentials/settings", True),
("logging", "free/essentials/logging", True),
("agent_runtime", "free/ai/llm_gateway", False),
(None, "free/ai/agent_runtime", False),
("agent_runtime", "", False),
],
)
def test_module_identity_matches(recorded, requested, expected) -> None:
assert module_identity_matches(recorded, requested) is expected
Loading