diff --git a/.changes/unreleased/Features-20230509-094147.yaml b/.changes/unreleased/Features-20230509-094147.yaml new file mode 100644 index 00000000000..21cd583e46c --- /dev/null +++ b/.changes/unreleased/Features-20230509-094147.yaml @@ -0,0 +1,6 @@ +kind: Features +body: Check for project dependency cycles +time: 2023-05-09T09:41:47.2-04:00 +custom: + Author: gshank + Issue: "7468" diff --git a/core/dbt/exceptions.py b/core/dbt/exceptions.py index e92948d9ad0..2471dce5442 100644 --- a/core/dbt/exceptions.py +++ b/core/dbt/exceptions.py @@ -871,6 +871,19 @@ def get_message(self) -> str: return msg +class ProjectDependencyCycleError(ParsingError): + def __init__(self, pub_project_name, project_name): + self.pub_project_name = pub_project_name + self.project_name = project_name + super().__init__(msg=self.get_message()) + + def get_message(self) -> str: + return ( + f"A project dependency cycle has been detected. The current project {self.project_name} " + f"depends on {self.pub_project_name} which also depends on the current project." + ) + + class MacroArgTypeError(CompilationError): def __init__(self, method_name: str, arg_name: str, got_value: Any, expected_type): self.method_name = method_name diff --git a/core/dbt/parser/manifest.py b/core/dbt/parser/manifest.py index a0b1f34de37..fdce9d7d553 100644 --- a/core/dbt/parser/manifest.py +++ b/core/dbt/parser/manifest.py @@ -103,7 +103,12 @@ PublicModel, ProjectDependencies, ) -from dbt.exceptions import TargetNotFoundError, AmbiguousAliasError, PublicationConfigNotFound +from dbt.exceptions import ( + TargetNotFoundError, + AmbiguousAliasError, + PublicationConfigNotFound, + ProjectDependencyCycleError, +) from dbt.parser.base import Parser from dbt.parser.analysis import AnalysisParser from dbt.parser.generic_test import GenericTestParser @@ -1712,6 +1717,10 @@ def write_publication_artifact(root_project: RuntimeConfig, manifest: Manifest): # Get dependencies from publication dependencies for pub_project in manifest.publications.values(): for project_name in pub_project.dependencies: + if project_name == root_project.project_name: + raise ProjectDependencyCycleError( + pub_project_name=pub_project.project_name, project_name=project_name + ) if project_name not in dependencies: dependencies.append(project_name) diff --git a/tests/functional/multi_project/test_publication.py b/tests/functional/multi_project/test_publication.py index 2725cb4c8bc..c85efe9f4d2 100644 --- a/tests/functional/multi_project/test_publication.py +++ b/tests/functional/multi_project/test_publication.py @@ -4,7 +4,11 @@ from dbt.tests.util import run_dbt, get_artifact, write_file, copy_file from dbt.contracts.publication import PublicationArtifact, PublicModel -from dbt.exceptions import PublicationConfigNotFound, TargetNotFoundError +from dbt.exceptions import ( + PublicationConfigNotFound, + TargetNotFoundError, + ProjectDependencyCycleError, +) model_one_sql = """ @@ -257,3 +261,21 @@ def test_multi_projects(self, project, project_alt): write_file(dependencies_alt_yml, project.project_root, "dependencies.yml") results = run_dbt(["run", "--project-dir", str(project.project_root)]) assert len(results) == 4 + + +class TestProjectCycles: + @pytest.fixture(scope="class") + def models(self): + return { + "model_one.sql": model_one_sql, + } + + def test_project_cycles(self, project): + write_file(dependencies_yml, "dependencies.yml") + # Create a project dependency that's the same as the current project + m_pub_json = marketing_pub_json.replace('"dependencies": []', '"dependencies": ["test"]') + (pathlib.Path(project.project_root) / "publications").mkdir(parents=True, exist_ok=True) + write_file(m_pub_json, project.project_root, "publications", "marketing_publication.json") + + with pytest.raises(ProjectDependencyCycleError): + run_dbt(["parse"])