diff --git a/CHANGES/+claude-md.feature b/CHANGES/+claude-md.feature new file mode 100644 index 00000000..0d9ac473 --- /dev/null +++ b/CHANGES/+claude-md.feature @@ -0,0 +1 @@ +Manage a shared CLAUDE.md section from plugin-template while preserving plugin-specific notes. diff --git a/README.md b/README.md index 0751a088..f6acb4aa 100644 --- a/README.md +++ b/README.md @@ -163,8 +163,20 @@ The following settings are stored in `template_config.yml`. ci_trigger Value for the `on` clause on workflow/ci.yml (push, pull_request, etc...) ci_env Environment variables to set for the CI build. lint_requirements Boolean (defaults True) to enable upper bound check on requirements.txt + + claude_cli_app_label App label used in generated pulp-cli examples in CLAUDE.md. Defaults to + `plugin_app_label`, or `file` for pulpcore. + + claude_cli_examples Optional list of pulp-cli example lines for CLAUDE.md. When set, replaces + the default content/repository examples. + + claude_bindings_note Optional override for the client-bindings sentence in CLAUDE.md. ``` +`CLAUDE.md` is updated by `--github`. Content between `` and +`` is managed; anything after the END marker (plugin-specific notes) +is preserved. `AGENTS.md` is created as a symlink to `CLAUDE.md` when missing. + # Bootstrap a new Pulp plugin The next step is to bootstrap the plugin. This will create a functional but useless plugin, with diff --git a/plugin-template b/plugin-template index 3dc4b09d..a9c48fcd 100755 --- a/plugin-template +++ b/plugin-template @@ -37,6 +37,9 @@ DEFAULT_SETTINGS = { "ci_base_image": "ghcr.io/pulp/pulp-ci-centos9", "ci_env": {}, "ci_trigger": "{pull_request: {branches: ['*']}}", + "claude_bindings_note": None, + "claude_cli_app_label": None, + "claude_cli_examples": None, "core_import_allowed": [], "deploy_client_to_pypi": True, "deploy_client_to_rubygems": True, @@ -396,6 +399,8 @@ def write_template_section( "is_pulpdocs_member": config["plugin_name"] in utils.get_pulpdocs_members(PULPDOCS_BRANCH), "config": config, **config, + # Computed after **config so None defaults do not clobber derived values. + "claude_cli_app_label": utils.default_claude_cli_app_label(config), } relative_path_set = generate_relative_path_set(section_templates_dir) @@ -430,6 +435,13 @@ def write_template_section( plugin_root_dir, destination, ) + elif destination == "CLAUDE.md": + utils.merge_marked_markdown( + template, + plugin_root_dir, + destination, + template_vars, + ) else: utils.template_to_file( template, diff --git a/templates/github/CLAUDE.md.j2 b/templates/github/CLAUDE.md.j2 new file mode 100644 index 00000000..5c3a333a --- /dev/null +++ b/templates/github/CLAUDE.md.j2 @@ -0,0 +1,82 @@ + + + +# CLAUDE.md + +The role of this file is to describe common mistakes and confusion points that agents might encounter as they work in this project. +If you ever encounter something in the project that surprises you, please alert the developer working with you and indicate that this is the case in the CLAUDE.md file (below the managed section) to help prevent future agents from having the same issue. + +## Interacting with the developer environment + +Use the `pulp-cli` to interact with the Pulp API. Fallback on `httpie/curl` when the CLI doesn't support the endpoint/options needed. + +```bash +pulp --help +pulp --refresh-api status +{% if claude_cli_examples -%} +{% for line in claude_cli_examples -%} +{{ line }} +{% endfor -%} +{% else -%} +pulp {{ claude_cli_app_label }} content list --limit 5 +pulp {{ claude_cli_app_label }} repository create --name foo +pulp -v {{ claude_cli_app_label }} repository sync --name foo --remote foo +{% endif -%} +pulp task show --wait --href prn:core.task:019c8cae-cc5f-7148-a3de-456d0a9f39a1 +pulp show --href /pulp/api/v3/tasks/019c8cae-cc5f-7148-a3de-456d0a9f39a1/ +``` + +Use the `oci-env` cli to interact with the developer's Pulp instance. It has commands for managing state, running tests, and executing commands against a running Pulp. + +```bash +oci-env --help +oci-env compose ps # check status of the Pulp dev container +oci-env compose up/down/restart # start/stop/restart the Pulp dev container +oci-env poll --attempts 10 --wait 10 # wait till Pulp container finishes booting up +oci-env pstart/pstop/prestart # start/stop/restart the services inside the Pulp container +oci-env generate-client --help # create the client bindings needed for the functional tests! +oci-env test --help # run the functional/unit tests +oci-env pulpcore-manager # run any pulpcore or Django commands +``` + +## Running/Writing tests + +Prefer writing functional tests for new changes/bugfixes and only fallback on unit tests when the change is not easily testable through the API. + +{% if claude_bindings_note -%} +{{ claude_bindings_note }} +{%- elif plugin_name == "pulpcore" -%} +pulpcore & pulp-file functional tests require both client bindings to be installed. The bindings must be regenerated for any changes to the API spec. +{%- else -%} +{{ plugin_name | dash }} functional tests require {{ plugin_name | dash }}, pulpcore & pulp-file client bindings to be installed. The bindings must be regenerated for any changes to the API spec. +{%- endif %} + +**Always** use the `oci-env` to run the functional and unit tests. + +## Modifying template_config.yml + +Use the `plugin-template` tool after any changes made to `template_config.yml`. + +```bash +# typically located in the parent directory of pulpcore/plugin +../plugin_template/plugin-template --github +``` + +## Fixing failed backports + +When patchback fails to cherry-pick a PR into an older branch, you need to manually apply the equivalent change. Key things to know: + +- Older branches may use `requirements.txt` for dependencies, while newer branches use `pyproject.toml`. Always check which file the target branch uses before applying changes. +- When creating a PR include `[]` in the PR title (e.g. `[3.12] Raise upperbound for some requirement`). +- Use `git cherry-pick -x`. + +## Contributing + +All docs, code comments, and changelogs are in markdown format. Keep comments and changelogs short and concise. Try to keep changelogs to just one line. + +When preparing to commit and create a PR you **must** follow our [PR checklist](https://pulpproject.org/pulpcore/docs/dev/guides/pull-request-walkthrough/) Important to note is the AI attribution requirement in our commit messages. Also, note that our changelog entries are markdown. + + diff --git a/utils.py b/utils.py index b874ade5..febc212d 100644 --- a/utils.py +++ b/utils.py @@ -249,3 +249,78 @@ def merge_toml(template, plugin_root_path, relative_path, template_vars): if output[-1] != "\n": output = output + "\n" path.write_text(output) + + +PLUGIN_TEMPLATE_BEGIN = "" +PLUGIN_TEMPLATE_END = "" +CLAUDE_PLUGIN_NOTES_HEADING = "## Plugin-specific notes" + + +def default_claude_cli_app_label(config: dict) -> str: + """ + Pick a CLI plugin label for example commands. + + pulpcore's monorepo demos pulp-file, so prefer a non-core app label there. + """ + if config.get("claude_cli_app_label"): + return config["claude_cli_app_label"] + if config["plugin_name"] == "pulpcore": + labels = [p["app_label"] for p in config.get("plugins") or [] if p["app_label"] != "core"] + if "file" in labels: + return "file" + if labels: + return labels[0] + return "file" + return config["plugin_app_label"] + + +def ensure_agents_md_symlink(plugin_root_path: Path) -> None: + """Point AGENTS.md at CLAUDE.md when AGENTS.md is missing.""" + agents = plugin_root_path / "AGENTS.md" + if agents.exists() or agents.is_symlink(): + return + agents.symlink_to("CLAUDE.md") + + +def merge_marked_markdown(template, plugin_root_path, relative_path, template_vars): + """ + Render a markdown template that owns content between plugin-template markers. + + Content outside the BEGIN/END markers (typically plugin-specific notes after END) is + preserved across updates. Existing marker-less files are migrated under a plugin notes + heading so prior customizations are not lost. + """ + managed = template.render(**template_vars).strip() + "\n" + if PLUGIN_TEMPLATE_BEGIN not in managed or PLUGIN_TEMPLATE_END not in managed: + raise ValueError( + f"Template for {relative_path} must include " + f"{PLUGIN_TEMPLATE_BEGIN!r} and {PLUGIN_TEMPLATE_END!r} markers." + ) + + path = plugin_root_path / relative_path + default_trailer = f"\n{CLAUDE_PLUGIN_NOTES_HEADING}\n\n" + + if not path.exists(): + content = managed + default_trailer + else: + existing = path.read_text() + if PLUGIN_TEMPLATE_BEGIN in existing and PLUGIN_TEMPLATE_END in existing: + before, rest = existing.split(PLUGIN_TEMPLATE_BEGIN, 1) + _, after = rest.split(PLUGIN_TEMPLATE_END, 1) + if not after.strip(): + after = default_trailer + elif not after.startswith("\n"): + after = "\n" + after + content = before + managed.rstrip("\n") + after + else: + content = ( + managed + + default_trailer + + "\n\n" + + existing.lstrip() + ) + + path.write_text(content.rstrip() + "\n") + + if relative_path == "CLAUDE.md": + ensure_agents_md_symlink(plugin_root_path)