Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/+claude-md.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Manage a shared CLAUDE.md section from plugin-template while preserving plugin-specific notes.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<!-- BEGIN plugin-template -->` and
`<!-- END plugin-template -->` 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
Expand Down
12 changes: 12 additions & 0 deletions plugin-template
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
82 changes: 82 additions & 0 deletions templates/github/CLAUDE.md.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<!-- BEGIN plugin-template -->
<!--
This section is managed by plugin_template. Do not edit between the BEGIN/END markers.
Run './plugin-template --github' to update it. Add plugin-specific notes below the END marker.
-->

# 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 `[<version>]` 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.

<!-- END plugin-template -->
75 changes: 75 additions & 0 deletions utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<!-- BEGIN plugin-template -->"
PLUGIN_TEMPLATE_END = "<!-- END plugin-template -->"
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
+ "<!-- Migrated from pre-template CLAUDE.md; trim or rewrite as needed. -->\n\n"
+ existing.lstrip()
)

path.write_text(content.rstrip() + "\n")

if relative_path == "CLAUDE.md":
ensure_agents_md_symlink(plugin_root_path)