diff --git a/scripts/registryctl-build-output.py b/scripts/registryctl-build-output.py index 1393ced..b150b68 100755 --- a/scripts/registryctl-build-output.py +++ b/scripts/registryctl-build-output.py @@ -56,12 +56,13 @@ def parse_build_output( if not isinstance(output_value, str) or not output_value: raise BuildReportError("the JSON report has no output root") output = Path(output_value) - if not output.is_absolute(): - raise BuildReportError("the JSON report output root is not absolute") - try: project_root = project_directory.resolve(strict=True) - output_root = output.resolve(strict=True) + output_root = ( + output.resolve(strict=True) + if output.is_absolute() + else (project_root / output).resolve(strict=True) + ) output_root.relative_to(project_root) except (FileNotFoundError, RuntimeError, ValueError) as error: raise BuildReportError( diff --git a/scripts/test_registryctl_build_output.py b/scripts/test_registryctl_build_output.py index 54a1834..fb14e67 100644 --- a/scripts/test_registryctl_build_output.py +++ b/scripts/test_registryctl_build_output.py @@ -59,6 +59,16 @@ def test_accepts_the_versioned_project_owned_build_root(self) -> None: self.output.resolve(), ) + def test_accepts_the_portable_project_relative_build_root(self) -> None: + self.assertEqual( + MODULE.parse_build_output( + self.report(output=".registry-stack/build/local"), + project_directory=self.project, + environment="local", + ), + self.output.resolve(), + ) + def test_rejects_the_wrong_environment_without_echoing_report_values(self) -> None: with self.assertRaisesRegex( MODULE.BuildReportError, @@ -84,6 +94,19 @@ def test_rejects_an_output_root_outside_the_project(self) -> None: environment="local", ) + def test_rejects_a_relative_output_root_that_escapes_the_project(self) -> None: + outside = Path(self.temporary.name) / "outside" + outside.mkdir() + with self.assertRaisesRegex( + MODULE.BuildReportError, + "not a real project-owned directory", + ): + MODULE.parse_build_output( + self.report(output="../outside"), + project_directory=self.project, + environment="local", + ) + def test_rejects_an_incomplete_product_closure(self) -> None: (self.output / "private" / "notary" / "config" / "notary.yaml").unlink() with self.assertRaisesRegex(