Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-flux-local for 
openSUSE:Factory checked in at 2026-01-13 21:30:41
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-flux-local (Old)
 and      /work/SRC/openSUSE:Factory/.python-flux-local.new.1928 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-flux-local"

Tue Jan 13 21:30:41 2026 rev:16 rq:1326908 version:8.1.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-flux-local/python-flux-local.changes      
2025-11-17 12:25:23.870879494 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-flux-local.new.1928/python-flux-local.changes
    2026-01-13 21:32:36.147116106 +0100
@@ -1,0 +2,35 @@
+Tue Jan 13 06:43:49 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- update to 8.1.0:
+  * Apply cruft updates by @allenporter in #1046
+  * Update kubectl version in Dockerfile by @allenporter in #1047
+  * feat(tests): add new test cases for build hr-new command by
+    @allenporter in #1048
+  * chore(deps): update fluxcd/flux2 action to v2.7.4 by
+    @renovate[bot] in #1051
+  * Fix races in new controller flows by @allenporter in #1052
+  * Add get ks-new and hr-new for the new API by @allenporter in
+    #1053
+  * New updates detected with Cruft by @github-actions[bot] in
+    #1055
+  * Small fixes by @mouchar in #1065
+  * chore: Update snapshots for latest helm releases by
+    @allenporter in #1067
+  * chore(deps): update dependency coverage to v7.13.0 by
+    @renovate[bot] in #1057
+  * chore(deps): update codecov/codecov-action action to v5.5.2 by
+    @renovate[bot] in #1058
+  * chore(deps): update pre-commit hook astral-sh/ruff-pre-commit
+    to v0.14.10 by @renovate[bot] in #1062
+  * chore(deps): update dependency ruff to v0.14.10 - autoclosed by
+    @renovate[bot] in #1061
+  * chore(deps): update registry.k8s.io/kubectl docker tag to
+    v1.35.0 by @renovate[bot] in #1059
+  * chore(deps): update github artifact actions (major) by
+    @renovate[bot] in #1063
+  * chore(deps): update dependency mypy to v1.19.1 by
+    @renovate[bot] in #1064
+  * feat(gha): add skip-invalid-kustomization-paths to github
+    action by @layertwo in #1069
+
+-------------------------------------------------------------------

Old:
----
  flux_local-8.0.1.tar.gz

New:
----
  flux_local-8.1.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-flux-local.spec ++++++
--- /var/tmp/diff_new_pack.M3lCRg/_old  2026-01-13 21:32:37.083155400 +0100
+++ /var/tmp/diff_new_pack.M3lCRg/_new  2026-01-13 21:32:37.087155568 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-flux-local
 #
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -21,7 +21,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-flux-local
-Version:        8.0.1
+Version:        8.1.0
 Release:        0
 Summary:        Set of tools for managing a flux gitops repository
 License:        Apache-2.0

++++++ flux_local-8.0.1.tar.gz -> flux_local-8.1.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/flux_local-8.0.1/PKG-INFO 
new/flux_local-8.1.0/PKG-INFO
--- old/flux_local-8.0.1/PKG-INFO       2025-11-17 06:09:25.379094800 +0100
+++ new/flux_local-8.1.0/PKG-INFO       2025-12-24 21:23:53.666897500 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: flux_local
-Version: 8.0.1
+Version: 8.1.0
 Summary: flux-local is a set of tools and libraries for managing a local flux 
gitops repository focused on validation steps to help improve quality of 
commits, PRs, and general local testing.
 Author-email: Allen Porter <[email protected]>
 License-Expression: Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/helm.py 
new/flux_local-8.1.0/flux_local/helm.py
--- old/flux_local-8.0.1/flux_local/helm.py     2025-11-17 06:09:18.000000000 
+0100
+++ new/flux_local-8.1.0/flux_local/helm.py     2025-12-24 21:23:47.000000000 
+0100
@@ -381,7 +381,9 @@
             if release.values:
                 values_path = self._tmp_dir / 
f"{release.release_name}-values.yaml"
                 async with aiofiles.open(values_path, mode="w") as values_file:
-                    await values_file.write(yaml.dump(release.values, 
sort_keys=False))
+                    await values_file.write(
+                        yaml.dump(release.values, sort_keys=False, 
default_style='"')
+                    )
                 args.extend(["--values", str(values_path)])
             cmd = Kustomize([command.Command(args, exc=HelmException)])
             if options.skip_resources:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/flux_local-8.0.1/flux_local/helm_controller/artifact.py 
new/flux_local-8.1.0/flux_local/helm_controller/artifact.py
--- old/flux_local-8.0.1/flux_local/helm_controller/artifact.py 2025-11-17 
06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/helm_controller/artifact.py 2025-12-24 
21:23:47.000000000 +0100
@@ -8,8 +8,6 @@
 from dataclasses import dataclass
 from typing import Any
 
-import yaml
-
 from flux_local.store.artifact import Artifact
 
 
@@ -27,10 +25,5 @@
     values: dict[str, Any]
     """Resolved values used for rendering the HelmRelease."""
 
-    objects: list[dict[str, Any]]
+    manifests: list[dict[str, Any]]
     """The rendered Kubernetes objects, output of templating the Helm Chart."""
