From a84e8c5683fd85a612c63e7cf0434e011f0db7d4 Mon Sep 17 00:00:00 2001 From: sksizer Date: Tue, 7 Apr 2026 12:01:19 -0500 Subject: [PATCH] feat(scripts): add template_backport to auto-PR improvements into template template_backport.sh clones the template fresh into a tempdir (never touching the user's working copy), then runs Claude with Edit/Write/Bash to harvest high-confidence generalizable improvements from a downstream clone, apply them, run 'just full-check', and open a PR via gh. Claude classifies each diff conservatively (BACKPORT vs SKIP) and reverts any change that fails checks. Project-specific files are hard-excluded. template_backport_all.sh fans this out over downstream.txt in parallel, producing one PR per downstream repo. Exposed via 'just tb' / 'just tba'. --- justfile | 14 ++++ scripts/template_backport.sh | 118 ++++++++++++++++++++++++++++++ scripts/template_backport/role.md | 3 + scripts/template_backport/task.md | 93 +++++++++++++++++++++++ scripts/template_backport_all.sh | 60 +++++++++++++++ 5 files changed, 288 insertions(+) create mode 100755 scripts/template_backport.sh create mode 100644 scripts/template_backport/role.md create mode 100644 scripts/template_backport/task.md create mode 100755 scripts/template_backport_all.sh diff --git a/justfile b/justfile index 3a7a997..8d5adfb 100644 --- a/justfile +++ b/justfile @@ -121,3 +121,17 @@ alias tr := template-review template-review-all *args: bash scripts/template_review_all.sh {{args}} alias tra := template-review-all + +# ---------------------------------------------------------------------------- # +# TEMPLATE BACKPORT # +# ---------------------------------------------------------------------------- # + +# Backport improvements from a downstream project into a fresh template clone and open a PR (dry-run by default; --execute to run) +template-backport *args: + bash scripts/template_backport.sh {{args}} +alias tb := template-backport + +# Backport improvements from all downstream projects, one PR each (dry-run by default; --execute to run) +template-backport-all *args: + bash scripts/template_backport_all.sh {{args}} +alias tba := template-backport-all diff --git a/scripts/template_backport.sh b/scripts/template_backport.sh new file mode 100755 index 0000000..29e935f --- /dev/null +++ b/scripts/template_backport.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROMPT_DIR="${SCRIPT_DIR}/template_backport" +TEMPLATE_REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# --- Locate a JS package runner (pnpm dlx preferred, npx fallback) ---------- +find_runner() { + if command -v pnpm &>/dev/null; then + echo "pnpm dlx" + elif command -v npx &>/dev/null; then + echo "npx" + else + echo "Error: neither pnpm nor npx found. Install one of them first." >&2 + exit 1 + fi +} + +RUNNER="$(find_runner)" + +# --- Parse arguments -------------------------------------------------------- +EXECUTE=false +DOWNSTREAM_DIR="" + +for arg in "$@"; do + case "$arg" in + --execute) EXECUTE=true ;; + *) DOWNSTREAM_DIR="$arg" ;; + esac +done + +if [[ -z "$DOWNSTREAM_DIR" ]]; then + echo "Usage: $0 [--execute] " >&2 + exit 2 +fi +DOWNSTREAM_DIR="$(cd "$DOWNSTREAM_DIR" && pwd)" +DOWNSTREAM_NAME="$(basename "$DOWNSTREAM_DIR")" + +# --- Determine the template's git origin URL (so we can clone fresh) -------- +TEMPLATE_ORIGIN_URL="${TEMPLATE_ORIGIN_URL:-$(git -C "$TEMPLATE_REPO_ROOT" remote get-url origin)}" + +# --- Compose the prompt ----------------------------------------------------- +compose_prompt() { + local prompt="" + if [[ -f "${PROMPT_DIR}/role.md" ]]; then + prompt+="$(cat "${PROMPT_DIR}/role.md")" + prompt+=$'\n\n' + fi + for f in "${PROMPT_DIR}"/*.md; do + [[ "$(basename "$f")" == "role.md" ]] && continue + prompt+="$(cat "$f")" + prompt+=$'\n\n' + done + echo "$prompt" +} +PROMPT="$(compose_prompt)" + +# --- Open a URL in the default browser (best-effort) ----------------------- +open_url() { + local url="$1" + case "$(uname -s)" in + Darwin) open "$url" ;; + Linux) xdg-open "$url" ;; + MINGW*|MSYS*|CYGWIN*) cmd.exe /c start "$url" ;; + esac 2>/dev/null || true +} + +# --- Allowed tools ---------------------------------------------------------- +# Backport mode: Claude must edit files, run checks, and create a PR. +ALLOWED_TOOLS="Read Edit Write Bash" + +# --- Execute or dry-run ----------------------------------------------------- +if [[ "$EXECUTE" == true ]]; then + # Fresh clone of the template into a temp dir — never edit the user's working copy. + WORK_DIR="$(mktemp -d)" + TEMPLATE_CLONE="${WORK_DIR}/template" + echo "Runner: ${RUNNER}" + echo "Downstream: ${DOWNSTREAM_DIR} (${DOWNSTREAM_NAME})" + echo "Template origin: ${TEMPLATE_ORIGIN_URL}" + echo "Template clone: ${TEMPLATE_CLONE}" + echo "Prompt length: ${#PROMPT} chars" + echo "Allowed tools: ${ALLOWED_TOOLS}" + echo "---" + + git clone --quiet "$TEMPLATE_ORIGIN_URL" "$TEMPLATE_CLONE" + + cd "$TEMPLATE_CLONE" + export DOWNSTREAM_DIR DOWNSTREAM_NAME + OUTPUT="$(echo "${PROMPT}" | ${RUNNER} @anthropic-ai/claude-code --print \ + --allowed-tools ${ALLOWED_TOOLS})" + echo "$OUTPUT" + + PR_URL="$(echo "$OUTPUT" | grep -oE 'https://github\.com/[^ ]+/pull/[0-9]+' | head -1 || true)" + if [[ -n "$PR_URL" ]]; then + echo "Opening PR: ${PR_URL}" + open_url "$PR_URL" + else + echo "No PR opened (no candidates, or run was a no-op)." + fi + + echo "Template clone left at: ${TEMPLATE_CLONE}" +else + echo "=== DRY RUN ===" + echo + echo "${PROMPT}" + echo "---" + echo "Runner: ${RUNNER}" + echo "Downstream: ${DOWNSTREAM_DIR} (${DOWNSTREAM_NAME})" + echo "Template origin: ${TEMPLATE_ORIGIN_URL}" + echo "Prompt length: ${#PROMPT} chars" + echo "Allowed tools: ${ALLOWED_TOOLS}" + echo + echo "Would clone the template fresh, set DOWNSTREAM_DIR/DOWNSTREAM_NAME," + echo "and run Claude to apply backports + open a PR." + echo + echo "Pass --execute to actually run." +fi diff --git a/scripts/template_backport/role.md b/scripts/template_backport/role.md new file mode 100644 index 0000000..efbd4fb --- /dev/null +++ b/scripts/template_backport/role.md @@ -0,0 +1,3 @@ +You are an experienced, pragmatic software engineer backporting improvements from a downstream Rust project into the shared template it was forked from. +Your job is to identify generalizable improvements in the downstream project, apply only the safe, high-confidence ones to a clean clone of the template, run the template's checks, and open a PR. +You are conservative: when in doubt, skip a change. It is much better to open a small, obviously-correct PR than a large, ambiguous one. diff --git a/scripts/template_backport/task.md b/scripts/template_backport/task.md new file mode 100644 index 0000000..fdbca3e --- /dev/null +++ b/scripts/template_backport/task.md @@ -0,0 +1,93 @@ +## Context + +You are running inside a **fresh clone of the template repo**, which is your current working directory. Do NOT `cd` away from it for edits — this is the tree you will commit and push. + +Environment variables: + +- `DOWNSTREAM_DIR` — absolute path to a local clone of the downstream project you are harvesting from. +- `DOWNSTREAM_NAME` — short name of the downstream project (e.g. `rust-path-opener`), safe for branch names and PR titles. + +Shared infrastructure files (candidates for backport): + +- `.editorconfig`, `.gitignore`, `.prettierrc.yml` +- `.github/workflows/**` +- `.vscode/settings.json` +- `CLAUDE.md`, `cliff.toml`, `justfile`, `release.toml`, `rust-toolchain.toml`, `rustfmt.toml` +- `scripts/**` (except `scripts/downstream.txt`, which is template-only and MUST NEVER be backported) + +Files that are ALWAYS off-limits (project-specific — never backport): + +- `Cargo.toml`, `Cargo.lock` +- `README.md` +- `src/**` +- `LICENSE.md` +- `scripts/downstream.txt` + +## Task + +Follow these steps in order. + +### 1. Diff shared files + +For each shared-infrastructure path above, diff the downstream copy against the template copy (your cwd): + +``` +diff -u ./ "$DOWNSTREAM_DIR/" +``` + +For directories, use `diff -ruN ./ "$DOWNSTREAM_DIR/"`. + +### 2. Classify every difference + +For each hunk, decide: + +- **BACKPORT** — a clear improvement (bug fix, better default, new reusable script, CI fix, etc.) that would benefit every downstream project. Must be high confidence. +- **SKIP** — project-specific, stale drift, stylistic noise, or anything you are not ~95% sure is an improvement. + +Check commit history in the downstream for intent: + +``` +cd "$DOWNSTREAM_DIR" && git log --oneline -10 -- && cd - +``` + +A commit like "fix ci flake on macos" is a strong BACKPORT signal. "tweak for acme deployment" is SKIP. When in doubt, SKIP. + +### 3. Decide whether to proceed + +If there are ZERO backport candidates after classification, print: + +``` +No backport candidates from ${DOWNSTREAM_NAME}. +``` + +…and exit. Do NOT create a branch, commit, or PR. + +### 4. Apply BACKPORT changes + +For each BACKPORT candidate, copy/merge the change from `$DOWNSTREAM_DIR` into the template (your cwd). Prefer `cp "$DOWNSTREAM_DIR/" ./` when the entire file should be replaced; use `Edit` for surgical hunks. Never touch off-limits files. + +### 5. Run checks + +``` +just full-check +``` + +If formatting fails, run `just full-write` and re-check. If clippy or other errors remain, either fix them yourself or **revert the offending backport** (`git checkout -- `) and remove it from your candidate list. Re-run until `just full-check` passes. + +If this leaves you with zero applied changes, print `No backport candidates from ${DOWNSTREAM_NAME} survived checks.` and exit without branching/committing/pushing. + +### 6. Branch, commit, push, PR + +``` +git checkout -b chore/backport-from-${DOWNSTREAM_NAME}-$(date +%Y-%m-%d) +git add -A +git commit -m "chore: backport improvements from ${DOWNSTREAM_NAME}" +git push -u origin HEAD +``` + +Create the PR with `gh pr create`: + +- Title: `chore: backport improvements from ${DOWNSTREAM_NAME}` +- Body: a markdown summary listing each backported file, one sentence per file explaining the improvement and citing the downstream commit sha (short form) and subject line when available. End with a "Skipped" section briefly noting anything you classified SKIP, so the reviewer can cross-check your judgment. + +Print the PR URL on the final line of your output. diff --git a/scripts/template_backport_all.sh b/scripts/template_backport_all.sh new file mode 100755 index 0000000..8d1499b --- /dev/null +++ b/scripts/template_backport_all.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOWNSTREAM_FILE="${SCRIPT_DIR}/downstream.txt" +TEMPLATE_BACKPORT="${SCRIPT_DIR}/template_backport.sh" + +if [[ ! -f "$DOWNSTREAM_FILE" ]]; then + echo "Error: ${DOWNSTREAM_FILE} not found" >&2 + exit 1 +fi + +ARGS=("$@") + +WORK_DIR="$(mktemp -d)" +echo "Clone directory: ${WORK_DIR}" + +PIDS=() +REPOS=() +LOG_FILES=() + +while IFS= read -r repo_url; do + [[ -z "$repo_url" || "$repo_url" == \#* ]] && continue + + REPO_NAME="$(basename "${repo_url%/}" .git)" + CLONE_PATH="${WORK_DIR}/${REPO_NAME}" + LOG_FILE="${WORK_DIR}/${REPO_NAME}.log" + + echo "Cloning downstream: ${repo_url} -> ${CLONE_PATH}" + ( + { + git clone --quiet "$repo_url" "$CLONE_PATH" + bash "$TEMPLATE_BACKPORT" ${ARGS[@]+"${ARGS[@]}"} "$CLONE_PATH" + } >"$LOG_FILE" 2>&1 + ) & + PIDS+=($!) + REPOS+=("$repo_url") + LOG_FILES+=("$LOG_FILE") +done < "$DOWNSTREAM_FILE" + +FAILED=0 +for i in "${!PIDS[@]}"; do + echo + echo "════════════════════════════════════════════════════════════════" + echo " ${REPOS[$i]}" + echo "════════════════════════════════════════════════════════════════" + if wait "${PIDS[$i]}"; then + cat "${LOG_FILES[$i]}" + else + echo "FAILED:" >&2 + cat "${LOG_FILES[$i]}" >&2 || true + FAILED=$((FAILED + 1)) + fi +done + +echo +echo "---" +echo "Finished: $((${#PIDS[@]} - FAILED))/${#PIDS[@]} succeeded" +echo "Work dir left at: ${WORK_DIR}" +exit $FAILED