feat(cli): backfill project_id from every user-invoked project command (#7057)

* feat(cli): backfill project_id from every user-invoked project command

`crewai run` has always backfilled: a project declaring [tool.crewai] without a
project_id gets one minted the first time it runs. No other command did, so a
project driven entirely through `crewai test`, `crewai deploy` or
`crewai traces enable` never acquired an id and every one of its runs stayed
unattributable - which is the denominator problem, not a cosmetic gap.

Adds the same call to train, replay, test, login, deploy create, deploy push,
flow add-crew, enterprise configure and traces enable. Every one is an action the
user explicitly invoked, which is the condition run_crew already relies on, so this
is the existing principle applied evenly rather than a new policy. It is still never
called from the SDK during kickoff, and get_or_create_project_id still refuses to
create the [tool.crewai] table, so an unrelated directory is never rewritten.

`crewai flow kickoff` is deliberately untouched: it delegates to run_crew and
already inherits the backfill. A test pins that so the delegation is not
accidentally duplicated. There is no `crewai evaluate` command - `crewai test` is
that path.

The call is the first statement in each command so a command that later fails still
leaves the project with an id. The tests patch the backfill to raise, which proves
the call happened and guarantees nothing after it runs, so no test touches user
settings, spawns a subprocess or reaches the network. Verified they fail against the
unpatched module: 9 command tests fail, the 2 guard tests still pass.

Tests live under lib/crewai/tests/cli/ because that is the path the required CI job
runs; nothing runs lib/cli/tests/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN

* test(cli): assert the backfill at runtime instead of reading source text

Addresses CodeRabbit and github-code-quality on #7057. The two guard tests
grepped module source for a call string, which asserts on formatting rather than
behavior: a reformat would break them and a real regression could slip past.

They now invoke the commands in an isolated project and assert on observed calls.
The flow-kickoff test patches the two distinct import sites separately and asserts
run_crew's is called exactly once while cli's is not called at all, which is what
makes 'delegates' and 'duplicates' distinguishable at runtime rather than by
reading the file.

Verified both catch what they claim: injecting a duplicate call into flow_run
fails the delegation test, and removing run_crew's own call fails the run test.

This also drops the module-level 'import crewai_cli.cli as cli_module' that mixed
import styles with the existing 'from crewai_cli.cli import crewai', which is the
code-quality finding - the rewrite removes the need for it entirely.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN

* test(cli): make the exact-once assertion observable

Addresses CodeRabbit on #7057, and the finding was correct: with
side_effect=_BackfillReached the mock raised on first use, so call_count == 1 was
guaranteed by the mock rather than by the code. A second backfill call inside the
same run_crew execution could never have been observed.

Both backfill mocks now return normally and execution is stopped at the first call
AFTER the backfill (configured_project_json_crew), so the recorded count is real.

Verified the difference this makes: injecting a duplicate get_or_create_project_id()
INSIDE run_crew now fails both tests, which the previous version could not detect at
all. The flow_run duplicate case is still caught.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN

* test(cli): assert flow kickoff reaches the post-backfill boundary

Addresses CodeRabbit on #7057, and the finding was right: the flow-kickoff test
discarded the runner.invoke() result, so if the path returned or raised after one
backfill call but before configured_project_json_crew, both call-count assertions
would still have passed - for the wrong reason.

test_run_still_backfills already asserted the boundary; this makes the pair
consistent.

Verified it earns its place: injecting an early return after the backfill and
before the boundary now fails both tests, and previously would have failed
neither.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN

* test(cli): pin that the backfill precedes command-specific work

Addresses CodeRabbit on #7057. The finding is valid: the parametrized test proves
the backfill is reached, not that nothing ran before it, so its assertion message
claimed more than the test established.

Fixed in two parts rather than as proposed. The message now states what the test
actually proves, and a new test pins the ordering on login: , whose first action
goes through a module-level name that can be patched without reaching into the
command.

Deliberately not parameterized across all nine commands, which is what the finding
suggested: that would mean naming each command's current first action, and those
change as commands evolve, so the suite would end up tracking their internals
rather than this ordering property. One representative command establishes it, and
placement is visible in the diff for the rest.

Verified it catches the regression: swapping login's first two statements so its
own work runs before the backfill fails the new test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
João Moura
2026-08-20 03:14:09 -03:00
committed by GitHub
parent 7c72d57b73
commit 0c2bcb510c
2 changed files with 227 additions and 0 deletions

View File

@@ -17,6 +17,7 @@ from crewai_cli.user_data import (
from crewai_cli.utils import (
build_env_with_all_tool_credentials,
enable_prompt_line_editing,
get_or_create_project_id,
is_dmn_mode_enabled,
read_toml,
warn_deprecated,
@@ -331,6 +332,11 @@ def train(
filename: str,
) -> None:
"""Train the crew."""
# Backfills a project_id for projects that have [tool.crewai] but no id yet.
# Safe in every command the user explicitly invoked: get_or_create_project_id
# is a no-op without a pyproject.toml and refuses to create [tool.crewai], so it
# never rewrites an unrelated directory. Never called from the SDK during kickoff.
get_or_create_project_id()
if deprecated_n_iterations is not None:
warn_deprecated(kind="flag", old="--n_iterations", new="--n-iterations")
n_iterations = deprecated_n_iterations
@@ -378,6 +384,11 @@ def replay(
task_id: The ID of the task to replay from.
trained_agents_file: Optional trained-agents pickle path.
"""
# Backfills a project_id for projects that have [tool.crewai] but no id yet.
# Safe in every command the user explicitly invoked: get_or_create_project_id
# is a no-op without a pyproject.toml and refuses to create [tool.crewai], so it
# never rewrites an unrelated directory. Never called from the SDK during kickoff.
get_or_create_project_id()
if deprecated_task_id is not None:
warn_deprecated(kind="flag", old="--task_id", new="--task-id")
task_id = deprecated_task_id
@@ -593,6 +604,11 @@ def test(
trained_agents_file: str | None,
) -> None:
"""Test the crew and evaluate the results."""
# Backfills a project_id for projects that have [tool.crewai] but no id yet.
# Safe in every command the user explicitly invoked: get_or_create_project_id
# is a no-op without a pyproject.toml and refuses to create [tool.crewai], so it
# never rewrites an unrelated directory. Never called from the SDK during kickoff.
get_or_create_project_id()
if deprecated_n_iterations is not None:
warn_deprecated(kind="flag", old="--n_iterations", new="--n-iterations")
n_iterations = deprecated_n_iterations
@@ -669,6 +685,11 @@ def update() -> None:
@crewai.command()
def login() -> None:
"""Sign Up/Login to CrewAI AMP."""
# Backfills a project_id for projects that have [tool.crewai] but no id yet.
# Safe in every command the user explicitly invoked: get_or_create_project_id
# is a no-op without a pyproject.toml and refuses to create [tool.crewai], so it
# never rewrites an unrelated directory. Never called from the SDK during kickoff.
get_or_create_project_id()
Settings().clear_user_settings()
AuthenticationCommand().login()
@@ -703,6 +724,11 @@ def deploy() -> None:
)
def deploy_create(yes: bool, skip_validate: bool) -> None:
"""Create a Crew deployment."""
# Backfills a project_id for projects that have [tool.crewai] but no id yet.
# Safe in every command the user explicitly invoked: get_or_create_project_id
# is a no-op without a pyproject.toml and refuses to create [tool.crewai], so it
# never rewrites an unrelated directory. Never called from the SDK during kickoff.
get_or_create_project_id()
deploy_cmd = DeployCommand()
deploy_cmd.create_crew(yes, skip_validate=skip_validate)
@@ -723,6 +749,11 @@ def deploy_list() -> None:
)
def deploy_push(uuid: str | None, skip_validate: bool) -> None:
"""Deploy the Crew."""
# Backfills a project_id for projects that have [tool.crewai] but no id yet.
# Safe in every command the user explicitly invoked: get_or_create_project_id
# is a no-op without a pyproject.toml and refuses to create [tool.crewai], so it
# never rewrites an unrelated directory. Never called from the SDK during kickoff.
get_or_create_project_id()
deploy_cmd = DeployCommand()
deploy_cmd.deploy(uuid=uuid, skip_validate=skip_validate)
@@ -927,6 +958,11 @@ def flow_plot() -> None:
@click.argument("crew_name")
def flow_add_crew(crew_name: str) -> None:
"""Add a crew to an existing flow."""
# Backfills a project_id for projects that have [tool.crewai] but no id yet.
# Safe in every command the user explicitly invoked: get_or_create_project_id
# is a no-op without a pyproject.toml and refuses to create [tool.crewai], so it
# never rewrites an unrelated directory. Never called from the SDK during kickoff.
get_or_create_project_id()
from crewai_cli.add_crew_to_flow import add_crew_to_flow
click.echo(f"Adding crew {crew_name} to the flow")
@@ -1006,6 +1042,11 @@ def enterprise() -> None:
@click.argument("enterprise_url")
def enterprise_configure(enterprise_url: str) -> None:
"""Configure CrewAI AMP OAuth2 settings from the provided Enterprise URL."""
# Backfills a project_id for projects that have [tool.crewai] but no id yet.
# Safe in every command the user explicitly invoked: get_or_create_project_id
# is a no-op without a pyproject.toml and refuses to create [tool.crewai], so it
# never rewrites an unrelated directory. Never called from the SDK during kickoff.
get_or_create_project_id()
from crewai_cli.enterprise.main import EnterpriseConfigureCommand
enterprise_command = EnterpriseConfigureCommand()
@@ -1130,6 +1171,11 @@ def traces() -> None:
@traces.command("enable")
def traces_enable() -> None:
"""Enable trace collection for crew/flow executions."""
# Backfills a project_id for projects that have [tool.crewai] but no id yet.
# Safe in every command the user explicitly invoked: get_or_create_project_id
# is a no-op without a pyproject.toml and refuses to create [tool.crewai], so it
# never rewrites an unrelated directory. Never called from the SDK during kickoff.
get_or_create_project_id()
from rich.console import Console
from rich.panel import Panel