From 49723fbf1fc535a064539144541d621c285662af Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah Date: Sat, 20 Jun 2026 10:14:57 -0700 Subject: [PATCH] Make project replacement idempotent --- .ai-context/COMMANDS.md | 3 +- .ai-context/WORKFLOWS.md | 1 + README.md | 4 +- cli/bash/commands/basectl/README.md | 3 +- cli/bash/commands/basectl/subcommands/gh.sh | 1 + cli/python/base_github_projects/engine.py | 3 +- .../test_project_configure_replacement.py | 57 +++++++++++++++---- docs/github-workflow.md | 3 +- docs/repo-baseline.md | 3 +- 9 files changed, 60 insertions(+), 18 deletions(-) diff --git a/.ai-context/COMMANDS.md b/.ai-context/COMMANDS.md index 39a03e6..4336aa9 100644 --- a/.ai-context/COMMANDS.md +++ b/.ai-context/COMMANDS.md @@ -51,7 +51,8 @@ the canonical current command list. fields against the Base Project schema. - `basectl gh project configure --project ` - create or repair the standard Project metadata schema; pass `--replace-project` with `--repo` - to archive and recreate a repo Project whose views are nonstandard. + to archive and recreate a repo Project whose views are nonstandard; Projects + that already have standard Base views are left intact. - `basectl gh project issue set-fields <number>` - add an issue to the Project if needed and update its metadata fields. - `basectl clean` - remove old Base runtime logs, temp files, and cache entries. diff --git a/.ai-context/WORKFLOWS.md b/.ai-context/WORKFLOWS.md index 0cc7eaa..3cb46a2 100644 --- a/.ai-context/WORKFLOWS.md +++ b/.ai-context/WORKFLOWS.md @@ -26,6 +26,7 @@ creates it when missing from older repositories. If a repo Project has GitHub's default `View 1` instead of the standard Base views, use `basectl repo configure --replace-project` with `--repo`; Base archives the old Project and recreates it from `base-project-template`. +Already-standard Projects are left intact and continue through metadata repair. ## Branch And Worktree Flow diff --git a/README.md b/README.md index 8a0c727..acd67ec 100644 --- a/README.md +++ b/README.md @@ -573,7 +573,9 @@ from `base-project-template`. Base renames and closes the old Project as a legacy archive, creates a fresh Project with the original title, links it to the repo, backfills repo issues, and copies missing issue item fields from the legacy Project before applying repo defaults. The repaired Project gets a new -Project number and URL. +Project number and URL. If the existing Project already has the standard Base +views, `--replace-project` leaves it intact and continues normal metadata +repair. Run a discovered project's declared test command with: diff --git a/cli/bash/commands/basectl/README.md b/cli/bash/commands/basectl/README.md index e142513..a00db06 100644 --- a/cli/bash/commands/basectl/README.md +++ b/cli/bash/commands/basectl/README.md @@ -109,7 +109,8 @@ such command directories exist. Optional utility CLIs such as `caff` and remaining blanks in the repo Project. Use `--replace-project` when an existing repo Project has nonstandard views; Base archives the old Project, recreates it from `base-project-template`, backfills repository issues, and - preserves missing item field values where possible. + preserves missing item field values where possible. Already-standard Projects + are left intact. `basectl repo agent-guidance [path]` seeds optional repo-local agent guidance files and `basectl repo check [path] --agent-guidance` verifies that optional layer for repos that opt in. Use `--pr` when generated guidance should land diff --git a/cli/bash/commands/basectl/subcommands/gh.sh b/cli/bash/commands/basectl/subcommands/gh.sh index b527b62..929c0d3 100644 --- a/cli/bash/commands/basectl/subcommands/gh.sh +++ b/cli/bash/commands/basectl/subcommands/gh.sh @@ -110,6 +110,7 @@ Notes: - Project operations delegate to Base's Python Project engine. - Use project issue set-fields to move issue cards through Backlog, In Progress, In Review, and Done. - Use --replace-project to replace a nonstandard repo Project from base-project-template. + Already-standard Projects are left intact. EOF } diff --git a/cli/python/base_github_projects/engine.py b/cli/python/base_github_projects/engine.py index ed14302..b50ad1c 100644 --- a/cli/python/base_github_projects/engine.py +++ b/cli/python/base_github_projects/engine.py @@ -477,7 +477,8 @@ def replacement_plan_for_args( raise ProjectError(f"Project '{args.project_title}' was not found for owner '{owner}'; cannot replace it.") view_errors = standard_template_view_errors(fetch_project_views(project.project_id)) if not view_errors: - raise ProjectError(f"Project '{args.project_title}' already has standard Base views; refusing to replace it.") + print(f"INFO: Project '{args.project_title}' already has standard Base views; skipping replacement.") + return None return ProjectReplacement( legacy_project=project, legacy_title=legacy_project_title(args.project_title or ""), diff --git a/cli/python/base_github_projects/tests/test_project_configure_replacement.py b/cli/python/base_github_projects/tests/test_project_configure_replacement.py index aa288c6..76e74cf 100644 --- a/cli/python/base_github_projects/tests/test_project_configure_replacement.py +++ b/cli/python/base_github_projects/tests/test_project_configure_replacement.py @@ -73,7 +73,13 @@ def test_configure_command_replace_project_requires_repo() -> None: assert str(excinfo.value) == "--replace-project requires --repo." -def test_configure_command_replace_project_refuses_standard_views(monkeypatch: pytest.MonkeyPatch) -> None: +def test_configure_command_replace_project_skips_replacement_for_standard_views( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + linked: list[tuple[str, str]] = [] + backfilled: list[tuple[str, str]] = [] + monkeypatch.setattr( engine, "find_owner_and_project", @@ -88,20 +94,47 @@ def test_configure_command_replace_project_refuses_standard_views(monkeypatch: p "fetch_project_views", lambda project_id: project_model.STANDARD_TEMPLATE_VIEWS, ) + monkeypatch.setattr(engine, "fetch_project_fields", lambda project_id: complete_project_fields()) + monkeypatch.setattr(engine, "create_single_select_field", lambda project_id, spec: None) + monkeypatch.setattr(engine, "update_single_select_field", lambda field, spec: None) + monkeypatch.setattr( + engine, + "link_project_to_repository", + lambda project_id, repo: linked.append((project_id, repo)), + ) + monkeypatch.setattr( + engine, + "backfill_repository_issues", + lambda project_id, repo: backfilled.append((project_id, repo)) or 3, + ) + monkeypatch.setattr( + engine, + "update_project", + lambda project_id, title=None, closed=None: pytest.fail("standard project must not be renamed or closed"), + ) + monkeypatch.setattr( + engine, + "copy_project", + lambda template_project_id, owner_id, title: pytest.fail("standard project must not be replaced"), + ) - with pytest.raises(engine.ProjectError) as excinfo: - engine.configure_command( - engine.ProjectArguments( - area="project", - command="configure", - project_title="base-demo", - owner="codeforester", - repo="codeforester/base-demo", - replace_project=True, - ) + status = engine.configure_command( + engine.ProjectArguments( + area="project", + command="configure", + project_title="base-demo", + owner="codeforester", + repo="codeforester/base-demo", + replace_project=True, ) + ) - assert str(excinfo.value) == "Project 'base-demo' already has standard Base views; refusing to replace it." + assert status == 0 + assert linked == [("project-id", "codeforester/base-demo")] + assert backfilled == [("project-id", "codeforester/base-demo")] + output = capsys.readouterr().out + assert "INFO: Project 'base-demo' already has standard Base views; skipping replacement." in output + assert "Configured GitHub Project base-demo" in output def test_configure_command_replace_project_dry_run_reports_cutover_plan( diff --git a/docs/github-workflow.md b/docs/github-workflow.md index 85b579a..cd9e439 100644 --- a/docs/github-workflow.md +++ b/docs/github-workflow.md @@ -435,7 +435,8 @@ If a repo Project exists with nonstandard views such as GitHub's default archives the old Project by renaming and closing it, creates a fresh Project from `base-project-template`, backfills repository issues, and copies missing issue field values from the legacy Project. The new Project has a different -Project number and URL. +Project number and URL. If the Project already has the standard Base views, +`--replace-project` leaves it intact and continues normal metadata repair. Repo Projects show workflow and prioritization. Milestones show release grouping. Cross-repo portfolio Projects should be curated roll-ups rather than diff --git a/docs/repo-baseline.md b/docs/repo-baseline.md index b043f74..9b0aec8 100644 --- a/docs/repo-baseline.md +++ b/docs/repo-baseline.md @@ -127,7 +127,8 @@ old Project, copies `base-project-template` into a new Project with the original title, links the new Project to the repository, backfills repository issues, copies missing issue field values from the legacy Project, and then applies repo defaults. Replacement changes the Project number and URL, so keep the closed -legacy Project as the audit trail. +legacy Project as the audit trail. Already-standard Projects are left intact +and continue through normal metadata repair. `basectl gh project` is the lower-level direct surface for Project inspection, schema repair, and issue field updates.