Skip to content
Merged
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
9 changes: 5 additions & 4 deletions .claude/skills/bootstrap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ python .claude/skills/bootstrap/scripts/bootstrap.py \

Pass `--clean` because the user opted to strip the template-only scaffolding (the README "Use this template / Next steps" block; the bootstrap skill itself is removed separately in Step 6). Omit it only if the user changed their mind and wants the template block kept.

The dry run prints the package-directory rename and the list of files that would be edited. Present that summary and **get explicit confirmation** before the real run. Only pass `--dist` if the user wants a distribution name that isn't just the package with dashes.
The dry run prints the package-directory rename, the list of files that would be edited and the template's own maintenance files that would be removed. Present that summary and **get explicit confirmation** before the real run. Only pass `--dist` if the user wants a distribution name that isn't just the package with dashes.

## Step 4 — Run the replacement

Expand All @@ -75,8 +75,9 @@ Re-run the exact same command **without** `--dry-run`. The script:
- fills in description, and (if given) author, repo URL
- applies the license choice in all three places: the `LICENSE` body, `license = "..."` in `pyproject.toml`, and the README license line
- strips the README template block
- removes the template's own maintenance, listed in the script's `MAINTAINER_ONLY_PATHS`: the CLA assistant (`cla.yml`), the branch-flow guard (`guard-branches.yml`), the release checks (`version-check.yml`, `changelog-check.yml`), the GitHub Release job (`github-release.yml`) and the `release` skill. They encode Pipelex's contributor agreement and release discipline, not the user's; each of their jobs is guarded to `Pipelex/pipelex-starter-python`, so they were already inert in the new repository, and removing them keeps them out of it altogether. `lint-check.yml`, `tests-check.yml` and `package-check.yml` stay: they are the project's own CI. The removal is a plain delete, unstaged like the edits, and it happens after the survivor check, so an aborted run has removed nothing.

It deliberately does **not** run the lock file, run the checks, commit, or touch `.github/`, `.venv/`, `uv.lock`, or the existing `release` skill.
It deliberately does **not** run the lock file, run the checks, commit, or touch `.venv/`, `uv.lock` or the workflows it keeps.

**Heads-up — file state changed on disk.** The script rewrites `pyproject.toml`, `README.md`, and `LICENSE` (and `--clean` shifts README line numbers). If you find you need a manual `Edit` afterward, **re-read the file first** and re-derive any line numbers — a pre-run `grep` result is stale, and an `Edit` against an unread/old version will fail with "modified since read." In practice the script is meant to cover every placeholder so manual edits shouldn't be needed; if you reach for one, it's worth checking whether the script should handle that case instead.

Expand Down Expand Up @@ -109,7 +110,7 @@ Use a plain `rm` (not `git rm`) so the deletion stays unstaged, like the other c

Finally, give the user a short summary:
- the three name forms that were applied, and the license that was set
- that the package directory was renamed
- that the package directory was renamed, and that the template's own CLA, branch-flow and release workflows and its `release` skill were removed
- that `uv.lock` was regenerated and `make agent-check` / `make agent-test` pass
- that **nothing is committed**; the renames are staged (`R`) and the content edits are unstaged (`M`) — they should review with `git status` and `git diff`, then commit (a single `git add -A && git commit` captures everything)
- a nudge to skim the new `README.md` and write real project content, and to update `CLAUDE.md` if the project's specifics have changed
Expand All @@ -120,5 +121,5 @@ Finally, give the user a short summary:
- **Always dry-run before the real run** and get confirmation. This edits a brand-new repo and renames a directory; the preview is cheap insurance.
- **Regenerate `uv.lock`.** Renaming the distribution name makes the lock stale; `make li` (or `uv lock`) is what keeps `package-check.yml` green. Don't skip it.
- **Don't stop on a red check.** A failing `make agent-check` / `make agent-test` here means CI will fail too — fix the root cause and re-run.
- **Don't touch the `release` skill or `.github/` workflows** — they're generic to the template and not placeholders.
- **Don't edit the workflows the script keeps** (`lint-check.yml`, `tests-check.yml`, `package-check.yml`) — they're the project's own CI and hold no placeholder. Everything else under `.github/workflows/`, and the `release` skill, is the template's own and the script removes it.
- If any step fails or the user wants to abort, stop immediately and leave the tree in a state they can inspect — don't push forward through errors.
52 changes: 48 additions & 4 deletions .claude/skills/bootstrap/scripts/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
what makes it testable, and it hard-fails if any placeholder token survives the
substitution.

