refactor: drop the console announcement when minting project_id

Minting now happens silently. With no message to print, the (id, created)
tuple had no consumer, so simplify the API rather than keep the flag around
for a hypothetical caller:

- get_or_create_project_id() returns `str | None` instead of
  `tuple[str | None, bool]`.
- Remove crewai_cli.utils.ensure_project_id, which existed only to print the
  message and discard the flag. The four call sites (crewai create crew,
  crewai create flow, crewai run, and tool-repository login) now call
  get_or_create_project_id directly.
- Update tests for the simplified signature; still 18 tests covering minting,
  stability, table placement, formatting preservation, five pyproject
  layouts, and missing/malformed/read-only handling.

Behaviour is otherwise unchanged: minting stays restricted to CLI commands
the user invoked, library code still only reads via get_project_id, and a
missing or read-only pyproject.toml still returns None rather than raising.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t
This commit is contained in:
Joao Moura
2026-08-03 10:55:43 -07:00
parent 7f51bb6137
commit 998691ae22
5 changed files with 24 additions and 59 deletions

View File

@@ -20,7 +20,7 @@ from crewai_cli.input_prompt import (
)
from crewai_cli.utils import (
build_env_with_all_tool_credentials,
ensure_project_id,
get_or_create_project_id,
is_dmn_mode_enabled,
)
from crewai_cli.version import get_crewai_tools_dependency, get_crewai_version
@@ -620,7 +620,7 @@ def run_crew(
"""
# Backfills projects created before project_id existed. Only here, in a
# command the user explicitly invoked - never from the SDK during kickoff.
ensure_project_id()
get_or_create_project_id()
# --definition is a pure override: run that flow directly.
if definition is not None:

View File

@@ -16,7 +16,7 @@ from crewai_cli.config import Settings
from crewai_cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
from crewai_cli.utils import (
build_env_with_tool_repository_credentials,
ensure_project_id,
get_or_create_project_id,
get_project_description,
get_project_name,
get_project_version,
@@ -231,7 +231,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
get_user_id = _require_get_user_id()
login_response = self.plus_api_client.login_to_tool_repository(
user_identifier=get_user_id(),
project_id=ensure_project_id(),
project_id=get_or_create_project_id(),
)
if login_response.status_code != 200:

View File

@@ -31,7 +31,6 @@ __all__ = [
"build_env_with_tool_repository_credentials",
"copy_template",
"enable_prompt_line_editing",
"ensure_project_id",
"fetch_and_json_env_file",
"get_or_create_project_id",
"get_project_description",
@@ -53,32 +52,6 @@ console = Console()
_TEMPLATE_TOKEN_RE = re.compile(r"{{([a-zA-Z_][a-zA-Z0-9_]*)}}")
def ensure_project_id(pyproject_path: str | Path = "pyproject.toml") -> str | None:
"""Return the project's id, minting one and telling the user if it was added.
Safe to call from any CLI command: returns None rather than raising when
there is no project, or when ``pyproject.toml`` is not writable.
Args:
pyproject_path: Path to the project's ``pyproject.toml``.
Returns:
The project id, or None if one could not be read or created.
"""
project_id, created = get_or_create_project_id(pyproject_path)
if created:
console.print(
f"Added [bold]project_id[/bold] to {pyproject_path} under "
# Escaped: Rich would otherwise parse [tool.crewai] as a style tag.
r"[bold]\[tool.crewai][/bold] so this project's runs and traces stay "
"linked. Commit it to share that link with your team.",
style="dim",
)
return project_id
def is_dmn_mode_enabled() -> bool:
"""Return True when the enterprise non-interactive mode is enabled."""
value = os.environ.get("CREWAI_DMN")

View File

@@ -252,7 +252,7 @@ def get_project_id(pyproject_path: str | Path = "pyproject.toml") -> str | None:
def get_or_create_project_id(
pyproject_path: str | Path = "pyproject.toml",
) -> tuple[str | None, bool]:
) -> str | None:
"""Return the project's id, minting and persisting one if absent.
Writes ``project_id`` into the ``[tool.crewai]`` table so it is committed
@@ -267,36 +267,34 @@ def get_or_create_project_id(
pyproject_path: Path to the project's ``pyproject.toml``.
Returns:
A ``(project_id, created)`` tuple. ``created`` is True only when an id
was minted and written on this call, so callers can tell the user. Both
values are ``(None, False)`` when the file is missing or not writable -
this is best-effort and never raises.
The project id, or None when ``pyproject.toml`` is missing, malformed,
or not writable. Best-effort - never raises.
"""
existing = get_project_id(pyproject_path)
if existing:
return existing, False
return existing
path = Path(pyproject_path)
if not path.is_file():
return None, False
return None
try:
content = path.read_text(encoding="utf-8")
except OSError:
return None, False
return None
project_id = str(uuid.uuid4())
updated = _insert_project_id(content, project_id)
if updated is None:
return None, False
return None
try:
path.write_text(updated, encoding="utf-8")
except OSError:
# Read-only checkout, permissions, container FS - not worth failing over.
return None, False
return None
return project_id, True
return project_id
def _insert_project_id(content: str, project_id: str) -> str | None:

View File

@@ -38,24 +38,22 @@ def test_returns_none_when_no_id_configured(pyproject):
def test_mints_and_persists_an_id(pyproject):
project_id, created = get_or_create_project_id(pyproject)
project_id = get_or_create_project_id(pyproject)
assert created is True
assert uuid.UUID(project_id)
assert get_project_id(pyproject) == project_id
def test_id_is_stable_across_calls(pyproject):
first, created_first = get_or_create_project_id(pyproject)
second, created_second = get_or_create_project_id(pyproject)
first = get_or_create_project_id(pyproject)
second = get_or_create_project_id(pyproject)
assert created_first is True
assert created_second is False, "must not mint a second id"
assert first == second
assert first == second, "must not mint a second id"
assert uuid.UUID(first)
def test_id_lands_in_the_tool_crewai_table(pyproject):
project_id, _ = get_or_create_project_id(pyproject)
project_id = get_or_create_project_id(pyproject)
data = parse_toml(pyproject.read_text())
assert data["tool"]["crewai"]["project_id"] == project_id
@@ -102,9 +100,9 @@ def test_produces_valid_toml_for_varied_layouts(tmp_path, source, label):
path = tmp_path / "pyproject.toml"
path.write_text(source)
project_id, created = get_or_create_project_id(path)
project_id = get_or_create_project_id(path)
assert created is True, label
assert project_id is not None, label
data = parse_toml(path.read_text())
assert data["tool"]["crewai"]["project_id"] == project_id, label
@@ -124,10 +122,7 @@ def test_id_does_not_leak_into_a_neighbouring_table(tmp_path):
def test_missing_file_is_not_an_error(tmp_path):
project_id, created = get_or_create_project_id(tmp_path / "nope.toml")
assert project_id is None
assert created is False
assert get_or_create_project_id(tmp_path / "nope.toml") is None
def test_malformed_toml_is_not_an_error(tmp_path):
@@ -141,12 +136,11 @@ def test_read_only_file_is_not_an_error(pyproject):
"""A read-only checkout must not break the command that called this."""
pyproject.chmod(0o444)
try:
project_id, created = get_or_create_project_id(pyproject)
project_id = get_or_create_project_id(pyproject)
finally:
pyproject.chmod(0o644)
assert project_id is None
assert created is False
def test_get_project_id_never_creates_anything(pyproject):
@@ -171,7 +165,7 @@ def test_ids_are_unique_across_projects(tmp_path):
path = tmp_path / name / "pyproject.toml"
path.parent.mkdir()
path.write_text(CREW_PYPROJECT)
project_id, _ = get_or_create_project_id(path)
project_id = get_or_create_project_id(path)
ids.add(project_id)
assert len(ids) == 3