-
-    @property
-    def manifests(self) -> list[str]:
-        """List of rendered Kubernetes manifests as YAML strings."""
-        return [yaml.dump(obj, sort_keys=False) for obj in self.objects]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/flux_local-8.0.1/flux_local/helm_controller/controller.py 
new/flux_local-8.1.0/flux_local/helm_controller/controller.py
--- old/flux_local-8.0.1/flux_local/helm_controller/controller.py       
2025-11-17 06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/helm_controller/controller.py       
2025-12-24 21:23:47.000000000 +0100
@@ -201,7 +201,7 @@
         # Store the result
         artifact = HelmReleaseArtifact(
             chart_name=helm_release.chart.chart_name,
-            objects=objects,
+            manifests=objects,
             values=helm_release.values or {},
         )
         self.store.set_artifact(resource_id, artifact)
@@ -213,6 +213,13 @@
         dependencies = set()
         if helm_release.values_from:
             for ref in helm_release.values_from:
+                if ref.optional:
+                    _LOGGER.debug(
+                        "Skipping optional ValuesFrom reference %s for %s",
+                        ref.name,
+                        helm_release.namespaced_name,
+                    )
+                    continue
                 if ref.kind == CONFIG_MAP_KIND:
                     dependencies.add(
                         NamedResource(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/flux_local-8.0.1/flux_local/orchestrator/orchestrator.py 
new/flux_local-8.1.0/flux_local/orchestrator/orchestrator.py
--- old/flux_local-8.0.1/flux_local/orchestrator/orchestrator.py        
2025-11-17 06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/orchestrator/orchestrator.py        
2025-12-24 21:23:47.000000000 +0100
@@ -227,7 +227,7 @@
             )
         )
 
-    async def bootstrap(self, options: BootstrapOptions) -> bool:
+    async def bootstrap(self, options: BootstrapOptions) -> None:
         """Bootstrap the system by loading resources and starting controllers.
 
         This is a convenience method that loads resources and starts the 
orchestrator.
@@ -253,12 +253,12 @@
                     git_repos.append(resource)
                 elif isinstance(resource, Kustomization):
                     kustomizations.append(resource)
-        except FluxException as e:
-            _LOGGER.error("Failed to load initial resources: %s", e, 
exc_info=True)
-            return False
-        except Exception as e:
-            _LOGGER.error("Failed to load initial resources: %s", e, 
exc_info=True)
-            return False
+        except FluxException as err:
+            raise FluxException(f"Failed to load initial resources: {err}") 
from err
+        except Exception as err:
+            raise FluxException(
+                f"Uncaught exception loading initial resources: {err}"
+            ) from err
 
         # 2. Find the bootstrap GitRepository to associate with the local path
         repo = git_repo.git_repo(options.path)
@@ -294,15 +294,15 @@
         # 3. Start controllers and run
         await self.start()
         try:
-            return await self.run()
+            await self.run()
         finally:
             await self.stop()
 
-    async def run(self) -> bool:
+    async def run(self) -> None:
         """Run the orchestrator until all work is complete.
 