The script only transforms files. It does NOT touch git, run `uv lock`, run the
checks, or remove the bootstrap skill — the SKILL.md orchestrates those so each
step stays reviewable and the script stays a pure, idempotent transform.
Besides transforming files, it deletes the template's own maintenance files
(MAINTAINER_ONLY_PATHS), which no project made from the template has a use for.
It does NOT commit, run `uv lock`, run the checks, or remove the bootstrap skill
— the SKILL.md orchestrates those so each step stays reviewable and the script
stays an idempotent transform.
"""

from __future__ import annotations
Expand All @@ -45,6 +47,23 @@
# it tests. See gather_target_files().
BOOTSTRAP_TEST_FILE = Path("tests") / "unit" / "test_bootstrap_script.py"

# The template's own maintenance, which run() deletes: the CLA assistant, the branch-flow
# guard, the release version and changelog checks, the GitHub Release job, and the `release`
# skill that drives them. They encode Pipelex's contributor agreement and release discipline,
# not a project's. Every job in those workflows is also guarded to the template's own
# repository, so a project made from the template without running the bootstrap inherits them
# inert. The workflows a project does want — lint, tests and the lock check — are not listed.
# tests/unit/test_bootstrap_script.py holds this list against the guards in the workflows on
# disk, so the two cannot drift apart.
MAINTAINER_ONLY_PATHS = (
Path(".github") / "workflows" / "changelog-check.yml",
Path(".github") / "workflows" / "cla.yml",
Path(".github") / "workflows" / "github-release.yml",
Path(".github") / "workflows" / "guard-branches.yml",
Path(".github") / "workflows" / "version-check.yml",
Path(".claude") / "skills" / "release",
)

PACKAGE_RE = re.compile(r"^[a-z][a-z0-9_]*$")
# PEP 503/508 distribution name: alphanumerics separated by single '.', '-' or
# '_', starting and ending with an alphanumeric. uv/packaging reject anything
Expand Down Expand Up @@ -446,6 +465,22 @@ def move(root: Path, src: Path, dst: Path, opts: "Options") -> None:
print(f" renamed {rel_src} -> {rel_dst}")


def remove(root: Path, rel: Path, opts: "Options") -> bool:
"""Delete a file or directory with a plain delete, so the deletion stays unstaged like the edits."""
path = root / rel
if not path.exists():
return False
if opts.dry_run:
print(f" remove {rel}")
return True
if path.is_dir():
shutil.rmtree(path)
else:
path.unlink()
print(f" removed {rel}")
return True


# --------------------------------------------------------------------------- #
# Orchestration
# --------------------------------------------------------------------------- #
Expand Down Expand Up @@ -624,7 +659,16 @@ def run(root: Path, names: Names, opts: Options) -> None:
if changed == 0:
print(" (no content changes)")
print()
print(f"Done. {changed} file(s) {'would be ' if opts.dry_run else ''}edited.")

# 3. The template's own maintenance files. After the edits, so a run the survivor
# check aborts has deleted nothing; and idempotent, since a re-run finds them gone.
print("Removals:")
removed = sum(remove(root, rel, opts) for rel in MAINTAINER_ONLY_PATHS)
if removed == 0:
print(" (nothing to remove)")
print()
would_be = "would be " if opts.dry_run else ""
print(f"Done. {changed} file(s) {would_be}edited, {removed} path(s) {would_be}removed.")
if not opts.dry_run:
print("\nNext: regenerate the lock file (uv.lock pins the project name) and run the checks:")
print(" make li && make agent-check && make agent-test")
Expand Down
8 changes: 5 additions & 3 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ description: >

# Releasing pipelex-starter-python

The procedure is the workspace release play, [`docs/releasing.md`](../../../../docs/releasing.md) at the workspace root — read it first, then run it with what follows. The repo key is `pipelex-starter-python`, the base is `dev`, and the pull request targets `main`: `guard-branches.yml`'s `gate-main` job refuses any head branch but `release/vX.Y.Z` into `main`, so there is no other way in. The release worktree is `_pipelex-starter-python--release`, made with `wt add pipelex-starter-python release --branch release/vX.Y.Z`. The repo declares neither `.worktree.toml` nor `.worktreeinclude`, so `wt` resolves the base from `origin/dev` and provisions with the Makefile's `install` target, which is what creates the `.venv` every gate below runs out of.
The procedure is the workspace release play, [`docs/workspace/releasing.md`](../../../../docs/workspace/releasing.md) at the workspace root — read it first, then run it with what follows. The repo key is `pipelex-starter-python`, the base is `dev`, and the pull request targets `main`: `guard-branches.yml`'s `gate-main` job refuses any head branch but `release/vX.Y.Z` into `main`, so there is no other way in. The release worktree is `_pipelex-starter-python--release`, made with `wt add pipelex-starter-python release --branch release/vX.Y.Z`. The repo declares neither `.worktree.toml` nor `.worktreeinclude`, so `wt` resolves the base from `origin/dev` and provisions with the Makefile's `install` target, which is what creates the `.venv` every gate below runs out of.

## What ships

Expand All @@ -24,7 +24,7 @@ The procedure is the workspace release play, [`docs/releasing.md`](../../../../d

Two things about that job are worth knowing before the merge:

- **It is unguarded.** Nothing checks whether the Release or the tag already exists, so a push to `main` carrying no version bump fails at `gh release create`, and re-running a run whose Release was already created fails the same way. Once a version has shipped, the way forward is a new version, not a re-run.
- **It is not idempotent.** Nothing checks whether the Release or the tag already exists, so a push to `main` carrying no version bump fails at `gh release create`, and re-running a run whose Release was already created fails the same way. Once a version has shipped, the way forward is a new version, not a re-run.
- **A missing changelog entry does not fail it.** When no `## [vX.Y.Z] - ` heading matches the version, the extraction step prints a warning, sets the notes empty and exits 0, so the Release ships carrying the bare line `Release vX.Y.Z`. `changelog-check.yml` on the pull request is the only thing standing between a forgotten entry and that outcome.

The landing verifies the publish — the run, the Release, the tag:
Expand Down Expand Up @@ -55,7 +55,9 @@ Run in the worktree, in this order, before the commit:

## CI on the release pull request

- `guard-branches.yml` — the `gate-main` job asserts the head branch into `main` matches `^release/v[0-9]+\.[0-9]+\.[0-9]+$` exactly. Its `protect-workflows` job additionally refuses a change under `.github/workflows/` from an author whose association is `CONTRIBUTOR`.
The template's own workflows — `guard-branches.yml`, `version-check.yml`, `changelog-check.yml`, `cla.yml` and the `github-release.yml` above — guard every job with `if: github.repository == 'Pipelex/pipelex-starter-python' && …`, so a repository made with "Use this template" inherits them inert, and the bootstrap deletes them together with this skill (its `MAINTAINER_ONLY_PATHS`). A job added to one of them carries the same guard, and so does every job of a new workflow written for the template's own maintenance, which also joins `MAINTAINER_ONLY_PATHS`: `tests/unit/test_bootstrap_script.py` fails when the guards and the list disagree, but it cannot tell that a new workflow carrying neither is the template's own, so that call is the author's. Here the guard always holds, so it changes nothing about what follows; the required checks they post keep posting.

- `guard-branches.yml` — the `gate-main` job asserts the head branch into `main` matches `^release/v[0-9]+\.[0-9]+\.[0-9]+$` exactly. Its `protect-workflows` job additionally refuses a change under `.github/workflows/` from an author without write access to the repository, reading the author's effective permission and the pull request's own file list from the API.
- `version-check.yml` — the `pyproject.toml` version equals the version in the branch name. That is all it compares: it never checks the number against what `main` already carries, so a re-used or lower version passes here and fails after the merge, at `gh release create`. A head that is not a release branch does not slip past either — the `exit 0` in its first step ends that step alone, and the comparison that follows then fails on an empty branch version.
- `changelog-check.yml` — `CHANGELOG.md` carries a `## [vX.Y.Z] - ` heading for the version in the branch name. It asserts nothing about `[Unreleased]`; leaving none behind is the play's rule, not CI's.
- `lint-check.yml` — `merge-check-ruff-format`, `merge-check-ruff-lint` (which pulls in `check-unused-imports`), `merge-check-pyright` and `merge-check-mypy` on each Python version in the matrix; the aggregator job `Lint (all versions)` is the single required status.
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/changelog-check.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# The template's own maintenance, not a project's: every job runs only in
# Pipelex/pipelex-starter-python, so a repository made with "Use this template" inherits
# this file inert, and /bootstrap deletes it. A workflow added for the template's own
# maintenance carries the same guard on every job and joins MAINTAINER_ONLY_PATHS in the
# bootstrap script; tests/unit/test_bootstrap_script.py holds the two together.
name: Changelog Version Check

on:
Expand All @@ -9,7 +14,7 @@ on:
jobs:
check-changelog:
runs-on: ubuntu-latest
if: startsWith(github.head_ref, 'release/v') # Only run on release branches
if: github.repository == 'Pipelex/pipelex-starter-python' && startsWith(github.head_ref, 'release/v') # Only run on the template's release branches
steps:
- uses: actions/checkout@v4

Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/cla.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# The template's own maintenance, not a project's: every job runs only in
# Pipelex/pipelex-starter-python, so a repository made with "Use this template" inherits
# this file inert, and /bootstrap deletes it. A workflow added for the template's own
# maintenance carries the same guard on every job and joins MAINTAINER_ONLY_PATHS in the
# bootstrap script; tests/unit/test_bootstrap_script.py holds the two together.
name: "CLA Assistant bot"
on:
issue_comment:
Expand All @@ -13,6 +18,7 @@ permissions:

jobs:
CLAAssistant:
if: github.repository == 'Pipelex/pipelex-starter-python'
runs-on: ubuntu-latest
steps:
- name: Get GitHub App token
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/github-release.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# The template's own maintenance, not a project's: every job runs only in
# Pipelex/pipelex-starter-python, so a repository made with "Use this template" inherits
# this file inert, and /bootstrap deletes it. A workflow added for the template's own
# maintenance carries the same guard on every job and joins MAINTAINER_ONLY_PATHS in the
# bootstrap script; tests/unit/test_bootstrap_script.py holds the two together.
name: Create release

on:
Expand All @@ -8,6 +13,7 @@ on:
jobs:
github-release:
name: Create GitHub Release
if: github.repository == 'Pipelex/pipelex-starter-python'
runs-on: ubuntu-latest
permissions:
contents: write # IMPORTANT: mandatory for making GitHub Releases
Expand Down
Loading
Loading