diff --git a/README.md b/README.md index 3430704..b163d53 100644 --- a/README.md +++ b/README.md @@ -13,44 +13,14 @@ standards, reproducible quality tooling, and selectable source-control hosting. ## Start an Independent Project -Prefer GitHub's **Use this template** action. It creates a repository with an -independent initial commit instead of inheriting the boilerplate history. Fork -only when contributing changes back to this boilerplate. - -After creating the repository, preview the one-time setup command. Choose the -hosting provider and collaboration model independently: - -| Host | Collaboration | Result | -| :--- | :--- | :--- | -| `github` | `collaborative` | GitHub automation and public collaboration baseline | -| `github` | `solo` | GitHub automation without public collaboration files | -| `gitlab` | `collaborative` | GitLab automation and team collaboration baseline | -| `gitlab` | `solo` | GitLab automation without public collaboration files | -| `neutral` | `collaborative` | Provider-neutral team documentation and local tooling | -| `neutral` | `solo` | Minimal provider-neutral project and local tooling | - -Example preview: - -```bash -uv run python scripts/setup_project.py \ - --host github \ - --collaboration collaborative \ - --distribution-name example-service \ - --import-package example_service \ - --display-name "Example Service" \ - --description "Describe the project's value." \ - --author-name "Your Name" \ - --author-email "you@example.com" \ - --project-license mit \ - --repository-url https://github.com/example/example-service \ - --code-owner @example -``` - -Review the plan, then repeat it with `--apply`. See -[Getting Started](docs/getting-started.md) and the complete -[customization guide](docs/project/customization.md) before applying it. -Apply mode requires a clean Git worktree with the boilerplate baseline -committed and stages all transformations before publishing them. +Create an independent repository rather than a fork unless you intend to +contribute changes back to this boilerplate. Then follow the single linear +[Getting Started guide](docs/getting-started.md) from repository creation +through project setup and verification. + +The [customization reference](docs/project/customization.md) explains the +available setup fields and generated behavior. Consult it when choosing values; +the executable sequence remains in Getting Started. ## What the Baseline Provides diff --git a/docs/README.md b/docs/README.md index 7083504..ed5f588 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,9 +1,9 @@ # Documentation -- [Getting Started](getting-started.md) explains repository creation, local - setup, and the first verification. -- [Customizing the Boilerplate](project/customization.md) documents every - automated and manual conversion step. +- [Getting Started](getting-started.md) is the ordered walkthrough for + repository creation, local setup, and the first verification. +- [Customizing the Boilerplate](project/customization.md) is the reference for + setup fields, choices, and generated behavior. - [Development Tooling](development-tooling.md) explains local and hosted quality gates. - [Agent Configuration](project/agent-configuration.md) describes Codex diff --git a/docs/getting-started.md b/docs/getting-started.md index 57f538b..6efb14c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -45,12 +45,16 @@ the boilerplate repository's `.git` directory. Create the repository using the host's template or repository-copy mechanism. Select `neutral` during setup so GitHub and GitLab files are removed. -## Create the Environment +## Create the Initial Environment ```bash uv sync --locked ``` +This environment initially carries the boilerplate prompt. Project setup +cannot safely replace an environment while its Python process is running, so a +later step recreates it with the new distribution name. + ## Preview Project Setup The setup command combines two independent choices: @@ -59,8 +63,31 @@ The setup command combines two independent choices: - `--collaboration solo|collaborative` selects contribution infrastructure. It also requires explicit project identity, authorship, and license treatment. -Run the example in the root README without `--apply`, inspect every reported -action, and then apply the same command explicitly. +The author fields populate Python package metadata and generated project +documents; they do not configure Git identity. + +Preview setup first. This GitLab solo example intentionally omits +`--repository-url`; repository links may be added to the resulting project +metadata after the remote repository exists: + +```bash +uv run python scripts/setup_project.py \ + --host gitlab \ + --collaboration solo \ + --distribution-name example-service \ + --import-package example_service \ + --display-name "Example Service" \ + --description "Processes example events." \ + --author-name "Example Team" \ + --author-email "team@example.com" \ + --project-license mit +``` + +Inspect every reported action, then repeat the same command with `--apply`. +Collaborative GitHub and GitLab projects additionally require +`--repository-url` and `--code-owner`. A repository URL only populates package +metadata and generated links; setup never creates a remote repository or +changes a Git remote. `--project-license mit` keeps the root MIT license as the new project's license. `--project-license unset` removes the project-level license metadata @@ -71,20 +98,60 @@ The command customizes project files, regenerates `uv.lock`, and removes its one-time templates and tests. It never commits, rewrites history, changes a remote, or pushes. -## Complete the Manual Steps +## Recreate the Project Environment + +If the initial environment is active, deactivate it. Then replace it so its +shell prompt uses the new distribution name and synchronize the dependencies: + +```bash +deactivate +uv venv --clear --prompt "example-service" +uv sync --locked +``` + +Omit `deactivate` when no virtual environment is active. `--clear` replaces +the existing `.venv`; do not use it for an environment containing unrecorded +manual changes. + +## Configure Git Identity and Signing + +Set repository-local identity when it should differ from the global Git +configuration: + +```bash +git config --local user.name "Your Name" +git config --local user.email "you@example.com" +``` + +For GPG signing, list available secret keys, copy the long key ID from the +`sec` entry, and configure signing for this repository: + +```bash +gpg --list-secret-keys --keyid-format=long +git config --local user.signingkey YOUR_KEY_ID +git config --local commit.gpgsign true +git config --local tag.gpgsign true +``` + +Verify the effective local configuration without exposing private key +material: + +```bash +git config --local --list +``` + +## Complete Setup and Verify After setup: 1. complete `docs/project/definition.md`; 2. decide only technical questions required for the first useful milestone; -3. configure repository-local Git identity and signing when required; -4. apply the selected host's documented remote settings; -5. add only required CI secrets and integrations; -6. install local hooks; and -7. run the complete verification. +3. apply the selected host's documented remote settings; +4. add only required CI secrets and integrations; +5. install local hooks; and +6. run the complete verification. ```bash -git config --local --list git remote -v uv run pre-commit install --hook-type pre-commit --hook-type pre-push uv lock --check @@ -93,5 +160,5 @@ uv run pre-commit run --all-files --hook-stage pre-push uv build ``` -For all identity fields, manual decisions, provider behavior, and completion -criteria, follow the [customization guide](project/customization.md). +For the meaning and constraints of individual setup fields, consult the +[customization reference](project/customization.md). diff --git a/docs/project/customization.md b/docs/project/customization.md index 3db8c37..fb2b3eb 100644 --- a/docs/project/customization.md +++ b/docs/project/customization.md @@ -1,9 +1,11 @@ # Customizing the Boilerplate -Use this checklist once, immediately after creating a repository from the -boilerplate and before implementing product behavior. The setup command -automates deterministic repository transformations; product, legal, security, -and operational decisions remain explicit human work. +Use this reference while following the linear +[Getting Started guide](../getting-started.md). This document explains setup +choices and generated behavior; it intentionally does not duplicate the +executable walkthrough. The setup command automates deterministic repository +transformations, while product, legal, security, and operational decisions +remain explicit human work. ## 1. Choose Repository Creation and Git History @@ -17,11 +19,10 @@ Prefer a template mechanism over a fork: - a normal clone preserves boilerplate history and is acceptable when that provenance is wanted. -For a fresh GitLab history, use `git archive` to copy the tracked working tree -into a new directory, run `git init -b main` there, and commit that unchanged -baseline before customization. The setup command requires this clean recovery -point. Commit the customized result separately after reviewing it. Do not -delete or rewrite the source repository's history. +For a fresh GitLab history, Getting Started provides the exact export and Git +initialization sequence. Commit the unchanged baseline before customization so +setup has a clean recovery point. Do not delete or rewrite the source +repository's history. The setup command never deletes `.git`, rewrites commits, changes remotes, commits, or pushes. Do not rename inherited commits: either keep their history @@ -56,13 +57,20 @@ Prepare these values: | Import package | `example_service` | `src/`, imports, coverage, and Import Linter | | Display name | `Example Service` | README and package documentation | | Description | `Processes example events.` | Package and repository metadata | -| Author | `Example Team` | Package ownership metadata | -| Author email | `team@example.com` | Package ownership metadata | +| Author | `Example Team` | Package metadata and generated documents | +| Author email | `team@example.com` | Package metadata and generated documents | | Repository URL | `https://host/group/example-service` | Hosted project links | | Code Owner | `@example-team` | Collaborative host ownership | -Hosted profiles require a repository URL. Hosted collaborative profiles also -require a Code Owner. Neutral profiles reject those provider-specific values. +Hosted solo profiles accept an optional repository URL, allowing setup before +the remote repository exists. Hosted collaborative profiles require both a +repository URL and a Code Owner. Neutral profiles reject those +provider-specific values. + +The repository URL only populates package metadata and links in generated +files. It does not create a repository, configure a remote, or contact the +hosting provider. Author fields likewise do not configure Git identity; follow +Getting Started for repository-local identity and signing commands. ## 4. Decide License Treatment @@ -76,26 +84,9 @@ Choose explicitly: The second option does not select a new project license. Apply organizational or legal policy separately, and never delete the inherited third-party notice. -## 5. Preview and Apply - -Preview first: - -```bash -uv run python scripts/setup_project.py \ - --host gitlab \ - --collaboration collaborative \ - --distribution-name example-service \ - --import-package example_service \ - --display-name "Example Service" \ - --description "Processes example events." \ - --author-name "Example Team" \ - --author-email "team@example.com" \ - --project-license unset \ - --repository-url https://gitlab.example.com/group/example-service \ - --code-owner @example-team -``` +## 5. Setup Behavior -Inspect the plan and repeat it with `--apply`. The command: +Getting Started owns the preview-and-apply command sequence. The command: - validates that it is running against a fresh boilerplate; - selects one host and one collaboration profile; @@ -133,15 +124,9 @@ because they are not project decision history. ## 7. Configure Git and the Hosting Platform -Inspect local configuration and remotes: - -```bash -git config --local --list -git remote -v -``` - -Set repository-local identity or signing only when it should differ from the -user's global configuration. Never copy another project's signing key. +Getting Started owns the commands for repository-local identity, GPG signing, +and remote inspection. Author metadata supplied to setup is independent from +Git configuration. Never copy another project's signing key. For GitHub, apply `.github/REPOSITORY_SETTINGS.md`. For GitLab, apply `.gitlab/REPOSITORY_SETTINGS.md`. These blueprints cover protected branches, @@ -167,14 +152,8 @@ Do not add dependencies or infrastructure merely because they are common. ## 9. Verify the Result -```bash -uv lock --check -uv run pre-commit run --all-files -uv run pre-commit run --all-files --hook-stage pre-push -uv build -``` - -Then verify manually: +Run the complete quality-gate sequence in Getting Started, then verify +manually: - no boilerplate identity remains in project-owned metadata; - exactly one provider baseline is present, or none for `neutral`; diff --git a/pyproject.toml b/pyproject.toml index a9064cd..b6ab4de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "codex-python-boilerplate" -version = "1.0.0" +version = "1.0.1" description = "An opinionated Codex-native foundation for Python projects" authors = [{ name = "Emad Helmi", email = "s.emad.helmi@gmail.com" }] readme = "README.md" diff --git a/scripts/setup_project.py b/scripts/setup_project.py index ee6cf26..de56358 100644 --- a/scripts/setup_project.py +++ b/scripts/setup_project.py @@ -181,9 +181,13 @@ def validate_config(config: SetupConfig) -> None: "Neutral projects cannot use provider-specific repository " "or Code Owner values." ) - elif config.repository_url is None: + elif ( + config.collaboration == "collaborative" + and config.repository_url is None + ): raise ProjectSetupError( - "GitHub and GitLab projects require --repository-url." + "Collaborative GitHub and GitLab projects require " + "--repository-url." ) elif config.repository_url is not None: _validate_repository_url(config.host, config.repository_url) @@ -950,6 +954,18 @@ def main(argv: list[str] | None = None) -> int: print(f"- {action}") if not arguments.apply: print("Re-run with --apply to perform these changes.") + else: + print("Next steps:") + print("- deactivate the current virtual environment if it is active") + print( + "- the next command replaces .venv; preserve any unrecorded " + "manual changes first" + ) + print( + "- recreate the local environment with the project prompt: " + f'uv venv --clear --prompt "{config.distribution_name}"' + ) + print("- synchronize it: uv sync --locked") return 0 diff --git a/tests/test_project_setup.py b/tests/test_project_setup.py index 4313de0..9b6c472 100644 --- a/tests/test_project_setup.py +++ b/tests/test_project_setup.py @@ -434,6 +434,27 @@ def test_hosted_collaboration_requires_repository_and_owner( setup_project.validate_config(setup_project.SetupConfig(**base)) +@pytest.mark.parametrize("host", ["github", "gitlab"]) +def test_hosted_solo_allows_missing_repository_url( + setup_project: ModuleType, + template_root: Path, + host: str, +) -> None: + """Allow setup before a solo hosted repository has been created.""" + + config = make_config(setup_project, host=host, collaboration="solo") + config = setup_project.SetupConfig( + **{**config.__dict__, "repository_url": None} + ) + + setup_project.apply_setup(template_root, config, regenerate_lock=False) + + pyproject = (template_root / "pyproject.toml").read_text(encoding="utf-8") + readme = (template_root / "README.md").read_text(encoding="utf-8") + assert "[project.urls]" not in pyproject + assert "Repository:" not in readme + + @pytest.mark.parametrize( ("host", "url", "message"), [ @@ -664,6 +685,49 @@ def test_cli_previews_and_reports_invalid_configuration( assert "project setup failed" in capsys.readouterr().err +def test_cli_apply_reports_virtual_environment_next_steps( + setup_project: ModuleType, + template_root: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Explain how to replace the prompt stored in an existing environment.""" + + monkeypatch.chdir(template_root) + monkeypatch.setattr(setup_project, "_run_uv_lock", lambda _root: None) + arguments = [ + "--host", + "gitlab", + "--collaboration", + "solo", + "--distribution-name", + "example-service", + "--import-package", + "example_service", + "--display-name", + "Example Service", + "--description", + "Processes example events.", + "--author-name", + "Example Team", + "--author-email", + "team@example.com", + "--project-license", + "mit", + "--apply", + ] + + assert setup_project.main(arguments) == 0 + + output = capsys.readouterr().out + assert ( + "deactivate the current virtual environment if it is active" in output + ) + assert "the next command replaces .venv" in output + assert 'uv venv --clear --prompt "example-service"' in output + assert "uv sync --locked" in output + + def test_apply_with_lock_regeneration_uses_runner( setup_project: ModuleType, template_root: Path, diff --git a/uv.lock b/uv.lock index 1e834cb..5c25b18 100644 --- a/uv.lock +++ b/uv.lock @@ -22,7 +22,7 @@ wheels = [ [[package]] name = "codex-python-boilerplate" -version = "1.0.0" +version = "1.0.1" source = { editable = "." } [package.dev-dependencies]