-        Returns:
-            bool: True if all work completed successfully, False if any 
resources failed.
+        Raises:
+            FluxException: If the orchestrator fails or is cancelled.
         """
         try:
             await self.start()
@@ -310,24 +310,20 @@
             # Wait for completion or error
             while True:
                 if self.has_failed_resources():
-                    _LOGGER.error("Resource failures detected, stopping")
-                    return False
+                    raise FluxException("Resource failures detected")
 
                 if self.is_complete():
                     _LOGGER.info("All work completed successfully")
-                    return True
+                    return
 
                 # Allow tasks to run and avoid busy waiting
                 await get_task_service().block_till_done()
                 await asyncio.sleep(0.001)
-
-        except asyncio.CancelledError:
-            _LOGGER.info("Orchestrator was cancelled")
-            return False
-
-        except Exception as e:
-            _LOGGER.exception("Orchestrator failed: %s", e)
-            return False
-
+        except asyncio.CancelledError as err:
+            raise FluxException("Orchestrator was cancelled") from err
+        except FluxException:
+            raise
+        except Exception as err:
+            raise FluxException(f"Uncaught error in orchestrator: {err}") from 
err
         finally:
             await self.stop()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/store/in_memory.py 
new/flux_local-8.1.0/flux_local/store/in_memory.py
--- old/flux_local-8.0.1/flux_local/store/in_memory.py  2025-11-17 
06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/store/in_memory.py  2025-12-24 
21:23:47.000000000 +0100
@@ -61,7 +61,8 @@
                     "Object %s already exists in store, skipping", resource_id
                 )
                 return
-            _LOGGER.debug("Updating existing object %s in store", resource_id)
+            _LOGGER.info("Ignoring changes to existing object %s in store", 
resource_id)
+            return
 
         self._objects[resource_id] = obj
         self._fire_event(StoreEvent.OBJECT_ADDED, resource_id, obj)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/build.py 
new/flux_local-8.1.0/flux_local/tool/build.py
--- old/flux_local-8.0.1/flux_local/tool/build.py       2025-11-17 
06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/build.py       2025-12-24 
21:23:47.000000000 +0100
@@ -17,6 +17,9 @@
 from .build_kustomization import (
     BuildKustomizationAction as BuildKustomizationNewAction,
 )
+from .build_helm import (
+    BuildHelmReleaseAction as BuildHelmReleaseNewAction,
+)
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -26,7 +29,8 @@
 
     @classmethod
     def register(
-        cls, subparsers: SubParsersAction  # type: ignore[type-arg]
+        cls,
+        subparsers: SubParsersAction,  # type: ignore[type-arg]
     ) -> ArgumentParser:
         """Register the subparser commands."""
         args = cast(
@@ -129,7 +133,8 @@
 
     @classmethod
     def register(
-        cls, subparsers: SubParsersAction  # type: ignore[type-arg]
+        cls,
+        subparsers: SubParsersAction,  # type: ignore[type-arg]
     ) -> ArgumentParser:
         """Register the subparser commands."""
         args: ArgumentParser = cast(
@@ -179,7 +184,8 @@
 
     @classmethod
     def register(
-        cls, subparsers: SubParsersAction  # type: ignore[type-arg]
+        cls,
+        subparsers: SubParsersAction,  # type: ignore[type-arg]
     ) -> ArgumentParser:
         """Register the subparser commands."""
         args: ArgumentParser = cast(
@@ -244,7 +250,8 @@
 
     @classmethod
     def register(
-        cls, subparsers: SubParsersAction  # type: ignore[type-arg]
+        cls,
+        subparsers: SubParsersAction,  # type: ignore[type-arg]
     ) -> ArgumentParser:
         """Register the subparser commands."""
         args: ArgumentParser = subparsers.add_parser(
@@ -261,6 +268,7 @@
         BuildKustomizationAction.register(subcmds)
         BuildKustomizationNewAction.register(subcmds)
         BuildHelmReleaseAction.register(subcmds)
+        BuildHelmReleaseNewAction.register(subcmds)
         BuildAllAction.register(subcmds)
         args.set_defaults(cls=cls)
         return args
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/build_common.py 
new/flux_local-8.1.0/flux_local/tool/build_common.py
--- old/flux_local-8.0.1/flux_local/tool/build_common.py        1970-01-01 
01:00:00.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/build_common.py        2025-12-24 
21:23:47.000000000 +0100
@@ -0,0 +1,159 @@
+"""Common build utilities for flux-local."""
+
+import logging
+import pathlib
+from typing import Any, Callable
+import yaml
+
+from flux_local.manifest import (
+    BaseManifest,
+    NamedResource,
+    strip_resource_attributes,
+    STRIP_ATTRIBUTES,
+    HelmRelease,
+    Kustomization,
+)
+from flux_local.helm_controller.artifact import HelmReleaseArtifact
+from flux_local.kustomize_controller.artifact import KustomizationArtifact
+from flux_local.exceptions import FluxException
+from flux_local.orchestrator import BootstrapOptions, Orchestrator, 
OrchestratorConfig
+from flux_local.store import InMemoryStore, Status
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def filter_manifest(doc: dict[str, Any], **kwargs: Any) -> bool:
+    """Return true if the manifest should be included in the output."""
+    if kwargs.get("skip_crds") and doc.get("kind") == 
"CustomResourceDefinition":
+        return False
+    if kwargs.get("skip_secrets") and doc.get("kind") == "Secret":
+        return False
+    if (skip_kinds := kwargs.get("skip_kinds")) and isinstance(skip_kinds, 
list):
+        if doc.get("kind") in skip_kinds:
+            return False
+    return True
+
+
+class BuildRunner:
+    """Common build runner for HelmReleases and Kustomizations."""
+
+    def __init__(
+        self,
+        config: OrchestratorConfig,
+        resource_kind: str,
+        resource_type: type[HelmRelease] | type[Kustomization],
+        artifact_type: type[HelmReleaseArtifact] | type[KustomizationArtifact],
+        selector_predicate: Callable[[BaseManifest], bool],
+    ) -> None:
+        self.config = config
+        self.resource_kind = resource_kind
+        self.resource_type = resource_type
+        self.artifact_type = artifact_type
+        self.selector_predicate = selector_predicate
+
+    def _process_manifest(
+        self, store: InMemoryStore, resource_id: NamedResource, **kwargs: Any
+    ) -> list[dict[str, Any]] | None:
+        """Process a single resource and return its manifests if ready."""
+        status = store.get_status(resource_id)
+        if not status:
+            _LOGGER.warning(
+                "%s %s has no status in the store", self.resource_kind, 
resource_id
+            )
+            return None
+
+        if status.status != Status.READY:
+            _LOGGER.error(
+                "%s %s failed: %s", self.resource_kind, resource_id, 
status.error
+            )
+            return None
+
+        artifact = store.get_artifact(resource_id, self.artifact_type)
+        if not artifact or not artifact.manifests:
+            _LOGGER.warning(
+                "%s %s is Ready but has no artifact or manifests",
+                self.resource_kind,
+                resource_id,
+            )
+            return None
+
+        _LOGGER.info(
+            "Found %d manifests for %s %s",
+            len(artifact.manifests),
+            self.resource_kind,
+            resource_id,
+        )
+        return [
+            manifest_item
+            for manifest_item in artifact.manifests
+            if filter_manifest(manifest_item, **kwargs)
+        ]
+
+    async def run(
+        self,
+        path: pathlib.Path,
+        output_file: str,
+        **kwargs: Any,
+    ) -> None:
+        """Async Action implementation."""
+        _LOGGER.info(
+            "Building %ss from path %s using new orchestrator", 
self.resource_kind, path
+        )
+
+        store = InMemoryStore()
+        orchestrator = Orchestrator(store, self.config)
+        bootstrap_options = BootstrapOptions(path=path)
+        try:
+            await orchestrator.bootstrap(bootstrap_options)
+        except FluxException as err:
+            raise FluxException(
+                f"Orchestrator bootstrap failed for path {path}"
+            ) from err
+
+        manifest_found = False
+        manifest_match = False
+
+        with open(output_file, "w", encoding="utf-8") as file:
+            for manifest_obj in store.list_objects(kind=self.resource_kind):
+                if not isinstance(manifest_obj, self.resource_type):
+                    continue
+                # We can remove these type ignores once we define 
kind/name/namespace
+                # in the BaseManifest.
+                resource_id = NamedResource(
+                    kind=manifest_obj.kind,  # type: ignore[attr-defined]
+                    name=manifest_obj.name,  # type: ignore[attr-defined]
+                    namespace=manifest_obj.namespace,  # type: 
ignore[attr-defined]
+                )
+
+                if not self.selector_predicate(manifest_obj):
+                    _LOGGER.debug(
+                        "%s %s did not match selector", self.resource_kind, 
resource_id
+                    )
+                    continue
+
+                manifest_found = True
+                manifests = self._process_manifest(store, resource_id, 
**kwargs)
+                if not manifests:
+                    continue
+                manifest_match = True
+
+                for manifest_item in manifests:
+                    strip_resource_attributes(
+                        manifest_item,
+                        STRIP_ATTRIBUTES,
+                    )
+                    yaml.dump(manifest_item, file, sort_keys=False, 
explicit_start=True)
+
+            if not manifest_match:
+                if not manifest_found:
+                    _LOGGER.warning(
+                        "No %ss found or processed from path %s that matched 
selector",
+                        self.resource_kind,
+                        path,
+                    )
+                else:
+                    _LOGGER.warning(
+                        "No %ss that matched the selector were successfully 
built from path %s",
+                        self.resource_kind,
+                        path,
+                    )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/build_helm.py 
new/flux_local-8.1.0/flux_local/tool/build_helm.py
--- old/flux_local-8.0.1/flux_local/tool/build_helm.py  1970-01-01 
01:00:00.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/build_helm.py  2025-12-24 
21:23:47.000000000 +0100
@@ -0,0 +1,100 @@
+"""Flux-local build for HelmReleases using the new Orchestrator."""
+
+import logging
+import pathlib
+from argparse import (
+    ArgumentParser,
+    _SubParsersAction as SubParsersAction,
+    BooleanOptionalAction,
+)
+from typing import Any, cast
+
+from flux_local import git_repo
+from flux_local.helm_controller.artifact import HelmReleaseArtifact
+from flux_local.manifest import (
+    HELM_RELEASE,
+    HelmRelease,
+)
+from flux_local.orchestrator import OrchestratorConfig
+
+from . import selector
+from .build_common import BuildRunner
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def build_hr_selector(path: pathlib.Path, **kwargs: Any) -> 
git_repo.ResourceSelector:
+    """Build a HelmRelease selector from CLI arguments."""
+    cli_selector = 
git_repo.ResourceSelector(path=git_repo.PathSelector(path=path))
+    cli_selector.helm_release.name = kwargs.get("helmrelease")
+    cli_selector.helm_release.namespace = kwargs.get("namespace")
+    cli_selector.helm_release.skip_crds = kwargs["skip_crds"]
+    cli_selector.helm_release.skip_secrets = kwargs["skip_secrets"]
+    cli_selector.helm_release.skip_kinds = kwargs.get("skip_kinds")
+    return cli_selector
+
+
+class BuildHelmReleaseAction:
+    """Flux-local build for HelmReleases using the new Orchestrator."""
+
+    @classmethod
+    def register(
+        cls,
+        subparsers: SubParsersAction,  # type: ignore[type-arg]
+    ) -> ArgumentParser:
+        """Register the subparser commands."""
+        args: ArgumentParser = cast(
+            ArgumentParser,
+            subparsers.add_parser(
+                "helmreleases-new",
+                aliases=["hr-new"],
+                help="Build HelmRelease objects using the new experimental 
Orchestrator",
+                description=(
+                    "The build command uses the new orchestrator to build 
HelmRelease objects."
+                ),
+            ),
+        )
+        args.add_argument(
+            "--output-file",
+            type=str,
+            default="/dev/stdout",
+            help="Output file for the results of the command",
+        )
+        args.add_argument(
+            "--wipe-secrets",
+            default=True,
+            action=BooleanOptionalAction,
+            help="Wipe secrets from the output",
+        )
+        args.add_argument(
+            "--enable-oci",
+            default=False,
+            action=BooleanOptionalAction,
+            help="Enable OCI repository sources",
+        )
+        selector.add_hr_selector_flags(args)
+        args.set_defaults(cls=cls)
+        return args
+
+    async def run(
+        self,
+        path: pathlib.Path,
+        output_file: str,
+        **kwargs: Any,
+    ) -> None:
+        """Async Action implementation."""
+        config = OrchestratorConfig(enable_helm=True)
+        config.kustomization_controller_config.wipe_secrets = 
kwargs["wipe_secrets"]
+        config.read_action_config.wipe_secrets = kwargs["wipe_secrets"]
+        config.source_controller_config.enable_oci = kwargs["enable_oci"]
+
+        cli_selector = build_hr_selector(path, **kwargs)
+
+        runner = BuildRunner(
+            config=config,
+            resource_kind=HELM_RELEASE,
+            resource_type=HelmRelease,
+            artifact_type=HelmReleaseArtifact,
+            selector_predicate=cli_selector.helm_release.predicate,  # type: 
ignore[arg-type]
+        )
+        await runner.run(path, output_file, **kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/flux_local-8.0.1/flux_local/tool/build_kustomization.py 
new/flux_local-8.1.0/flux_local/tool/build_kustomization.py
--- old/flux_local-8.0.1/flux_local/tool/build_kustomization.py 2025-11-17 
06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/build_kustomization.py 2025-12-24 
21:23:47.000000000 +0100
@@ -8,21 +8,17 @@
     BooleanOptionalAction,
 )
 from typing import Any, cast
-import yaml
 
 from flux_local import git_repo
 from flux_local.kustomize_controller.artifact import KustomizationArtifact
 from flux_local.manifest import (
     KUSTOMIZE_KIND,
     Kustomization,
-    NamedResource,
-    strip_resource_attributes,
-    STRIP_ATTRIBUTES,
 )
-from flux_local.orchestrator import BootstrapOptions, Orchestrator, 
OrchestratorConfig
-from flux_local.store import InMemoryStore, Status
+from flux_local.orchestrator import OrchestratorConfig
 
 from . import selector
+from .build_common import BuildRunner
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -42,24 +38,13 @@
     return cli_selector
 
 
-def filter_manifest(doc: dict[str, Any], **kwargs: Any) -> bool:
-    """Return true if the manifest should be included in the output."""
-    if kwargs["skip_crds"] and doc.get("kind") == "CustomResourceDefinition":
-        return False
-    if kwargs["skip_secrets"] and doc.get("kind") == "Secret":
-        return False
-    if (skip_kinds := kwargs.get("skip_kinds")) and isinstance(skip_kinds, 
list):
-        if doc.get("kind") in skip_kinds:
-            return False
-    return True
-
-
 class BuildKustomizationAction:
     """Flux-local build for Kustomizations using the new Orchestrator."""
 
     @classmethod
     def register(
-        cls, subparsers: SubParsersAction  # type: ignore[type-arg]
+        cls,
+        subparsers: SubParsersAction,  # type: ignore[type-arg]
     ) -> ArgumentParser:
         """Register the subparser commands."""
         args: ArgumentParser = cast(
@@ -95,38 +80,6 @@
         args.set_defaults(cls=cls)
         return args
 
-    def _process_manifest(
-        self, store: InMemoryStore, resource_id: NamedResource, **kwargs: Any
-    ) -> list[dict[str, Any]] | None:
-        """Process a single Kustomization and return its manifests if ready."""
-        status = store.get_status(resource_id)
-        if not status:
-            _LOGGER.warning("Kustomization %s has no status in the store", 
resource_id)
-            return None
-
-        if status.status != Status.READY:
-            _LOGGER.error("Kustomization %s failed: %s", resource_id, 
status.error)
-            return None
-
-        artifact = store.get_artifact(resource_id, KustomizationArtifact)
-        if not artifact or not artifact.manifests:
-            _LOGGER.warning(
-                "Kustomization %s is Ready but has no artifact or manifests",
-                resource_id,
-            )
-            return None
-
-        _LOGGER.info(
-            "Found %d manifests for Kustomization %s",
-            len(artifact.manifests),
-            resource_id,
-        )
-        return [
-            manifest_item
-            for manifest_item in artifact.manifests
-            if filter_manifest(manifest_item, **kwargs)
-        ]
-
     async def run(
         self,
         path: pathlib.Path,
@@ -134,64 +87,19 @@
         **kwargs: Any,
     ) -> None:
         """Async Action implementation."""
-        _LOGGER.info(
-            "Building Kustomizations from path %s using new orchestrator", path
-        )
-
-        store = InMemoryStore()
         # Disable Helm for ks-only build
         config = OrchestratorConfig(enable_helm=False)
         config.kustomization_controller_config.wipe_secrets = 
kwargs["wipe_secrets"]
         config.read_action_config.wipe_secrets = kwargs["wipe_secrets"]
         config.source_controller_config.enable_oci = kwargs["enable_oci"]
-        orchestrator = Orchestrator(store, config)
-        bootstrap_options = BootstrapOptions(path=path)
-        if not await orchestrator.bootstrap(bootstrap_options):
-            _LOGGER.error("Orchestrator bootstrap failed for path %s", path)
-            return
 
         cli_selector = build_ks_selector(path, **kwargs)
 
-        manifest_found = False
-        manifest_match = False
-        is_match = cli_selector.kustomization.predicate
-        with open(output_file, "w", encoding="utf-8") as file:
-            for manifest_obj in store.list_objects(kind=KUSTOMIZE_KIND):
-                if not isinstance(manifest_obj, Kustomization):
-                    continue
-                resource_id = NamedResource(
-                    kind=manifest_obj.kind,
-                    name=manifest_obj.name,
-                    namespace=manifest_obj.namespace,
-                )
-
-                if not is_match(manifest_obj):
-                    _LOGGER.debug(
-                        "Kustomization %s did not match selector", resource_id
-                    )
-                    continue
-
-                manifest_found = True
-                manifests = self._process_manifest(store, resource_id, 
**kwargs)
-                if not manifests:
-                    continue
-                manifest_match = True
-
-                for manifest_item in manifests:
-                    strip_resource_attributes(
-                        manifest_item,
-                        STRIP_ATTRIBUTES,
-                    )
-                    yaml.dump(manifest_item, file, sort_keys=False, 
explicit_start=True)
-
-            if not manifest_match:
-                if not manifest_found:
-                    _LOGGER.warning(
-                        "No Kustomizations found or processed from path %s 
that matched selector",
-                        path,
-                    )
-                else:
-                    _LOGGER.warning(
-                        "No Kustomizations that matched the selector were 
successfully built from path %s",
-                        path,
-                    )
+        runner = BuildRunner(
+            config=config,
+            resource_kind=KUSTOMIZE_KIND,
+            resource_type=Kustomization,
+            artifact_type=KustomizationArtifact,
+            selector_predicate=cli_selector.kustomization.predicate,  # type: 
ignore[arg-type]
+        )
+        await runner.run(path, output_file, **kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/get.py 
new/flux_local-8.1.0/flux_local/tool/get.py
--- old/flux_local-8.0.1/flux_local/tool/get.py 2025-11-17 06:09:18.000000000 
+0100
+++ new/flux_local-8.1.0/flux_local/tool/get.py 2025-12-24 21:23:47.000000000 
+0100
@@ -22,6 +22,8 @@
     StructFormatter,
 )
 from . import selector
+from .get_kustomization import GetKustomizationNewAction
+from .get_helm import GetHelmReleaseNewAction
 
 
 _LOGGER = logging.getLogger(__name__)
@@ -34,7 +36,8 @@
 
     @classmethod
     def register(
-        cls, subparsers: SubParsersAction  # type: ignore[type-arg]
+        cls,
+        subparsers: SubParsersAction,  # type: ignore[type-arg]
     ) -> ArgumentParser:
         """Register the subparser commands."""
         args = cast(
@@ -101,7 +104,8 @@
 
     @classmethod
     def register(
-        cls, subparsers: SubParsersAction  # type: ignore[type-arg]
+        cls,
+        subparsers: SubParsersAction,  # type: ignore[type-arg]
     ) -> ArgumentParser:
         """Register the subparser commands."""
         args = cast(
@@ -153,7 +157,8 @@
 
     @classmethod
     def register(
-        cls, subparsers: SubParsersAction  # type: ignore[type-arg]
+        cls,
+        subparsers: SubParsersAction,  # type: ignore[type-arg]
     ) -> ArgumentParser:
         """Register the subparser commands."""
         args = cast(
@@ -301,7 +306,8 @@
 
     @classmethod
     def register(
-        cls, subparsers: SubParsersAction  # type: ignore[type-arg]
+        cls,
+        subparsers: SubParsersAction,  # type: ignore[type-arg]
     ) -> ArgumentParser:
         """Register the subparser commands."""
         args = cast(
@@ -317,7 +323,9 @@
             required=True,
         )
         GetKustomizationAction.register(subcmds)
+        GetKustomizationNewAction.register(subcmds)
         GetHelmReleaseAction.register(subcmds)
+        GetHelmReleaseNewAction.register(subcmds)
         GetClusterAction.register(subcmds)
         args.set_defaults(cls=cls)
         return args
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/get_common.py 
new/flux_local-8.1.0/flux_local/tool/get_common.py
--- old/flux_local-8.0.1/flux_local/tool/get_common.py  1970-01-01 
01:00:00.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/get_common.py  2025-12-24 
21:23:47.000000000 +0100
@@ -0,0 +1,41 @@
+"""Common utilities for get commands."""
+
+import logging
+import pathlib
+from argparse import ArgumentParser, BooleanOptionalAction
+
+from flux_local.orchestrator import Orchestrator, OrchestratorConfig, 
BootstrapOptions
+from flux_local.store import InMemoryStore
+from flux_local.exceptions import FluxException
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def add_common_flags(args: ArgumentParser) -> None:
+    """Add common flags to the arguments object."""
+    args.add_argument(
+        "--enable-oci",
+        default=False,
+        action=BooleanOptionalAction,
+        help="Enable OCI repository sources",
+    )
+
+
+async def bootstrap(path: pathlib.Path, enable_oci: bool) -> InMemoryStore:
+    """Bootstrap the orchestrator and return the store."""
+    # We don't need to enable helm controller to just list HelmReleases
+    # found in Kustomizations.
+    config = OrchestratorConfig(enable_helm=False)
+    config.source_controller_config.enable_oci = enable_oci
+
+    store = InMemoryStore()
+    orchestrator = Orchestrator(store, config)
+    bootstrap_options = BootstrapOptions(path=path)
+
+    try:
+        await orchestrator.bootstrap(bootstrap_options)
+    except FluxException:
+        # Continue to show what we found, even if some failed
+        pass
+
+    return store
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/get_helm.py 
new/flux_local-8.1.0/flux_local/tool/get_helm.py
--- old/flux_local-8.0.1/flux_local/tool/get_helm.py    1970-01-01 
01:00:00.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/get_helm.py    2025-12-24 
21:23:47.000000000 +0100
@@ -0,0 +1,109 @@
+"""Flux-local get helmrelease action."""
+
+import logging
+import pathlib
+from argparse import (
+    ArgumentParser,
+    _SubParsersAction as SubParsersAction,
+)
+from typing import Any, cast
+
+from flux_local.kustomize_controller.artifact import KustomizationArtifact
+from flux_local.manifest import (
+    KUSTOMIZE_KIND,
+    Kustomization,
+    HelmRelease,
+    NamedResource,
+)
+
+from .format import PrintFormatter
+from . import selector, get_common
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class GetHelmReleaseNewAction:
+    """Get details about HelmReleases using the new Orchestrator."""
+
+    @classmethod
+    def register(
+        cls,
+        subparsers: SubParsersAction,  # type: ignore[type-arg]
+    ) -> ArgumentParser:
+        """Register the subparser commands."""
+        args = cast(
+            ArgumentParser,
+            subparsers.add_parser(
+                "helmreleases-new",
+                aliases=["hr-new"],
+                help="Get HelmRelease objects using the new experimental 
Orchestrator",
+                description="Print information about local flux HelmRelease 
objects",
+            ),
+        )
+        selector.add_hr_selector_flags(args)
+        get_common.add_common_flags(args)
+        args.set_defaults(cls=cls)
+        return args
+
+    async def run(
+        self,
+        path: pathlib.Path | None,
+        **kwargs: Any,
+    ) -> None:
+        """Async Action implementation."""
+        if path is None:
+            path = pathlib.Path(".")
+
+        store = await get_common.bootstrap(
+            path, enable_oci=kwargs.get("enable_oci", False)
+        )
+
+        cli_selector = selector.build_hr_selector(**kwargs)
+
+        results: list[dict[str, Any]] = []
+        cols = ["name", "revision", "chart", "source"]
+        if cli_selector.helm_release.namespace is None:
+            cols.insert(0, "namespace")
+
+        for manifest_obj in store.list_objects(kind=KUSTOMIZE_KIND):
+            if not isinstance(manifest_obj, Kustomization):
+                continue
+
+            resource_id = NamedResource(
+                kind=manifest_obj.kind,
+                name=manifest_obj.name,
+                namespace=manifest_obj.namespace,
+            )
+
+            artifact = store.get_artifact(resource_id, KustomizationArtifact)
+            if not artifact:
+                continue
+
+            for doc in artifact.manifests:
+                if doc.get("kind") != "HelmRelease":
+                    continue
+
+                try:
+                    helm_release = HelmRelease.parse_doc(doc)
+                except Exception as e:
+                    _LOGGER.debug("Failed to parse HelmRelease: %s", e)
+                    continue
+
+                if not cli_selector.helm_release.predicate(helm_release):
+                    continue
+
+                value = {
+                    "name": helm_release.name,
+                    "namespace": helm_release.namespace,
+                    "revision": str(helm_release.chart.version),
+                    "chart": 
f"{helm_release.namespace}-{helm_release.chart.name}",
+                    "source": helm_release.chart.repo_name,
+                }
+                results.append(value)
+
+        if not results:
+            print(selector.not_found("HelmRelease", cli_selector.helm_release))
+            return
+
+        PrintFormatter(cols).print(results)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/flux_local-8.0.1/flux_local/tool/get_kustomization.py 
new/flux_local-8.1.0/flux_local/tool/get_kustomization.py
--- old/flux_local-8.0.1/flux_local/tool/get_kustomization.py   1970-01-01 
01:00:00.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/get_kustomization.py   2025-12-24 
21:23:47.000000000 +0100
@@ -0,0 +1,123 @@
+"""Flux-local get kustomization action."""
+
+import logging
+import pathlib
+from argparse import (
+    ArgumentParser,
+    _SubParsersAction as SubParsersAction,
+)
+from typing import Any, cast
+from collections import Counter
+
+from flux_local import git_repo
+from flux_local.kustomize_controller.artifact import KustomizationArtifact
+from flux_local.manifest import (
+    KUSTOMIZE_KIND,
+    Kustomization,
+    NamedResource,
+)
+
+from .format import PrintFormatter
+from . import selector, get_common
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class GetKustomizationNewAction:
+    """Get details about kustomizations using the new Orchestrator."""
+
+    @classmethod
+    def register(
+        cls,
+        subparsers: SubParsersAction,  # type: ignore[type-arg]
+    ) -> ArgumentParser:
+        """Register the subparser commands."""
+        args = cast(
+            ArgumentParser,
+            subparsers.add_parser(
+                "kustomizations-new",
+                aliases=["ks-new"],
+                help="Get Kustomization objects using the new experimental 
Orchestrator",
+                description="Print information about local flux Kustomization 
objects",
+            ),
+        )
+        selector.add_ks_selector_flags(args)
+        args.add_argument(
+            "--output",
+            "-o",
+            choices=["wide"],
+            default=None,
+            help="Output format of the command",
+        )
+        get_common.add_common_flags(args)
+        args.set_defaults(cls=cls)
+        return args
+
+    async def run(
+        self,
+        path: pathlib.Path | None,
+        output: str | None,
+        **kwargs: Any,
+    ) -> None:
+        """Async Action implementation."""
+        if path is None:
+            path = pathlib.Path(".")
+
+        store = await get_common.bootstrap(
+            path, enable_oci=kwargs.get("enable_oci", False)
+        )
+
+        # We need to build the selector to filter results
+        # Reusing logic from build_kustomization.py
+        cli_selector = 
git_repo.ResourceSelector(path=git_repo.PathSelector(path=path))
+        cli_selector.kustomization.name = kwargs.get("kustomization")
+        cli_selector.kustomization.namespace = kwargs.get("namespace")
+        if kwargs.get("all_namespaces"):
+            cli_selector.kustomization.namespace = None
+        cli_selector.kustomization.skip_crds = kwargs.get("skip_crds", False)
+        cli_selector.kustomization.skip_secrets = kwargs.get("skip_secrets", 
False)
+        cli_selector.kustomization.skip_kinds = kwargs.get("skip_kinds")
+
+        results: list[dict[str, Any]] = []
+        cols = ["name", "path"]
+        if output == "wide":
+            cols.extend(["helmrepos", "ocirepos", "releases"])
+        if cli_selector.kustomization.namespace is None:
+            cols.insert(0, "namespace")
+
+        for manifest_obj in store.list_objects(kind=KUSTOMIZE_KIND):
+            if not isinstance(manifest_obj, Kustomization):
+                continue
+
+            # Filter
+            if not cli_selector.kustomization.predicate(manifest_obj):
+                continue
+
+            resource_id = NamedResource(
+                kind=manifest_obj.kind,
+                name=manifest_obj.name,
+                namespace=manifest_obj.namespace,
+            )
+
+            value: dict[str, str | int | None] = {
+                "name": manifest_obj.name,
+                "namespace": manifest_obj.namespace,
+                "path": manifest_obj.path,
+            }
+
+            if output == "wide":
+                artifact = store.get_artifact(resource_id, 
KustomizationArtifact)
+                manifests = artifact.manifests if artifact else []
+                counts: Counter[str] = Counter(doc["kind"] for doc in 
manifests)
+                value["helmrepos"] = counts["HelmRepository"]
+                value["ocirepos"] = counts["OCIRepository"]
+                value["releases"] = counts["HelmRelease"]
+
+            results.append(value)
+
+        if not results:
+            print(selector.not_found("Kustomization", 
cli_selector.kustomization))
+            return
+
+        PrintFormatter(cols).print(results)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/test.py 
new/flux_local-8.1.0/flux_local/tool/test.py
--- old/flux_local-8.0.1/flux_local/tool/test.py        2025-11-17 
06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/test.py        2025-12-24 
21:23:47.000000000 +0100
@@ -13,8 +13,6 @@
 from pathlib import Path
 import sys
 from typing import cast, Generator, Any
-
-import nest_asyncio
 import pytest
 
 from flux_local import git_repo, kustomize
@@ -75,7 +73,6 @@
 
     def runtest(self) -> None:
         """Dispatch the async work and run the test."""
-        nest_asyncio.apply()
         asyncio.run(self.async_runtest())
 
     async def async_runtest(self) -> None:
@@ -138,7 +135,6 @@
 
     def runtest(self) -> None:
         """Dispatch the async work and run the test."""
-        nest_asyncio.apply()
         asyncio.run(self.async_runtest())
 
     async def async_runtest(self) -> None:
@@ -278,7 +274,6 @@
         self.init_error: Exception | None = None
 
     def pytest_sessionstart(self, session: pytest.Session) -> None:
-        nest_asyncio.apply()
         asyncio.run(self.async_pytest_sessionstart(session))
 
     async def async_pytest_sessionstart(self, session: pytest.Session) -> None:
@@ -409,7 +404,6 @@
         options = selector.options(**kwargs)
         helm_options = selector.build_helm_options(**kwargs)
 
-        nest_asyncio.apply()
         pytest_args = [
             "--verbosity",
             str(verbosity),
@@ -417,9 +411,10 @@
             "--disable-warnings",
         ]
         _LOGGER.debug("pytest.main: %s", pytest_args)
-        retcode = pytest.main(
+        retcode = await asyncio.to_thread(
+            pytest.main,
             pytest_args,
-            plugins=[
+            [
                 ManifestPlugin(
                     query,
                     TestConfig(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local.egg-info/PKG-INFO 
new/flux_local-8.1.0/flux_local.egg-info/PKG-INFO
--- old/flux_local-8.0.1/flux_local.egg-info/PKG-INFO   2025-11-17 
06:09:25.000000000 +0100
+++ new/flux_local-8.1.0/flux_local.egg-info/PKG-INFO   2025-12-24 
21:23:53.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: flux_local
-Version: 8.0.1
+Version: 8.1.0
 Summary: flux-local is a set of tools and libraries for managing a local flux 
gitops repository focused on validation steps to help improve quality of 
commits, PRs, and general local testing.
 Author-email: Allen Porter <[email protected]>
 License-Expression: Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local.egg-info/SOURCES.txt 
new/flux_local-8.1.0/flux_local.egg-info/SOURCES.txt
--- old/flux_local-8.0.1/flux_local.egg-info/SOURCES.txt        2025-11-17 
06:09:25.000000000 +0100
+++ new/flux_local-8.1.0/flux_local.egg-info/SOURCES.txt        2025-12-24 
21:23:53.000000000 +0100
@@ -47,12 +47,17 @@
 flux_local/task/service.py
 flux_local/tool/__init__.py
 flux_local/tool/build.py
+flux_local/tool/build_common.py
+flux_local/tool/build_helm.py
 flux_local/tool/build_kustomization.py
 flux_local/tool/diagnostics.py
 flux_local/tool/diff.py
 flux_local/tool/flux_local.py
 flux_local/tool/format.py
 flux_local/tool/get.py
+flux_local/tool/get_common.py
+flux_local/tool/get_helm.py
+flux_local/tool/get_kustomization.py
 flux_local/tool/selector.py
 flux_local/tool/test.py
 flux_local/tool/shell/__init__.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/flux_local-8.0.1/pyproject.toml 
new/flux_local-8.1.0/pyproject.toml
--- old/flux_local-8.0.1/pyproject.toml 2025-11-17 06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/pyproject.toml 2025-12-24 21:23:47.000000000 +0100
@@ -4,7 +4,7 @@
 
 [project]
 name = "flux_local"
-version = "8.0.1"
+version = "8.1.0"
 license = "Apache-2.0"
 license-files = ["LICENSE"]
 description = "flux-local is a set of tools and libraries for managing a local 
flux gitops repository focused on validation steps to help improve quality of 
commits, PRs, and general local testing."

Reply via email to