From 454f3410a11f9315970626d71fa980de84b76ff9 Mon Sep 17 00:00:00 2001 From: clawbot Date: Mon, 9 Nov 2026 18:32:46 +0000 Subject: [PATCH 01/12] Add script/install; script/update-policy runs it and commits (closes #165) script/install fetches the policy files from prompts over HTTP and writes them into a repo, installing itself and script/update-policy there. script/update-policy fetches the current script/install, runs it on its own repo and commits without pushing. The file list and the rules for keeping a repo's own entries move to script/install. Model: opus-5-5 --- Makefile | 5 +- README.md | 9 +- TODO.md | 5 + prompts/EXISTING_REPO_CHECKLIST.md | 14 +- prompts/NEW_REPO_CHECKLIST.md | 7 +- prompts/REPO_POLICIES.md | 36 ++- script/install | 246 ++++++++++++++++++ script/update-policy | 211 ++------------- test/update-policy.sh | 400 ++++++++++++++++++++--------- 9 files changed, 598 insertions(+), 335 deletions(-) create mode 100755 script/install diff --git a/Makefile b/Makefile index 6488473..c22ff51 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks update-policy +.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks install update-policy # Makefile targets are thin shims; the implementations live in script/ # per the scripts-to-rule-them-all pattern (see the Entrypoints section @@ -31,5 +31,8 @@ docker: hooks: @script/install-precommit +install: + @script/install + update-policy: @script/update-policy diff --git a/README.md b/README.md index 740c99a..e35efd5 100644 --- a/README.md +++ b/README.md @@ -141,9 +141,12 @@ alpine. We provide: `script/check` - `script/install-precommit` — installs the git pre-commit hook (our own extension); `make hooks` shims to it -- `script/update-policy` — fetch the vendored policy files from this repo, - commit them and push (our own extension); it is for the repos that vendor them - and stops when run here +- `script/install` — fetch the vendored policy files from this repo over HTTP + and write them into the repo given as its argument, or its own repo with no + argument (our own extension); it stops when run here +- `script/update-policy` — fetch the current `script/install`, run it on its own + repo and commit the result without pushing (our own extension); it is for the + repos that vendor the policy files and stops when run here `make hooks` installs the pre-commit hook that runs `script/precommit`. diff --git a/TODO.md b/TODO.md index 3eb1e5b..31800dc 100644 --- a/TODO.md +++ b/TODO.md @@ -21,6 +21,11 @@ fmt-check, and commit. # Completed Steps +- 2026-11-09: `script/install` writes the vendored policy files into a repo over + HTTP, and installs itself and `script/update-policy` there + (https://git.eeqj.de/sneak/prompts/issues/165). `make update-policy` now + fetches the current `script/install`, runs it and commits without pushing. The + template repositories get both scripts in their own issues. - 2026-11-09: `script/update-policy` and `make update-policy` update a repo's vendored policy files with one command (https://git.eeqj.de/sneak/prompts/issues/162). They fetch the files from diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index 8c3849d..286cfcf 100644 --- a/prompts/EXISTING_REPO_CHECKLIST.md +++ b/prompts/EXISTING_REPO_CHECKLIST.md @@ -205,17 +205,19 @@ with your task. - [ ] `Makefile` exists in root — reference `https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile` - [ ] Has targets: `test`, `lint`, `fmt`, `fmt-check`, `check`, `docker`, - `hooks`, `update-policy` + `hooks`, `install`, `update-policy` - [ ] Target implementations live in `script/` (scripts-to-rule-them-all); Makefile targets are thin shims calling them — model scripts at `https://git.eeqj.de/sneak/prompts/raw/branch/main/script/` - [ ] `script/precommit` exists and the pre-commit hook (installed by `script/install-precommit`, shimmed by `make hooks`) runs it -- [ ] `script/update-policy` exists, identical to the model script, and - `make update-policy` shims to it. Before its first use, the repo's own - entries in `.gitignore`, `.editorconfig` and `.dockerignore` go below the - comment that ends each canonical file. An entry above that comment which - the canonical file has never held makes the script stop before it writes +- [ ] `script/install` and `script/update-policy` exist, identical to the model + scripts, and `make install` and `make update-policy` shim to them. + `make update-policy` commits without pushing, so a person reviews and + pushes the commit. Before its first use, the repo's own entries in + `.gitignore`, `.editorconfig` and `.dockerignore` go below the comment + that ends each canonical file. An entry above that comment which the + canonical file has never held makes the script stop before it writes anything, naming the file and the entry. It stops the same way on a line in `.golangci.yml` that the canonical file has never held, apart from comments and the repo's entries in the `deny` list of the `test-support` diff --git a/prompts/NEW_REPO_CHECKLIST.md b/prompts/NEW_REPO_CHECKLIST.md index ff769b1..cc00df2 100644 --- a/prompts/NEW_REPO_CHECKLIST.md +++ b/prompts/NEW_REPO_CHECKLIST.md @@ -266,9 +266,12 @@ are thin shims calling them. Model scripts: - [ ] `script/install-precommit` — installs the pre-commit hook that runs `script/precommit` - [ ] `make hooks` — shims to `script/install-precommit` -- [ ] `script/update-policy` / `make update-policy` — fetches the vendored - policy files from the `prompts` repo, commits them and pushes; identical +- [ ] `script/install` / `make install` — fetches the vendored policy files from + the `prompts` repo over HTTP and writes them into the repo; identical across repos +- [ ] `script/update-policy` / `make update-policy` — fetches the current + `script/install`, runs it on the repo and commits the result without + pushing; identical across repos - [ ] README **Entrypoints** section documents the scripts and links the standard diff --git a/prompts/REPO_POLICIES.md b/prompts/REPO_POLICIES.md index 528bcfa..0a1a783 100644 --- a/prompts/REPO_POLICIES.md +++ b/prompts/REPO_POLICIES.md @@ -93,8 +93,8 @@ style conventions are in separate documents: - `make check` (runs `test`, `lint`, `fmt-check`) - `make docker` - `make hooks` (installs pre-commit hook) - - `make update-policy` (fetches the vendored policy files, commits and - pushes) + - `make install` (writes the current policy files into the repo) + - `make update-policy` (brings the repo up to standards and commits) A model Makefile is at `https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile`. @@ -111,6 +111,7 @@ style conventions are in separate documents: - `script/fmt-check` - `script/check` - `script/docker` + - `script/install` - `script/update-policy` The scripts must be POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they @@ -185,15 +186,18 @@ style conventions are in separate documents: host. On a pristine checkout with nothing installed, the run dies there, after the containerised gates have passed. - Five further scripts are our own extensions to the standard: + Six further scripts are our own extensions to the standard: - `script/check` runs `script/test`, `script/lint` and `script/fmt-check`. - `script/precommit` is what the git pre-commit hook runs, and it calls `script/check`. - `script/install-precommit` installs the git pre-commit hook. The `make hooks` target shims to it. - - `script/update-policy` fetches the vendored policy files from the - `prompts` repo, commits them and pushes. The `make update-policy` target - shims to it. + - `script/install` fetches the vendored policy files from the `prompts` repo + over HTTP and writes them into the repo it is given, or its own repo with + no argument. The `make install` target shims to it. + - `script/update-policy` fetches the current `script/install` from the + `prompts` repo, runs it on its own repo and commits the result. It does + not push. The `make update-policy` target shims to it. - `script/projectname` (literally that filename) simply outputs the project's name. @@ -933,12 +937,18 @@ style conventions are in separate documents: - Make all changes on a feature branch. You can do whatever you want on a feature branch. -- A repo updates its policy files with one command: `make update-policy`. It - runs `script/update-policy`, which fetches each vendored file the repo carries - from the `prompts` repo, commits them and pushes. It uses the head of `main`, - or the commit given by a full hash: `script/update-policy `. The list of - vendored files is at the top of that script. The script replaces each file and - keeps only the repo's own entries: +- One command brings a repo up to standards: `make update-policy`. It runs + `script/update-policy`, which fetches the current `script/install` from the + head of `main` in the `prompts` repo and runs it on the repo. It then commits + the files that changed and does not push, so a person reviews and pushes the + commit. + + `script/install` fetches each vendored file the repo carries from `prompts` + over HTTP. It also installs `script/install` and `script/update-policy` in + the repo, so a repo updated once can update itself. Every repo made from a + template repo already has both. The list of vendored files is at the top of + `script/install`. The script replaces each file and keeps only the repo's + own entries: - in `.gitignore`, `.editorconfig` and `.dockerignore`: the lines below the comment that ends the canonical file - in `.golangci.yml`: the entries the repo added to the `deny` list of the @@ -1169,7 +1179,7 @@ style conventions are in separate documents: - `Makefile` - `script/` entrypoints (`bootstrap`, `setup`, `projectname`, `test`, `lint`, `fmt`, `fmt-check`, `check`, `docker`, `cibuild`, `precommit`, - `install-precommit`, `update-policy`) + `install-precommit`, `install`, `update-policy`) - `Dockerfile`, `.dockerignore` - `.gitea/workflows/check.yml` - Go: `go.mod`, `go.sum`, `.golangci.yml` diff --git a/script/install b/script/install new file mode 100755 index 0000000..788219f --- /dev/null +++ b/script/install @@ -0,0 +1,246 @@ +#!/bin/sh +# script/install: write the current policy files from the prompts repo +# into a repo clone, over HTTP. It commits nothing; script/update-policy +# does that. Identical in all repos. Our own extension to +# scripts-to-rule-them-all. +# +# Usage: script/install [path of a repo clone] +# Without a path it updates the repo this script sits in. +# +# Standard output is the full hash of the prompts commit, then each path +# written, one per line. Messages go to standard error. +# +# The script writes nothing until every file has been fetched and +# merged. A repo's own entries are kept: +# - .gitignore, .editorconfig, .dockerignore: the lines below the +# canonical file's last line. An entry above that line which the +# canonical file has never held stops the script. +# - .golangci.yml: the entries the repo added to the deny list of the +# test-support rule. Any other line the canonical file has never +# held stops the script, unless it is blank or a comment. +set -eu + +PROMPTS_URL="https://git.eeqj.de/sneak/prompts" +PROMPTS_API_URL="https://git.eeqj.de/api/v1/repos/sneak/prompts" +PROMPTS_FIRST_COMMIT="af257bef63f162a681bddc80d9b941c47a44b748" + +# Paths in the prompts repo. A file the target repo does not carry is +# skipped, except the two scripts, which every repo gets. +VENDORED_FILES=" +.dockerignore +.editorconfig +.gitea/workflows/check.yml +.gitignore +.golangci.yml +.prettierrc +prompts/API_POLICIES.md +prompts/CODE_STYLEGUIDE.md +prompts/CODE_STYLEGUIDE_GO.md +prompts/CODE_STYLEGUIDE_JS.md +prompts/CODE_STYLEGUIDE_PYTHON.md +prompts/EXISTING_REPO_CHECKLIST.md +prompts/GO_HTTP_SERVER_CONVENTIONS.md +prompts/LLM_PROSE_TELLS.md +prompts/NEW_REPO_CHECKLIST.md +prompts/REPO_POLICIES.md +script/install +script/update-policy +" + +die() { + echo "install: $*" >&2 + exit 1 +} + +# fetch +fetch() { + curl --fail --silent --show-error --max-time 60 --output "$2" "$1" || + die "cannot fetch $1" +} + +# local_path : print where the target repo keeps the +# file, or nothing if git does not track it there. A prompts/NAME.md +# lands at docs/NAME.md or NAME.md. +local_path() { + name="${1#prompts/}" + if [ "$name" != "$1" ] && [ -n "$(git ls-files -- "docs/$name")" ]; then + echo "docs/$name" + elif [ -n "$(git ls-files -- "$name")" ]; then + echo "$name" + fi +} + +# former_lines : print the lines earlier commits +# removed from the file. The file's section of the patch ends at the +# next diff or the next commit. +former_lines() { + awk -v header="diff --git a/$2 b/$2" ' + /^diff --git / { inside = ($0 == header); next } + /^From / { inside = 0; next } + inside && /^-/ { print substr($0, 2) } + ' "$1" +} + +# local_only_lines [end]: print each line of the local +# file, above line end if given, that the canonical file has never held: +# not in the fetched file or in .former, the lines earlier +# commits removed from it. Blank lines and comments do not count. +local_only_lines() { + cat "$1" "$1.former" | awk -v end="${3:-}" ' + NR == FNR { held[$0]; next } + (end == "" || FNR < end) && !/^[ \t]*(#|$)/ && !($0 in held) + ' - "$2" +} + +# keep_lines_below_last : append to the fetched file +# whatever follows its last line in the local file. Stop if the canonical +# file has never held a line above that one, unless it is blank or a +# comment: replacing the file would delete it. +keep_lines_below_last() { + last="$(tail -n 1 "$1")" + number="$(grep -n -F -x -- "$last" "$2" | head -n 1 | cut -d: -f1)" + if [ -z "$last" ] || [ -z "$number" ]; then + die "$2 lacks this line; add it and put the repo's own entries below it: $last" + fi + own="$(local_only_lines "$1" "$2" "$number")" + if [ -n "$own" ]; then + die "$2 has entries above its last canonical line that the canonical file lacks; move each below that line to keep it, or delete it: +$own" + fi + tail -n "+$((number + 1))" "$2" >> "$1" +} + +# keep_deny_entries : add to the deny list of the +# test-support rule in the fetched file the entries only the local file +# has. An entry is a "- pkg:" line, the lines under it and any comment +# above it. Stop if the local file still has a line that neither the +# result nor the canonical file has ever held, unless it is blank or a +# comment: replacing the file would delete it. +keep_deny_entries() { + awk ' + function indent(line) { return match(line, /[^ ]/) - 1 } + function print_local_entries(i) { + for (i = 1; i <= count; i++) + if (!(pkg[i] in fetched_pkg)) + printf "%s", entry[i] + } + + FNR == 1 { in_rule = 0; in_list = 0 } + + in_list && /[^ ]/ && indent($0) <= list_indent { + in_list = 0 + if (FILENAME == ARGV[2]) print_local_entries() + } + + in_list { + is_entry = ($1 == "-" && $2 == "pkg:") + if (FILENAME == ARGV[2]) { + if (is_entry) fetched_pkg[$3] + print + } else if (is_entry) { + count++ + pkg[count] = $3 + entry[count] = comment $0 "\n" + comment = "" + } else if (/^ *#/) { + comment = comment $0 "\n" + } else if (count) { + entry[count] = entry[count] $0 "\n" + } + next + } + + in_rule && /^ *[^ #]/ && indent($0) <= rule_indent { in_rule = 0 } + /^ *test-support:/ { in_rule = 1; rule_indent = indent($0) } + in_rule && /^ *deny:/ { + in_rule = 0 + in_list = 1 + list_indent = indent($0) + if (FILENAME == ARGV[2]) found = 1 + } + FILENAME == ARGV[2] { print } + + END { + if (in_list) print_local_entries() + exit !found + } + ' "$2" "$1" > "$1.merged" || + die "the fetched $2 has no deny list in its test-support rule" + mv "$1.merged" "$1" + own="$(local_only_lines "$1" "$2")" + if [ -n "$own" ]; then + die "$2 has lines the canonical file has never held; only the deny list of the test-support rule may differ from canonical: +$own" + fi +} + +main() { + [ "$#" -le 1 ] || die "usage: script/install [path of a repo clone]" + + target="${1:-$(dirname "$0")/..}" + cd "$target" || die "no such directory: $target" + if [ "$(git rev-parse --show-toplevel 2> /dev/null)" != "$(pwd -P)" ]; then + die "not the top of a git clone: $target" + fi + if [ -e prompts/REPO_POLICIES.md ]; then + die "this repo is the source of the policy files" + fi + + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + + fetch "$PROMPTS_API_URL/branches/main" "$tmp/branch.json" + commit="$(grep -Eo '"id":"[0-9a-f]{40}"' "$tmp/branch.json" | + head -n 1 | cut -d'"' -f4)" + [ -n "$commit" ] || die "no commit hash in $PROMPTS_API_URL/branches/main" + + fetch "$PROMPTS_URL/compare/$PROMPTS_FIRST_COMMIT...$commit.patch" \ + "$tmp/history.patch" + + carried="" + for file in $VENDORED_FILES; do + case "$file" in + script/*) path="$file" ;; + *) path="$(local_path "$file")" ;; + esac + [ -n "$path" ] || continue + if [ -n "$(git status --porcelain -- "$path")" ]; then + die "$path has uncommitted changes" + fi + mkdir -p "$tmp/$(dirname "$path")" + fetch "$PROMPTS_URL/raw/commit/$commit/$file" "$tmp/$path" + former_lines "$tmp/history.patch" "$file" > "$tmp/$path.former" + carried="$carried $path" + done + + for path in $carried; do + case "$path" in + .gitignore | .editorconfig | .dockerignore) + keep_lines_below_last "$tmp/$path" "$path" + ;; + .golangci.yml) + keep_deny_entries "$tmp/$path" "$path" + ;; + esac + done + + echo "$commit" + for path in $carried; do + case "$path" in + script/*) + # A new name and mv, because script/update-policy may + # be the script running. + mkdir -p script + cp "$tmp/$path" "$path.new" + chmod 755 "$path.new" + mv "$path.new" "$path" + ;; + *) + cp "$tmp/$path" "$path" + ;; + esac + echo "$path" + done +} + +main "$@" diff --git a/script/update-policy b/script/update-policy index eaf4bea..d58aa88 100755 --- a/script/update-policy +++ b/script/update-policy @@ -1,222 +1,43 @@ #!/bin/sh -# script/update-policy: replace this repo's vendored policy files with -# the canonical ones from the prompts repo, commit them and push. -# Identical in all repos. Our own extension to -# scripts-to-rule-them-all. -# -# Usage: script/update-policy [full commit hash of prompts] -# Without a hash it uses the head of the prompts main branch. -# -# The script writes nothing until every file has been fetched and -# merged. A repo's own entries are kept: -# - .gitignore, .editorconfig, .dockerignore: the lines below the -# canonical file's last line. An entry above that line which the -# canonical file has never held stops the script. -# - .golangci.yml: the entries the repo added to the deny list of the -# test-support rule. Any other line the canonical file has never -# held stops the script, unless it is blank or a comment. +# script/update-policy: bring this repo up to standards. It fetches the +# current script/install from the prompts repo over HTTP, runs it on this +# repo and commits the files it wrote. It does not push. Identical in all +# repos. Our own extension to scripts-to-rule-them-all. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" -PROMPTS_URL="https://git.eeqj.de/sneak/prompts" - -# Paths in the prompts repo. A file this repo does not carry is skipped. -VENDORED_FILES=" -.dockerignore -.editorconfig -.gitea/workflows/check.yml -.gitignore -.golangci.yml -.prettierrc -prompts/API_POLICIES.md -prompts/CODE_STYLEGUIDE.md -prompts/CODE_STYLEGUIDE_GO.md -prompts/CODE_STYLEGUIDE_JS.md -prompts/CODE_STYLEGUIDE_PYTHON.md -prompts/EXISTING_REPO_CHECKLIST.md -prompts/GO_HTTP_SERVER_CONVENTIONS.md -prompts/LLM_PROSE_TELLS.md -prompts/NEW_REPO_CHECKLIST.md -prompts/REPO_POLICIES.md -" +INSTALL_URL="https://git.eeqj.de/sneak/prompts/raw/branch/main/script/install" die() { echo "update-policy: $*" >&2 exit 1 } -# local_path : print where this repo keeps the file, or -# nothing if git does not track it here. A prompts/NAME.md lands at -# docs/NAME.md or NAME.md. -local_path() { - name="${1#prompts/}" - if [ "$name" != "$1" ] && [ -n "$(git ls-files -- "docs/$name")" ]; then - echo "docs/$name" - elif [ -n "$(git ls-files -- "$name")" ]; then - echo "$name" - fi -} - -# local_only_lines [end]: print each line of the local -# file, above line end if given, that the canonical file has never held: -# not in the fetched file or in .former, the lines earlier -# commits removed from it. Blank lines and comments do not count. -local_only_lines() { - cat "$1" "$1.former" | awk -v end="${3:-}" ' - NR == FNR { held[$0]; next } - (end == "" || FNR < end) && !/^[ \t]*(#|$)/ && !($0 in held) - ' - "$2" -} - -# keep_lines_below_last : append to the fetched file -# whatever follows its last line in the local file. Stop if the canonical -# file has never held a line above that one, unless it is blank or a -# comment: replacing the file would delete it. -keep_lines_below_last() { - last="$(tail -n 1 "$1")" - number="$(grep -n -F -x -- "$last" "$2" | head -n 1 | cut -d: -f1)" - if [ -z "$last" ] || [ -z "$number" ]; then - die "$2 lacks this line; add it and put the repo's own entries below it: $last" - fi - own="$(local_only_lines "$1" "$2" "$number")" - if [ -n "$own" ]; then - die "$2 has entries above its last canonical line that the canonical file lacks; move each below that line to keep it, or delete it: -$own" - fi - tail -n "+$((number + 1))" "$2" >> "$1" -} - -# keep_deny_entries : add to the deny list of the -# test-support rule in the fetched file the entries only the local file -# has. An entry is a "- pkg:" line, the lines under it and any comment -# above it. Stop if the local file still has a line that neither the -# result nor the canonical file has ever held, unless it is blank or a -# comment: replacing the file would delete it. -keep_deny_entries() { - awk ' - function indent(line) { return match(line, /[^ ]/) - 1 } - function print_local_entries(i) { - for (i = 1; i <= count; i++) - if (!(pkg[i] in fetched_pkg)) - printf "%s", entry[i] - } - - FNR == 1 { in_rule = 0; in_list = 0 } - - in_list && /[^ ]/ && indent($0) <= list_indent { - in_list = 0 - if (FILENAME == ARGV[2]) print_local_entries() - } - - in_list { - is_entry = ($1 == "-" && $2 == "pkg:") - if (FILENAME == ARGV[2]) { - if (is_entry) fetched_pkg[$3] - print - } else if (is_entry) { - count++ - pkg[count] = $3 - entry[count] = comment $0 "\n" - comment = "" - } else if (/^ *#/) { - comment = comment $0 "\n" - } else if (count) { - entry[count] = entry[count] $0 "\n" - } - next - } - - in_rule && /^ *[^ #]/ && indent($0) <= rule_indent { in_rule = 0 } - /^ *test-support:/ { in_rule = 1; rule_indent = indent($0) } - in_rule && /^ *deny:/ { - in_rule = 0 - in_list = 1 - list_indent = indent($0) - if (FILENAME == ARGV[2]) found = 1 - } - FILENAME == ARGV[2] { print } - - END { - if (in_list) print_local_entries() - exit !found - } - ' "$2" "$1" > "$1.merged" || - die "the fetched $2 has no deny list in its test-support rule" - mv "$1.merged" "$1" - own="$(local_only_lines "$1" "$2")" - if [ -n "$own" ]; then - die "$2 has lines the canonical file has never held; only the deny list of the test-support rule may differ from canonical: -$own" - fi -} - main() { + [ "$#" -eq 0 ] || die "usage: script/update-policy" + cd "$ROOT" - [ "$#" -le 1 ] || die "usage: script/update-policy [full commit hash]" - - project="$(script/projectname)" - if [ "$project" = "prompts" ]; then - die "this repo is the source of the policy files" - fi - - commit="${1:-}" - if [ -z "$commit" ]; then - head_line="$(git ls-remote "$PROMPTS_URL.git" refs/heads/main)" - commit="${head_line%%[[:space:]]*}" - fi - printf '%s\n' "$commit" | grep -Eqx '[0-9a-f]{40}' || - die "not a full commit hash: '$commit'" - tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT - git clone --quiet --bare "$PROMPTS_URL.git" "$tmp/prompts.git" || - die "cannot clone $PROMPTS_URL.git" + curl --fail --silent --show-error --max-time 60 \ + --output "$tmp/install" "$INSTALL_URL" || + die "cannot fetch $INSTALL_URL" - carried="" - for file in $VENDORED_FILES; do - path="$(local_path "$file")" - [ -n "$path" ] || continue - if [ -n "$(git status --porcelain -- "$path")" ]; then - die "$path has uncommitted changes" - fi - mkdir -p "$tmp/$(dirname "$path")" - curl --fail --silent --show-error --max-time 60 \ - --output "$tmp/$path" \ - "$PROMPTS_URL/raw/commit/$commit/$file" || - die "cannot fetch $file at $commit" - git -C "$tmp/prompts.git" log -p --format= "$commit" -- "$file" | - sed -n 's/^-//p' > "$tmp/$path.former" - carried="$carried $path" - done - [ -n "$carried" ] || die "this repo carries none of the vendored files" + sh "$tmp/install" "$ROOT" > "$tmp/written" + commit="$(head -n 1 "$tmp/written")" + paths="$(tail -n +2 "$tmp/written")" - for path in $carried; do - case "$path" in - .gitignore | .editorconfig | .dockerignore) - keep_lines_below_last "$tmp/$path" "$path" - ;; - .golangci.yml) - keep_deny_entries "$tmp/$path" "$path" - ;; - esac - done - - for path in $carried; do - cp "$tmp/$path" "$path" + for path in $paths; do git add -- "$path" done - if git diff --cached --quiet -- $carried; then + if git diff --cached --quiet -- $paths; then echo "update-policy: already at prompts $commit, nothing to commit" else - git commit -m "Update policy files from prompts $commit" -- $carried + git commit -m "Update policy files from prompts $commit" -- $paths fi - - # Also pushes when there was nothing to commit, so a run after a - # failed push delivers the commit that push left behind. - git push origin HEAD || die "push failed; fix that and run this again" } main "$@" diff --git a/test/update-policy.sh b/test/update-policy.sh index 08bdee5..70e1094 100755 --- a/test/update-policy.sh +++ b/test/update-policy.sh @@ -1,12 +1,14 @@ #!/bin/sh -# test/update-policy.sh: test script/update-policy on throwaway repos. -# Each scenario gets one temp directory holding a repo, its local bare -# origin, a local git repository of the canonical files in place of -# prompts, and a stub curl that serves them, so nothing touches the -# network. +# test/update-policy.sh: test script/install and script/update-policy on +# throwaway repos. Each scenario gets one temp directory holding a repo, +# its local bare origin, a local git repository of the canonical files +# in place of prompts, and a stub curl that serves them, so nothing +# touches the network. set -eu -UPDATE_POLICY="$(cd "$(dirname "$0")/.." && pwd -P)/script/update-policy" +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd -P)/script" +INSTALL="$SCRIPT_DIR/install" +UPDATE_POLICY="$SCRIPT_DIR/update-policy" dir="" trap 'rm -rf "$dir"' EXIT @@ -17,6 +19,8 @@ fail() { exit 1 } +# The stub stops with curl's HTTP error status on any URL containing +# FAIL_URL, and answers with no commit hash when BAD_BRANCH_JSON is set. write_stub_curl() { mkdir "$dir/bin" cat > "$dir/bin/curl" << 'EOF' @@ -29,21 +33,53 @@ for argument in "$@"; do previous="$argument" done url="$previous" -base="https://git.eeqj.de/sneak/prompts/raw/commit/$HASH/" -path="${url#"$base"}" -source="$(dirname "$0")/../canonical/$path" -if [ "$path" = "$url" ] || [ ! -f "$source" ]; then +canonical="$(dirname "$0")/../canonical" +site="https://git.eeqj.de/sneak/prompts" +first="af257bef63f162a681bddc80d9b941c47a44b748" + +not_found() { [ -n "$fail_flag" ] && exit 22 echo "404 page not found" > "$output" exit 0 -fi -cp "$source" "$output" +} + +serve_file() { + [ -f "$canonical/$1" ] || not_found + cp "$canonical/$1" "$output" +} + +case "$url" in + *"${FAIL_URL:-no url contains this}"*) + not_found + ;; + "https://git.eeqj.de/api/v1/repos/sneak/prompts/branches/main") + if [ -n "${BAD_BRANCH_JSON:-}" ]; then + echo '{"message":"no such branch"}' > "$output" + else + printf '{"name":"main","commit":{"id":"%s"},"x":{"id":"%s"}}\n' \ + "$HASH" 0000000000000000000000000000000000000000 > "$output" + fi + ;; + "$site/raw/branch/main/script/install") + serve_file script/install + ;; + "$site/compare/$first...$HASH.patch") + git -C "$canonical" format-patch --stdout --root "$HASH" > "$output" + ;; + "$site/raw/commit/$HASH/"*) + serve_file "${url#"$site/raw/commit/$HASH/"}" + ;; + *) + not_found + ;; +esac EOF chmod +x "$dir/bin/curl" } write_canonical_files() { - mkdir -p "$dir/canonical/prompts" + mkdir -p "$dir/canonical/prompts" "$dir/canonical/script" + cp "$INSTALL" "$UPDATE_POLICY" "$dir/canonical/script/" cat > "$dir/canonical/.gitignore" << 'EOF' *.log *.tmp @@ -87,7 +123,7 @@ EOF # curl serves only the newest commit; every other commit gets a 404. commit_canonical() { git -C "$dir/canonical" add .gitignore .dockerignore .editorconfig \ - .golangci.yml .prettierrc prompts + .golangci.yml .prettierrc prompts script git -C "$dir/canonical" commit -q -m "Canonical" HASH="$(git -C "$dir/canonical" rev-parse HEAD)" } @@ -144,16 +180,16 @@ start_scenario() { rm -rf "$dir" dir="$(mktemp -d)" : > "$dir/output" + mkdir "$dir/tmp" write_stub_curl write_canonical_files - # git clone reads no repo's own config, so the setting that points - # git at the local canonical repository has to be in this file. export GIT_CONFIG_GLOBAL="$dir/gitconfig" git config --global user.name "Test" git config --global user.email "test@example.com" - git config --global "url.$dir/canonical.insteadOf" \ - "https://git.eeqj.de/sneak/prompts.git" + + export FAIL_URL="" + export BAD_BRANCH_JSON="" git init -q -b main "$dir/canonical" export HASH @@ -164,7 +200,7 @@ start_scenario() { git -C "$dir/repo" remote add origin "$dir/origin.git" mkdir "$dir/repo/script" - cp "$UPDATE_POLICY" "$dir/repo/script/update-policy" + cp "$INSTALL" "$UPDATE_POLICY" "$dir/repo/script/" printf '#!/bin/sh\necho example\n' > "$dir/repo/script/projectname" chmod +x "$dir/repo/script/projectname" write_outdated_files @@ -185,17 +221,16 @@ commit_and_push_fixture() { run_update_policy() { status=0 - (cd "$dir/repo" && PATH="$dir/bin:$PATH" script/update-policy "$@") \ - > "$dir/output" 2>&1 || status=$? + (cd "$dir/repo" && PATH="$dir/bin:$PATH" TMPDIR="$dir/tmp" \ + script/update-policy "$@") > "$dir/output" 2>&1 || status=$? } -reject_pushes() { - printf '#!/bin/sh\nexit 1\n' > "$dir/origin.git/hooks/pre-receive" - chmod +x "$dir/origin.git/hooks/pre-receive" -} - -accept_pushes() { - rm "$dir/origin.git/hooks/pre-receive" +# run_install : run the script in this source tree, from +# outside the repo it updates. +run_install() { + status=0 + (cd "$dir" && PATH="$dir/bin:$PATH" TMPDIR="$dir/tmp" \ + "$INSTALL" "$@") > "$dir/output" 2>&1 || status=$? } head_of_repo() { @@ -206,6 +241,10 @@ head_of_origin() { git -C "$dir/origin.git" rev-parse refs/heads/main } +files_in_commit() { + git -C "$dir/repo" show --name-only --format= HEAD | tr '\n' ' ' +} + expect_success() { [ "$status" -eq 0 ] || fail "exited $status" } @@ -230,22 +269,44 @@ expect_file() { diff "$dir/expected" "$dir/repo/$1" >&2 || fail "$1 is wrong" } +expect_same_as_canonical() { + diff "$dir/canonical/$1" "$dir/repo/$1" >&2 || fail "$1 is not canonical" +} + expect_absent() { [ ! -e "$dir/repo/$1" ] || fail "$1 exists" } +expect_executable() { + [ "$(stat -c %a "$dir/repo/$1")" = 755 ] || fail "$1 is not mode 755" +} + +expect_committed_executable() { + git -C "$dir/repo" ls-files -s -- "$1" | grep -q '^100755 ' || + fail "$1 is not committed as executable" +} + +expect_new_commit_not_pushed() { + [ "$(head_of_repo)" != "$before" ] || fail "no commit was made" + [ "$(head_of_origin)" = "$before" ] || fail "the commit was pushed" +} + +expect_no_commit() { + [ "$(head_of_repo)" = "$before" ] || fail "the repo has a new commit" + [ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit" +} + expect_nothing_written() { [ -z "$(git -C "$dir/repo" status --porcelain)" ] || fail "the working tree changed" - [ "$(head_of_repo)" = "$before" ] || fail "the repo has a new commit" - [ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit" + expect_no_commit } test_own_entries_kept() { start_scenario "own entries kept in all four files" commit_and_push_fixture .gitignore .dockerignore .editorconfig \ .golangci.yml .prettierrc REPO_POLICIES.md - run_update_policy "$HASH" + run_update_policy expect_success expect_file .gitignore << 'EOF' @@ -296,11 +357,12 @@ EOF new policy EOF - [ "$(head_of_origin)" = "$(head_of_repo)" ] || - fail "the commit was not pushed" - [ "$(head_of_repo)" != "$before" ] || fail "no commit was made" + expect_new_commit_not_pushed git -C "$dir/repo" log -1 --format=%s | grep -q -F -e "$HASH" || - fail "the commit message does not name the hash" + fail "the commit message does not name the head of main" + [ "$(files_in_commit)" = ".dockerignore .editorconfig .gitignore .golangci.yml .prettierrc REPO_POLICIES.md " ] || + fail "the commit holds other files: $(files_in_commit)" + [ -z "$(ls -A "$dir/tmp")" ] || fail "a temporary directory is left" } test_files_not_carried_are_skipped() { @@ -308,7 +370,7 @@ test_files_not_carried_are_skipped() { mkdir "$dir/repo/docs" mv "$dir/repo/REPO_POLICIES.md" "$dir/repo/docs/REPO_POLICIES.md" commit_and_push_fixture docs/REPO_POLICIES.md - run_update_policy "$HASH" + run_update_policy expect_success expect_file docs/REPO_POLICIES.md << 'EOF' @@ -322,40 +384,145 @@ EOF expect_absent .editorconfig expect_absent .golangci.yml expect_absent .prettierrc - [ "$(head_of_origin)" = "$(head_of_repo)" ] || - fail "the commit was not pushed" + expect_new_commit_not_pushed } test_nothing_changed() { start_scenario "nothing changed" cp "$dir/canonical/.prettierrc" "$dir/repo/.prettierrc" commit_and_push_fixture .prettierrc - run_update_policy "$HASH" + run_update_policy expect_success expect_output "nothing to commit" expect_nothing_written } -test_bad_commit_hash() { - for hash in not-a-hash 0000000000000000000000000000000000000000; do - start_scenario "bad commit hash $hash stops with nothing written" - commit_and_push_fixture .gitignore .prettierrc REPO_POLICIES.md - run_update_policy "$hash" - expect_failure - expect_nothing_written - done - # The last hash has the right form, so it is the one that reaches curl. - expect_output "cannot fetch" +test_target_lacking_install_gets_both_scripts() { + start_scenario "a repo lacking script/install gets it, and the current script/update-policy, in the commit" + rm "$dir/repo/script/install" + echo "# An older version." >> "$dir/repo/script/update-policy" + commit_and_push_fixture .prettierrc + run_update_policy + expect_success + expect_same_as_canonical script/install + expect_same_as_canonical script/update-policy + expect_committed_executable script/install + expect_committed_executable script/update-policy + expect_new_commit_not_pushed + [ "$(files_in_commit)" = ".prettierrc script/install script/update-policy " ] || + fail "the commit holds other files: $(files_in_commit)" } -test_push_failure() { - start_scenario "push failure exits non-zero" +# The shell reads a script in blocks, so the padding makes it read the +# last line only after main has returned. It finds that line only if +# script/install left the old file in place under a new name. +test_update_policy_replaces_itself() { + start_scenario "script/update-policy replaces its own file and still exits 0" + { + cat "$dir/repo/script/update-policy" + i=0 + while [ "$i" -lt 2000 ]; do + echo "# Padding." + i=$((i + 1)) + done + echo "echo 'the old file ran to its end'" + } > "$dir/merged" + mv "$dir/merged" "$dir/repo/script/update-policy" + chmod 755 "$dir/repo/script/update-policy" + commit_and_push_fixture + run_update_policy + expect_success + expect_output "the old file ran to its end" + expect_same_as_canonical script/update-policy + expect_executable script/update-policy + expect_new_commit_not_pushed +} + +test_install_with_path_commits_nothing() { + start_scenario "script/install with a path writes into that clone and commits nothing" + rm "$dir/repo/script/install" "$dir/repo/script/update-policy" commit_and_push_fixture .prettierrc - reject_pushes - run_update_policy "$HASH" + run_install "$dir/repo" + expect_success + expect_file .prettierrc << 'EOF' +{ "tabWidth": 4 } +EOF + expect_same_as_canonical script/install + expect_same_as_canonical script/update-policy + expect_executable script/install + expect_executable script/update-policy + expect_no_commit + [ "$(head -n 1 "$dir/output")" = "$HASH" ] || + fail "the first line of output is not the hash" + expect_output "script/install" + [ -z "$(ls -A "$dir/tmp")" ] || fail "a temporary directory is left" +} + +test_install_without_argument_updates_own_repo() { + start_scenario "script/install with no argument updates the repo it sits in" + commit_and_push_fixture .prettierrc + status=0 + (cd "$dir" && PATH="$dir/bin:$PATH" repo/script/install) \ + > "$dir/output" 2>&1 || status=$? + expect_success + expect_file .prettierrc << 'EOF' +{ "tabWidth": 4 } +EOF + expect_no_commit +} + +test_install_bad_target() { + start_scenario "script/install refuses a path that is not the top of a clone" + commit_and_push_fixture .prettierrc + run_install "$dir/repo/script" expect_failure - expect_output "push failed" - [ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit" + expect_output "not the top of a git clone" + run_install "$dir/bin" + expect_failure + expect_output "not the top of a git clone" + run_install "$dir/no-such-directory" + expect_failure + expect_output "no such directory" + run_install "$dir/repo" extra + expect_failure + expect_output "usage:" + expect_nothing_written +} + +test_failed_fetch_stops() { + for url in \ + "/api/v1/repos/sneak/prompts/branches/main" \ + "/raw/branch/main/script/install" \ + "/compare/" \ + "/raw/commit/"; do + start_scenario "a failed fetch of $url stops with nothing written" + commit_and_push_fixture .gitignore .prettierrc REPO_POLICIES.md + FAIL_URL="$url" + run_update_policy + expect_failure + expect_output "cannot fetch" + expect_nothing_written + done +} + +test_one_missing_file_stops_all() { + start_scenario "one file missing from prompts stops before any file is written" + rm "$dir/canonical/prompts/REPO_POLICIES.md" + commit_and_push_fixture .prettierrc REPO_POLICIES.md + run_update_policy + expect_failure + expect_output "cannot fetch" + expect_nothing_written +} + +test_branch_response_without_hash_stops() { + start_scenario "a branch response with no commit hash stops" + commit_and_push_fixture .prettierrc + BAD_BRANCH_JSON=yes + run_update_policy + expect_failure + expect_output "no commit hash" + expect_nothing_written } # Each of these files, with the repo's own entries above the closing @@ -370,7 +537,7 @@ test_own_entry_above_closing_comment() { mv "$dir/merged" "$dir/repo/$file" commit_and_push_fixture .gitignore .dockerignore .editorconfig \ .prettierrc REPO_POLICIES.md - run_update_policy "$HASH" + run_update_policy expect_failure expect_output "$file has entries" expect_output "/myapp" @@ -380,44 +547,39 @@ test_own_entry_above_closing_comment() { done } -test_retry_after_failed_push() { - start_scenario "a retry after a failed push delivers the commit" - commit_and_push_fixture .prettierrc - reject_pushes - run_update_policy "$HASH" - expect_failure - committed="$(head_of_repo)" - [ "$committed" != "$before" ] || fail "no commit was made" - [ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit" - - accept_pushes - run_update_policy "$HASH" - expect_success - [ "$(head_of_repo)" = "$committed" ] || fail "the retry made a new commit" - [ "$(head_of_origin)" = "$committed" ] || - fail "the retry did not deliver the commit" -} - test_uncommitted_change_stops() { start_scenario "a carried file with uncommitted changes stops" commit_and_push_fixture .prettierrc REPO_POLICIES.md echo "uncommitted" >> "$dir/repo/REPO_POLICIES.md" - run_update_policy "$HASH" + run_update_policy expect_failure expect_output "REPO_POLICIES.md has uncommitted changes" expect_file REPO_POLICIES.md << 'EOF' old policy uncommitted EOF - [ "$(head_of_repo)" = "$before" ] || fail "the repo has a new commit" - [ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit" + expect_no_commit +} + +test_untracked_script_stops() { + start_scenario "an untracked script/install is not overwritten" + rm "$dir/repo/script/install" + commit_and_push_fixture .prettierrc + echo "our own script" > "$dir/repo/script/install" + run_install "$dir/repo" + expect_failure + expect_output "script/install has uncommitted changes" + expect_file script/install << 'EOF' +our own script +EOF + expect_no_commit } test_gitignore_lacks_canonical_last_line() { start_scenario ".gitignore lacking the canonical last line stops" printf '*.log\n/myapp\n' > "$dir/repo/.gitignore" commit_and_push_fixture .gitignore .prettierrc - run_update_policy "$HASH" + run_update_policy expect_failure expect_output ".gitignore lacks this line" expect_nothing_written @@ -438,7 +600,7 @@ linters: - pkg: example.com/other EOF commit_and_push_fixture .golangci.yml .prettierrc - run_update_policy "$HASH" + run_update_policy expect_failure expect_output "no deny list in its test-support rule" expect_nothing_written @@ -453,7 +615,7 @@ test_own_golangci_setting_stops() { - pattern: model.Operator EOF commit_and_push_fixture .golangci.yml .prettierrc - run_update_policy "$HASH" + run_update_policy expect_failure expect_output ".golangci.yml has lines" expect_output "forbidigo:" @@ -464,9 +626,10 @@ EOF test_source_repo_stops() { start_scenario "the repo the policy files come from stops" - printf '#!/bin/sh\necho prompts\n' > "$dir/repo/script/projectname" - commit_and_push_fixture .prettierrc REPO_POLICIES.md - run_update_policy "$HASH" + mkdir "$dir/repo/prompts" + echo "policy" > "$dir/repo/prompts/REPO_POLICIES.md" + commit_and_push_fixture .prettierrc prompts/REPO_POLICIES.md + run_update_policy expect_failure expect_output "this repo is the source of the policy files" expect_nothing_written @@ -477,42 +640,26 @@ test_staged_file_stays_out_of_commit() { commit_and_push_fixture .prettierrc echo "notes" > "$dir/repo/notes.txt" git -C "$dir/repo" add notes.txt - run_update_policy "$HASH" + run_update_policy expect_success - [ "$(git -C "$dir/repo" show --name-only --format= HEAD)" = ".prettierrc" ] || + [ "$(files_in_commit)" = ".prettierrc " ] || fail "the commit holds more than the policy file" [ "$(git -C "$dir/repo" diff --cached --name-only)" = "notes.txt" ] || fail "notes.txt is no longer staged" } -test_short_commit_hash_refused() { - start_scenario "a hexadecimal hash shorter than 40 characters is refused" +test_update_policy_takes_no_argument() { + start_scenario "script/update-policy refuses an argument" commit_and_push_fixture .prettierrc - run_update_policy "${HASH%?}" + run_update_policy "$HASH" expect_failure - expect_output "not a full commit hash" + expect_output "usage:" expect_nothing_written } -# git asks the local canonical repository for the head of main, and the -# stub curl serves only that commit. -test_no_hash_uses_head_of_main() { - start_scenario "with no hash the script uses the head of main" - commit_and_push_fixture .prettierrc - run_update_policy - expect_success - expect_file .prettierrc << 'EOF' -{ "tabWidth": 4 } -EOF - git -C "$dir/repo" log -1 --format=%s | grep -q -F -e "$HASH" || - fail "the commit message does not name the head of main" - [ "$(head_of_origin)" = "$(head_of_repo)" ] || - fail "the commit was not pushed" -} - # expect_former_canonical_line_replaced : an earlier # canonical commit held the line and the newest does not. This repo's -# file still has it, and the script replaces it and pushes. +# file still has it, and the script replaces it. expect_former_canonical_line_replaced() { cp "$dir/canonical/$1" "$dir/newest" { echo "$2"; cat "$dir/newest"; } > "$dir/canonical/$1" @@ -523,14 +670,12 @@ expect_former_canonical_line_replaced() { mv "$dir/merged" "$dir/repo/$1" commit_and_push_fixture "$1" .prettierrc - run_update_policy "$HASH" + run_update_policy expect_success if grep -q -F -x -e "$2" "$dir/repo/$1"; then fail "$1 still has: $2" fi - [ "$(head_of_repo)" != "$before" ] || fail "no commit was made" - [ "$(head_of_origin)" = "$(head_of_repo)" ] || - fail "the commit was not pushed" + expect_new_commit_not_pushed } test_former_canonical_golangci_line_replaced() { @@ -543,24 +688,49 @@ test_former_canonical_gitignore_line_replaced() { expect_former_canonical_line_replaced .gitignore "*.old" } +# A line another file's history removed does not count as held by this +# one. +test_line_removed_from_another_file_still_stops() { + start_scenario "a line only another canonical file once held stops" + { echo "*.old"; cat "$dir/canonical/.dockerignore"; } > "$dir/merged" + cp "$dir/merged" "$dir/canonical/.dockerignore" + commit_canonical + tail -n +2 "$dir/merged" > "$dir/canonical/.dockerignore" + commit_canonical + { echo "*.old"; cat "$dir/repo/.gitignore"; } > "$dir/merged" + mv "$dir/merged" "$dir/repo/.gitignore" + commit_and_push_fixture .gitignore .prettierrc + run_update_policy + expect_failure + expect_output ".gitignore has entries" + expect_output "*.old" + expect_nothing_written +} + main() { test_own_entries_kept test_files_not_carried_are_skipped test_nothing_changed - test_bad_commit_hash - test_push_failure + test_target_lacking_install_gets_both_scripts + test_update_policy_replaces_itself + test_install_with_path_commits_nothing + test_install_without_argument_updates_own_repo + test_install_bad_target + test_failed_fetch_stops + test_one_missing_file_stops_all + test_branch_response_without_hash_stops test_own_entry_above_closing_comment - test_retry_after_failed_push test_uncommitted_change_stops + test_untracked_script_stops test_gitignore_lacks_canonical_last_line test_fetched_golangci_without_deny_list test_own_golangci_setting_stops test_source_repo_stops test_staged_file_stays_out_of_commit - test_short_commit_hash_refused - test_no_hash_uses_head_of_main + test_update_policy_takes_no_argument test_former_canonical_golangci_line_replaced test_former_canonical_gitignore_line_replaced + test_line_removed_from_another_file_still_stops echo "update-policy tests passed" } -- 2.54.0 From f4275b9ac0720fe45d72ad95c49096a11da1df40 Mon Sep 17 00:00:00 2001 From: clawbot Date: Mon, 9 Nov 2026 18:49:16 +0000 Subject: [PATCH 02/12] script/install: stop on a repo's own script of the same name script/install replaced any tracked script/install or script/update-policy in the target, including a repo's own unrelated script. It now stops when such a file has a line the canonical file has never held. An older shared copy is still replaced. The padded file in the self-replacement test is now an earlier canonical version, so the new stop does not catch it. Model: opus-5-5 --- prompts/EXISTING_REPO_CHECKLIST.md | 3 ++- script/install | 16 ++++++++++++++++ test/update-policy.sh | 26 +++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index 286cfcf..92d68e1 100644 --- a/prompts/EXISTING_REPO_CHECKLIST.md +++ b/prompts/EXISTING_REPO_CHECKLIST.md @@ -221,7 +221,8 @@ with your task. anything, naming the file and the entry. It stops the same way on a line in `.golangci.yml` that the canonical file has never held, apart from comments and the repo's entries in the `deny` list of the `test-support` - rule. + rule. It also stops on a repo's own `script/install` or + `script/update-policy`. - [ ] README has an **Entrypoints** section documenting the `script/` entrypoints and linking the standard - [ ] `script/lint` and `script/test` each run diff --git a/script/install b/script/install index 788219f..d7667db 100755 --- a/script/install +++ b/script/install @@ -174,6 +174,19 @@ $own" fi } +# stop_on_own_script : stop if the local script has a +# line the canonical file has never held, unless it is blank or a +# comment: the repo wrote that script itself, and replacing it would +# delete it. A repo without the script is fine. +stop_on_own_script() { + [ -e "$2" ] || return 0 + own="$(local_only_lines "$1" "$2")" + if [ -n "$own" ]; then + die "$2 has lines the canonical file has never held; rename the repo's own script to keep it: +$own" + fi +} + main() { [ "$#" -le 1 ] || die "usage: script/install [path of a repo clone]" @@ -221,6 +234,9 @@ main() { .golangci.yml) keep_deny_entries "$tmp/$path" "$path" ;; + script/*) + stop_on_own_script "$tmp/$path" "$path" + ;; esac done diff --git a/test/update-policy.sh b/test/update-policy.sh index 70e1094..c847283 100755 --- a/test/update-policy.sh +++ b/test/update-policy.sh @@ -415,7 +415,8 @@ test_target_lacking_install_gets_both_scripts() { # The shell reads a script in blocks, so the padding makes it read the # last line only after main has returned. It finds that line only if -# script/install left the old file in place under a new name. +# script/install left the old file in place under a new name. The old +# file is an earlier canonical version, so script/install replaces it. test_update_policy_replaces_itself() { start_scenario "script/update-policy replaces its own file and still exits 0" { @@ -427,6 +428,10 @@ test_update_policy_replaces_itself() { done echo "echo 'the old file ran to its end'" } > "$dir/merged" + cp "$dir/merged" "$dir/canonical/script/update-policy" + commit_canonical + cp "$UPDATE_POLICY" "$dir/canonical/script/update-policy" + commit_canonical mv "$dir/merged" "$dir/repo/script/update-policy" chmod 755 "$dir/repo/script/update-policy" commit_and_push_fixture @@ -575,6 +580,17 @@ EOF expect_no_commit } +test_own_script_stops() { + start_scenario "a tracked script/install with a line the canonical file has never held stops" + echo "echo our own installer" >> "$dir/repo/script/install" + commit_and_push_fixture .prettierrc + run_update_policy + expect_failure + expect_output "script/install has lines" + expect_output "echo our own installer" + expect_nothing_written +} + test_gitignore_lacks_canonical_last_line() { start_scenario ".gitignore lacking the canonical last line stops" printf '*.log\n/myapp\n' > "$dir/repo/.gitignore" @@ -688,6 +704,12 @@ test_former_canonical_gitignore_line_replaced() { expect_former_canonical_line_replaced .gitignore "*.old" } +test_former_canonical_script_line_replaced() { + start_scenario "a script/install line an earlier canonical commit held is replaced" + expect_former_canonical_line_replaced script/install "echo an older install" + expect_same_as_canonical script/install +} + # A line another file's history removed does not count as held by this # one. test_line_removed_from_another_file_still_stops() { @@ -722,6 +744,7 @@ main() { test_own_entry_above_closing_comment test_uncommitted_change_stops test_untracked_script_stops + test_own_script_stops test_gitignore_lacks_canonical_last_line test_fetched_golangci_without_deny_list test_own_golangci_setting_stops @@ -730,6 +753,7 @@ main() { test_update_policy_takes_no_argument test_former_canonical_golangci_line_replaced test_former_canonical_gitignore_line_replaced + test_former_canonical_script_line_replaced test_line_removed_from_another_file_still_stops echo "update-policy tests passed" } -- 2.54.0 From 76b75fa79f7a8d3d99d906aa36454ca460158170 Mon Sep 17 00:00:00 2001 From: clawbot Date: Mon, 9 Nov 2026 19:38:41 +0000 Subject: [PATCH 03/12] script/install: write every listed file the repo lacks A listed file the repo lacks is now written whole, with no keep rule. A prompts/NAME.md goes to docs/NAME.md unless the repo keeps it at NAME.md, and any other file goes to its own path. A language's files are skipped when git tracks no source file of that language. A listed path git ignores now stops the script before it writes. README bullets split into short sentences, and a false sentence about template repos deleted. Model: opus-5-5 --- README.md | 8 +-- prompts/REPO_POLICIES.md | 14 +++-- script/install | 62 +++++++++++++------ test/update-policy.sh | 127 ++++++++++++++++++++++++++++++++++----- 4 files changed, 166 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index e35efd5..3e85810 100644 --- a/README.md +++ b/README.md @@ -142,11 +142,11 @@ alpine. We provide: - `script/install-precommit` — installs the git pre-commit hook (our own extension); `make hooks` shims to it - `script/install` — fetch the vendored policy files from this repo over HTTP - and write them into the repo given as its argument, or its own repo with no - argument (our own extension); it stops when run here + and write them into a repo (our own extension). It writes into the repo given + as its argument, or its own repo with no argument. It stops when run here. - `script/update-policy` — fetch the current `script/install`, run it on its own - repo and commit the result without pushing (our own extension); it is for the - repos that vendor the policy files and stops when run here + repo and commit the result without pushing (our own extension). It is for the + repos that vendor the policy files. It stops when run here. `make hooks` installs the pre-commit hook that runs `script/precommit`. diff --git a/prompts/REPO_POLICIES.md b/prompts/REPO_POLICIES.md index 0a1a783..8bdaeb3 100644 --- a/prompts/REPO_POLICIES.md +++ b/prompts/REPO_POLICIES.md @@ -943,12 +943,14 @@ style conventions are in separate documents: the files that changed and does not push, so a person reviews and pushes the commit. - `script/install` fetches each vendored file the repo carries from `prompts` - over HTTP. It also installs `script/install` and `script/update-policy` in - the repo, so a repo updated once can update itself. Every repo made from a - template repo already has both. The list of vendored files is at the top of - `script/install`. The script replaces each file and keeps only the repo's - own entries: + `script/install` fetches each vendored file from `prompts` over HTTP and + writes it into the repo. It skips a language's files when git tracks no + source file of that language. A file the repo lacks is written whole. A + `prompts/NAME.md` goes to `docs/NAME.md`, unless the repo keeps it at + `NAME.md`. It also installs `script/install` and `script/update-policy` in + the repo, so a repo updated once can update itself. The list of vendored + files is at the top of `script/install`. The script replaces each file and + keeps only the repo's own entries: - in `.gitignore`, `.editorconfig` and `.dockerignore`: the lines below the comment that ends the canonical file - in `.golangci.yml`: the entries the repo added to the `deny` list of the diff --git a/script/install b/script/install index d7667db..331be19 100755 --- a/script/install +++ b/script/install @@ -24,8 +24,9 @@ PROMPTS_URL="https://git.eeqj.de/sneak/prompts" PROMPTS_API_URL="https://git.eeqj.de/api/v1/repos/sneak/prompts" PROMPTS_FIRST_COMMIT="af257bef63f162a681bddc80d9b941c47a44b748" -# Paths in the prompts repo. A file the target repo does not carry is -# skipped, except the two scripts, which every repo gets. +# Paths in the prompts repo. Every repo gets each file, written whole if +# the repo lacks it. A language's files are skipped when git tracks no +# source file of that language. VENDORED_FILES=" .dockerignore .editorconfig @@ -58,15 +59,36 @@ fetch() { die "cannot fetch $1" } -# local_path : print where the target repo keeps the -# file, or nothing if git does not track it there. A prompts/NAME.md -# lands at docs/NAME.md or NAME.md. +# file_applies : fail if the file is for a language of +# which git tracks no source file in the target repo. +file_applies() { + case "$1" in + .golangci.yml | prompts/CODE_STYLEGUIDE_GO.md | \ + prompts/GO_HTTP_SERVER_CONVENTIONS.md) + [ -n "$(git ls-files -- '*.go')" ] + ;; + prompts/CODE_STYLEGUIDE_JS.md) + [ -n "$(git ls-files -- '*.js' '*.mjs' '*.ts')" ] + ;; + prompts/CODE_STYLEGUIDE_PYTHON.md) + [ -n "$(git ls-files -- '*.py')" ] + ;; + esac +} + +# local_path : print where the file goes in the target +# repo. A prompts/NAME.md stays at NAME.md if git tracks it there and not +# at docs/NAME.md; otherwise it goes to docs/NAME.md. Any other file goes +# to its own path. local_path() { name="${1#prompts/}" - if [ "$name" != "$1" ] && [ -n "$(git ls-files -- "docs/$name")" ]; then - echo "docs/$name" - elif [ -n "$(git ls-files -- "$name")" ]; then + if [ "$name" = "$1" ]; then + echo "$1" + elif [ -z "$(git ls-files -- "docs/$name")" ] && + [ -n "$(git ls-files -- "$name")" ]; then echo "$name" + else + echo "docs/$name" fi } @@ -177,9 +199,8 @@ $own" # stop_on_own_script : stop if the local script has a # line the canonical file has never held, unless it is blank or a # comment: the repo wrote that script itself, and replacing it would -# delete it. A repo without the script is fine. +# delete it. stop_on_own_script() { - [ -e "$2" ] || return 0 own="$(local_only_lines "$1" "$2")" if [ -n "$own" ]; then die "$2 has lines the canonical file has never held; rename the repo's own script to keep it: @@ -210,23 +231,24 @@ main() { fetch "$PROMPTS_URL/compare/$PROMPTS_FIRST_COMMIT...$commit.patch" \ "$tmp/history.patch" - carried="" + to_write="" for file in $VENDORED_FILES; do - case "$file" in - script/*) path="$file" ;; - *) path="$(local_path "$file")" ;; - esac - [ -n "$path" ] || continue + file_applies "$file" || continue + path="$(local_path "$file")" + if git check-ignore -q -- "$path"; then + die "$path is ignored by git; stop ignoring it" + fi if [ -n "$(git status --porcelain -- "$path")" ]; then die "$path has uncommitted changes" fi mkdir -p "$tmp/$(dirname "$path")" fetch "$PROMPTS_URL/raw/commit/$commit/$file" "$tmp/$path" former_lines "$tmp/history.patch" "$file" > "$tmp/$path.former" - carried="$carried $path" + to_write="$to_write $path" done - for path in $carried; do + for path in $to_write; do + [ -e "$path" ] || continue case "$path" in .gitignore | .editorconfig | .dockerignore) keep_lines_below_last "$tmp/$path" "$path" @@ -241,12 +263,12 @@ main() { done echo "$commit" - for path in $carried; do + for path in $to_write; do + mkdir -p "$(dirname "$path")" case "$path" in script/*) # A new name and mv, because script/update-policy may # be the script running. - mkdir -p script cp "$tmp/$path" "$path.new" chmod 755 "$path.new" mv "$path.new" "$path" diff --git a/test/update-policy.sh b/test/update-policy.sh index c847283..9d0451f 100755 --- a/test/update-policy.sh +++ b/test/update-policy.sh @@ -117,13 +117,20 @@ EOF echo '{ "tabWidth": 4 }' > "$dir/canonical/.prettierrc" echo "new policy" > "$dir/canonical/prompts/REPO_POLICIES.md" echo "new api policy" > "$dir/canonical/prompts/API_POLICIES.md" + for name in CODE_STYLEGUIDE CODE_STYLEGUIDE_GO CODE_STYLEGUIDE_JS \ + CODE_STYLEGUIDE_PYTHON EXISTING_REPO_CHECKLIST \ + GO_HTTP_SERVER_CONVENTIONS LLM_PROSE_TELLS NEW_REPO_CHECKLIST; do + echo "new $name" > "$dir/canonical/prompts/$name.md" + done + mkdir -p "$dir/canonical/.gitea/workflows" + echo "name: check" > "$dir/canonical/.gitea/workflows/check.yml" } # commit_canonical: commit the canonical files as they stand. The stub # curl serves only the newest commit; every other commit gets a 404. commit_canonical() { git -C "$dir/canonical" add .gitignore .dockerignore .editorconfig \ - .golangci.yml .prettierrc prompts script + .golangci.yml .prettierrc .gitea prompts script git -C "$dir/canonical" commit -q -m "Canonical" HASH="$(git -C "$dir/canonical" rev-parse HEAD)" } @@ -174,7 +181,8 @@ EOF } # start_scenario : make a new temp directory holding a repo with -# the outdated files, not yet committed, and an empty origin. +# a Go source file and the outdated files, not yet committed, and an +# empty origin. start_scenario() { scenario="$1" rm -rf "$dir" @@ -203,18 +211,46 @@ start_scenario() { cp "$INSTALL" "$UPDATE_POLICY" "$dir/repo/script/" printf '#!/bin/sh\necho example\n' > "$dir/repo/script/projectname" chmod +x "$dir/repo/script/projectname" + mkdir -p "$dir/repo/cmd/app" + echo "package main" > "$dir/repo/cmd/app/main.go" write_outdated_files } -# commit_and_push_fixture : commit those paths, remove the -# rest of the outdated files, and record the commit as $before. +# commit_and_push_fixture : commit those paths as they stand +# and every other shared file as the canonical file has it, so the +# script changes only those paths. Record the commit as $before. commit_and_push_fixture() { ( cd "$dir/repo" - git add script "$@" + git add script cmd "$@" + git clean -q -f -x + for file in $(git -C "$dir/canonical" ls-files); do + case "$file" in + script/*) continue ;; + prompts/REPO_POLICIES.md) path="REPO_POLICIES.md" ;; + prompts/*) path="docs/${file#prompts/}" ;; + *) path="$file" ;; + esac + if [ -z "$(git ls-files -- "$path")" ]; then + mkdir -p "$(dirname "$path")" + cp "$dir/canonical/$file" "$path" + git add "$path" + fi + done git commit -q -m "Fixture" git push -q origin HEAD - git clean -q -f -x + ) + before="$(git -C "$dir/repo" rev-parse HEAD)" +} + +# remove_from_fixture : delete those paths from the repo and +# its origin, and record the commit as $before. +remove_from_fixture() { + ( + cd "$dir/repo" + git rm -q -r "$@" + git commit -q -m "Remove from fixture" + git push -q origin HEAD ) before="$(git -C "$dir/repo" rev-parse HEAD)" } @@ -365,25 +401,55 @@ EOF [ -z "$(ls -A "$dir/tmp")" ] || fail "a temporary directory is left" } -test_files_not_carried_are_skipped() { - start_scenario "files the repo does not carry skipped" +test_files_the_repo_lacks_are_written_whole() { + start_scenario "files the repo lacks are written whole" mkdir "$dir/repo/docs" mv "$dir/repo/REPO_POLICIES.md" "$dir/repo/docs/REPO_POLICIES.md" - commit_and_push_fixture docs/REPO_POLICIES.md + echo "print()" > "$dir/repo/tool.py" + echo "export {}" > "$dir/repo/web.ts" + commit_and_push_fixture docs/REPO_POLICIES.md tool.py web.ts + remove_from_fixture REPO_POLICIES.md .gitignore .golangci.yml .gitea \ + docs/API_POLICIES.md docs/CODE_STYLEGUIDE_GO.md \ + docs/CODE_STYLEGUIDE_JS.md docs/CODE_STYLEGUIDE_PYTHON.md run_update_policy expect_success + expect_same_as_canonical .gitignore + expect_same_as_canonical .golangci.yml + expect_same_as_canonical .gitea/workflows/check.yml expect_file docs/REPO_POLICIES.md << 'EOF' new policy EOF expect_absent REPO_POLICIES.md + expect_file docs/API_POLICIES.md << 'EOF' +new api policy +EOF expect_absent API_POLICIES.md - expect_absent docs/API_POLICIES.md - expect_absent .gitignore - expect_absent .dockerignore - expect_absent .editorconfig + expect_file docs/CODE_STYLEGUIDE_GO.md << 'EOF' +new CODE_STYLEGUIDE_GO +EOF + expect_file docs/CODE_STYLEGUIDE_JS.md << 'EOF' +new CODE_STYLEGUIDE_JS +EOF + expect_file docs/CODE_STYLEGUIDE_PYTHON.md << 'EOF' +new CODE_STYLEGUIDE_PYTHON +EOF + expect_new_commit_not_pushed +} + +test_language_files_need_a_source_file() { + start_scenario "a repo with no source file of a language gets none of its files" + commit_and_push_fixture .prettierrc + remove_from_fixture cmd .golangci.yml docs/CODE_STYLEGUIDE_GO.md \ + docs/GO_HTTP_SERVER_CONVENTIONS.md docs/CODE_STYLEGUIDE_JS.md \ + docs/CODE_STYLEGUIDE_PYTHON.md + run_update_policy + expect_success expect_absent .golangci.yml - expect_absent .prettierrc + expect_absent docs/CODE_STYLEGUIDE_GO.md + expect_absent docs/GO_HTTP_SERVER_CONVENTIONS.md + expect_absent docs/CODE_STYLEGUIDE_JS.md + expect_absent docs/CODE_STYLEGUIDE_PYTHON.md expect_new_commit_not_pushed } @@ -463,6 +529,19 @@ EOF [ -z "$(ls -A "$dir/tmp")" ] || fail "a temporary directory is left" } +test_install_without_script_directory() { + start_scenario "script/install with a path writes both scripts into a clone with no script directory" + commit_and_push_fixture .prettierrc + remove_from_fixture script + run_install "$dir/repo" + expect_success + expect_same_as_canonical script/install + expect_same_as_canonical script/update-policy + expect_executable script/install + expect_executable script/update-policy + expect_no_commit +} + test_install_without_argument_updates_own_repo() { start_scenario "script/install with no argument updates the repo it sits in" commit_and_push_fixture .prettierrc @@ -580,6 +659,21 @@ EOF expect_no_commit } +test_ignored_file_stops() { + start_scenario "an ignored file at a listed path is not overwritten" + commit_and_push_fixture .prettierrc + remove_from_fixture docs/API_POLICIES.md + echo "docs/API_POLICIES.md" >> "$dir/repo/.git/info/exclude" + echo "our own notes" > "$dir/repo/docs/API_POLICIES.md" + run_install "$dir/repo" + expect_failure + expect_output "docs/API_POLICIES.md is ignored by git" + expect_file docs/API_POLICIES.md << 'EOF' +our own notes +EOF + expect_no_commit +} + test_own_script_stops() { start_scenario "a tracked script/install with a line the canonical file has never held stops" echo "echo our own installer" >> "$dir/repo/script/install" @@ -731,11 +825,13 @@ test_line_removed_from_another_file_still_stops() { main() { test_own_entries_kept - test_files_not_carried_are_skipped + test_files_the_repo_lacks_are_written_whole + test_language_files_need_a_source_file test_nothing_changed test_target_lacking_install_gets_both_scripts test_update_policy_replaces_itself test_install_with_path_commits_nothing + test_install_without_script_directory test_install_without_argument_updates_own_repo test_install_bad_target test_failed_fetch_stops @@ -744,6 +840,7 @@ main() { test_own_entry_above_closing_comment test_uncommitted_change_stops test_untracked_script_stops + test_ignored_file_stops test_own_script_stops test_gitignore_lacks_canonical_last_line test_fetched_golangci_without_deny_list -- 2.54.0 From e89fc86adc3a87bfb305eeb7c681f8f4051cd67d Mon Sep 17 00:00:00 2001 From: clawbot Date: Mon, 9 Nov 2026 20:04:01 +0000 Subject: [PATCH 04/12] Drop make install from the required targets; test three more stops The install target stays in this repo's Makefile only. sneak/vaultik already has an install target with another meaning, and every repo still gets script/install. Tests: a repo's own script/update-policy stops the script, and a repo whose only JavaScript source is a .js or .mjs file gets the JavaScript style guide. Model: opus-5-5 --- prompts/EXISTING_REPO_CHECKLIST.md | 4 ++-- prompts/NEW_REPO_CHECKLIST.md | 5 ++--- prompts/REPO_POLICIES.md | 3 +-- test/update-policy.sh | 34 +++++++++++++++++++++++------- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index 92d68e1..f62e99a 100644 --- a/prompts/EXISTING_REPO_CHECKLIST.md +++ b/prompts/EXISTING_REPO_CHECKLIST.md @@ -205,14 +205,14 @@ with your task. - [ ] `Makefile` exists in root — reference `https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile` - [ ] Has targets: `test`, `lint`, `fmt`, `fmt-check`, `check`, `docker`, - `hooks`, `install`, `update-policy` + `hooks`, `update-policy` - [ ] Target implementations live in `script/` (scripts-to-rule-them-all); Makefile targets are thin shims calling them — model scripts at `https://git.eeqj.de/sneak/prompts/raw/branch/main/script/` - [ ] `script/precommit` exists and the pre-commit hook (installed by `script/install-precommit`, shimmed by `make hooks`) runs it - [ ] `script/install` and `script/update-policy` exist, identical to the model - scripts, and `make install` and `make update-policy` shim to them. + scripts, and `make update-policy` shims to `script/update-policy`. `make update-policy` commits without pushing, so a person reviews and pushes the commit. Before its first use, the repo's own entries in `.gitignore`, `.editorconfig` and `.dockerignore` go below the comment diff --git a/prompts/NEW_REPO_CHECKLIST.md b/prompts/NEW_REPO_CHECKLIST.md index cc00df2..c5cd5a3 100644 --- a/prompts/NEW_REPO_CHECKLIST.md +++ b/prompts/NEW_REPO_CHECKLIST.md @@ -266,9 +266,8 @@ are thin shims calling them. Model scripts: - [ ] `script/install-precommit` — installs the pre-commit hook that runs `script/precommit` - [ ] `make hooks` — shims to `script/install-precommit` -- [ ] `script/install` / `make install` — fetches the vendored policy files from - the `prompts` repo over HTTP and writes them into the repo; identical - across repos +- [ ] `script/install` — fetches the vendored policy files from the `prompts` + repo over HTTP and writes them into the repo; identical across repos - [ ] `script/update-policy` / `make update-policy` — fetches the current `script/install`, runs it on the repo and commits the result without pushing; identical across repos diff --git a/prompts/REPO_POLICIES.md b/prompts/REPO_POLICIES.md index 8bdaeb3..ae433e1 100644 --- a/prompts/REPO_POLICIES.md +++ b/prompts/REPO_POLICIES.md @@ -93,7 +93,6 @@ style conventions are in separate documents: - `make check` (runs `test`, `lint`, `fmt-check`) - `make docker` - `make hooks` (installs pre-commit hook) - - `make install` (writes the current policy files into the repo) - `make update-policy` (brings the repo up to standards and commits) A model Makefile is at @@ -194,7 +193,7 @@ style conventions are in separate documents: `make hooks` target shims to it. - `script/install` fetches the vendored policy files from the `prompts` repo over HTTP and writes them into the repo it is given, or its own repo with - no argument. The `make install` target shims to it. + no argument. - `script/update-policy` fetches the current `script/install` from the `prompts` repo, runs it on its own repo and commits the result. It does not push. The `make update-policy` target shims to it. diff --git a/test/update-policy.sh b/test/update-policy.sh index 9d0451f..8a1ac08 100755 --- a/test/update-policy.sh +++ b/test/update-policy.sh @@ -453,6 +453,21 @@ test_language_files_need_a_source_file() { expect_new_commit_not_pushed } +test_javascript_source_brings_its_styleguide() { + for extension in js mjs; do + start_scenario "a repo whose only JavaScript source is a .$extension file gets the JavaScript style guide" + echo "export {}" > "$dir/repo/web.$extension" + commit_and_push_fixture "web.$extension" + remove_from_fixture docs/CODE_STYLEGUIDE_JS.md + run_update_policy + expect_success + expect_file docs/CODE_STYLEGUIDE_JS.md << 'EOF' +new CODE_STYLEGUIDE_JS +EOF + expect_new_commit_not_pushed + done +} + test_nothing_changed() { start_scenario "nothing changed" cp "$dir/canonical/.prettierrc" "$dir/repo/.prettierrc" @@ -675,14 +690,16 @@ EOF } test_own_script_stops() { - start_scenario "a tracked script/install with a line the canonical file has never held stops" - echo "echo our own installer" >> "$dir/repo/script/install" - commit_and_push_fixture .prettierrc - run_update_policy - expect_failure - expect_output "script/install has lines" - expect_output "echo our own installer" - expect_nothing_written + for script in script/install script/update-policy; do + start_scenario "a tracked $script with a line the canonical file has never held stops" + echo "echo our own script" >> "$dir/repo/$script" + commit_and_push_fixture .prettierrc + run_update_policy + expect_failure + expect_output "$script has lines" + expect_output "echo our own script" + expect_nothing_written + done } test_gitignore_lacks_canonical_last_line() { @@ -827,6 +844,7 @@ main() { test_own_entries_kept test_files_the_repo_lacks_are_written_whole test_language_files_need_a_source_file + test_javascript_source_brings_its_styleguide test_nothing_changed test_target_lacking_install_gets_both_scripts test_update_policy_replaces_itself -- 2.54.0 From 9f83f3c1b14f61527e0a61fb957b74ab7f5917b4 Mon Sep 17 00:00:00 2001 From: clawbot Date: Mon, 9 Nov 2026 20:46:27 +0000 Subject: [PATCH 05/12] README: script/install stops when its target is this repo Started from this repo with a path, the script writes into that clone, so "stops when run here" was false. Model: opus-5-5 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e85810..b4cffa7 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,8 @@ alpine. We provide: extension); `make hooks` shims to it - `script/install` — fetch the vendored policy files from this repo over HTTP and write them into a repo (our own extension). It writes into the repo given - as its argument, or its own repo with no argument. It stops when run here. + as its argument, or its own repo with no argument. It stops when its target is + this repo. - `script/update-policy` — fetch the current `script/install`, run it on its own repo and commit the result without pushing (our own extension). It is for the repos that vendor the policy files. It stops when run here. -- 2.54.0 From 8a25e502f4dc3c1a7632d23f92bad44830d5d082 Mon Sep 17 00:00:00 2001 From: clawbot Date: Mon, 9 Nov 2026 21:34:29 +0000 Subject: [PATCH 06/12] script/install: do not stop on a missing closing comment A .gitignore or .dockerignore lacking the canonical last line no longer stops the script: each line the canonical file has never held goes below the fetched content. An .editorconfig lacking it is replaced, unless it has such a line, which stops the script. The stub curl now writes files with no execute bit, as a real download does, so the tests exercise the mode the script sets. Model: opus-5-5 --- prompts/EXISTING_REPO_CHECKLIST.md | 21 ++++++++++-------- script/install | 21 ++++++++++++++++-- test/update-policy.sh | 35 ++++++++++++++++++++++++++---- 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index f62e99a..fbc2214 100644 --- a/prompts/EXISTING_REPO_CHECKLIST.md +++ b/prompts/EXISTING_REPO_CHECKLIST.md @@ -214,15 +214,18 @@ with your task. - [ ] `script/install` and `script/update-policy` exist, identical to the model scripts, and `make update-policy` shims to `script/update-policy`. `make update-policy` commits without pushing, so a person reviews and - pushes the commit. Before its first use, the repo's own entries in - `.gitignore`, `.editorconfig` and `.dockerignore` go below the comment - that ends each canonical file. An entry above that comment which the - canonical file has never held makes the script stop before it writes - anything, naming the file and the entry. It stops the same way on a line - in `.golangci.yml` that the canonical file has never held, apart from - comments and the repo's entries in the `deny` list of the `test-support` - rule. It also stops on a repo's own `script/install` or - `script/update-policy`. + pushes the commit. A `.gitignore` or `.dockerignore` without the comment + that ends the canonical file needs no preparation. The script moves each + line the canonical file has never held to below the canonical content. + Blank lines and comments are dropped. Before its first use, add that + comment to an `.editorconfig` that has lines of its own, and put those + lines below it. Otherwise the script stops before it writes anything, + naming the file and each line. In a file that has the comment, an entry + above it which the canonical file has never held also stops the script. It + stops the same way on a line in `.golangci.yml` that the canonical file + has never held, apart from comments and the repo's entries in the `deny` + list of the `test-support` rule. It also stops on a repo's own + `script/install` or `script/update-policy`. - [ ] README has an **Entrypoints** section documenting the `script/` entrypoints and linking the standard - [ ] `script/lint` and `script/test` each run diff --git a/script/install b/script/install index 331be19..842f727 100755 --- a/script/install +++ b/script/install @@ -15,6 +15,12 @@ # - .gitignore, .editorconfig, .dockerignore: the lines below the # canonical file's last line. An entry above that line which the # canonical file has never held stops the script. +# - .gitignore, .dockerignore lacking that last line: each line the +# canonical file has never held, unless it is blank or a comment, +# goes below the canonical content. +# - .editorconfig lacking that last line: it is replaced if it has no +# such line. Otherwise the script stops, because a setting means +# nothing away from its section. # - .golangci.yml: the entries the repo added to the deny list of the # test-support rule. Any other line the canonical file has never # held stops the script, unless it is blank or a comment. @@ -117,12 +123,23 @@ local_only_lines() { # keep_lines_below_last : append to the fetched file # whatever follows its last line in the local file. Stop if the canonical # file has never held a line above that one, unless it is blank or a -# comment: replacing the file would delete it. +# comment: replacing the file would delete it. If the local file lacks +# that last line, append each line the canonical file has never held, +# unless it is blank or a comment. An .editorconfig with such a line +# stops the script instead, because a setting means nothing away from +# its section. keep_lines_below_last() { last="$(tail -n 1 "$1")" number="$(grep -n -F -x -- "$last" "$2" | head -n 1 | cut -d: -f1)" if [ -z "$last" ] || [ -z "$number" ]; then - die "$2 lacks this line; add it and put the repo's own entries below it: $last" + own="$(local_only_lines "$1" "$2")" + [ -n "$own" ] || return 0 + if [ "$2" = .editorconfig ]; then + die "$2 lacks this line and has lines the canonical file has never held; add it and put those lines below it: $last +$own" + fi + printf '%s\n' "$own" >> "$1" + return 0 fi own="$(local_only_lines "$1" "$2" "$number")" if [ -n "$own" ]; then diff --git a/test/update-policy.sh b/test/update-policy.sh index 8a1ac08..4c1c144 100755 --- a/test/update-policy.sh +++ b/test/update-policy.sh @@ -45,7 +45,7 @@ not_found() { serve_file() { [ -f "$canonical/$1" ] || not_found - cp "$canonical/$1" "$output" + cat "$canonical/$1" > "$output" } case "$url" in @@ -703,12 +703,37 @@ test_own_script_stops() { } test_gitignore_lacks_canonical_last_line() { - start_scenario ".gitignore lacking the canonical last line stops" - printf '*.log\n/myapp\n' > "$dir/repo/.gitignore" + start_scenario ".gitignore lacking the canonical last line keeps the repo's own line below the canonical content" + printf '*.log\n\n# A comment of our own.\n/myapp\n' > "$dir/repo/.gitignore" commit_and_push_fixture .gitignore .prettierrc run_update_policy + expect_success + expect_file .gitignore << 'EOF' +*.log +*.tmp + +# Own entries go below this comment. +/myapp +EOF + expect_new_commit_not_pushed +} + +test_dockerignore_lacks_canonical_last_line() { + start_scenario ".dockerignore lacking the canonical last line drops a line an earlier canonical commit held" + echo "**/*.log" > "$dir/repo/.dockerignore" + expect_former_canonical_line_replaced .dockerignore ".git" + expect_same_as_canonical .dockerignore +} + +test_editorconfig_lacks_canonical_last_line() { + start_scenario ".editorconfig lacking the canonical last line with a line of its own stops" + printf 'root = true\n[*.py]\nindent_size = 2\n' > "$dir/repo/.editorconfig" + commit_and_push_fixture .editorconfig .prettierrc + run_update_policy expect_failure - expect_output ".gitignore lacks this line" + expect_output ".editorconfig lacks this line" + expect_output "[*.py]" + expect_output "indent_size = 2" expect_nothing_written } @@ -861,6 +886,8 @@ main() { test_ignored_file_stops test_own_script_stops test_gitignore_lacks_canonical_last_line + test_dockerignore_lacks_canonical_last_line + test_editorconfig_lacks_canonical_last_line test_fetched_golangci_without_deny_list test_own_golangci_setting_stops test_source_repo_stops -- 2.54.0 From 5d82424a208c2df9be487bdaa573a86a6dcb2bff Mon Sep 17 00:00:00 2001 From: clawbot Date: Mon, 9 Nov 2026 21:46:41 +0000 Subject: [PATCH 07/12] REPO_POLICIES: say which lines an ignore file lacking the comment keeps Model: opus-5-5 --- prompts/REPO_POLICIES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/prompts/REPO_POLICIES.md b/prompts/REPO_POLICIES.md index ae433e1..2218bb3 100644 --- a/prompts/REPO_POLICIES.md +++ b/prompts/REPO_POLICIES.md @@ -951,7 +951,9 @@ style conventions are in separate documents: files is at the top of `script/install`. The script replaces each file and keeps only the repo's own entries: - in `.gitignore`, `.editorconfig` and `.dockerignore`: the lines below the - comment that ends the canonical file + comment that ends the canonical file, and, in a `.gitignore` or + `.dockerignore` lacking that comment, every line the canonical file has + never held - in `.golangci.yml`: the entries the repo added to the `deny` list of the `test-support` depguard rule -- 2.54.0 From ff42f4013dece88000b4b1ae43fa84ebf196acbc Mon Sep 17 00:00:00 2001 From: clawbot Date: Mon, 9 Nov 2026 22:31:17 +0000 Subject: [PATCH 08/12] Correct the kept-lines bullet; test .editorconfig with no own line REPO_POLICIES said an ignore file lacking the closing comment keeps every line the canonical file has never held; blank lines and comments are dropped, so it now says entries, in a bullet of its own. A new scenario covers an .editorconfig lacking the comment with no line of its own, which the script replaces. Model: opus-5-5 --- prompts/REPO_POLICIES.md | 6 +++--- test/update-policy.sh | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/prompts/REPO_POLICIES.md b/prompts/REPO_POLICIES.md index 2218bb3..0e2d201 100644 --- a/prompts/REPO_POLICIES.md +++ b/prompts/REPO_POLICIES.md @@ -951,9 +951,9 @@ style conventions are in separate documents: files is at the top of `script/install`. The script replaces each file and keeps only the repo's own entries: - in `.gitignore`, `.editorconfig` and `.dockerignore`: the lines below the - comment that ends the canonical file, and, in a `.gitignore` or - `.dockerignore` lacking that comment, every line the canonical file has - never held + comment that ends the canonical file + - in a `.gitignore` or `.dockerignore` lacking that comment: every entry the + canonical file has never held - in `.golangci.yml`: the entries the repo added to the `deny` list of the `test-support` depguard rule diff --git a/test/update-policy.sh b/test/update-policy.sh index 4c1c144..79399f1 100755 --- a/test/update-policy.sh +++ b/test/update-policy.sh @@ -737,6 +737,13 @@ test_editorconfig_lacks_canonical_last_line() { expect_nothing_written } +test_editorconfig_lacks_canonical_last_line_no_own_line() { + start_scenario ".editorconfig lacking the canonical last line with no line of its own is replaced" + printf 'root = true\n[*]\n' > "$dir/repo/.editorconfig" + expect_former_canonical_line_replaced .editorconfig "indent_size = 8" + expect_same_as_canonical .editorconfig +} + test_fetched_golangci_without_deny_list() { start_scenario "a fetched .golangci.yml with no deny list stops" cat > "$dir/canonical/.golangci.yml" << 'EOF' @@ -888,6 +895,7 @@ main() { test_gitignore_lacks_canonical_last_line test_dockerignore_lacks_canonical_last_line test_editorconfig_lacks_canonical_last_line + test_editorconfig_lacks_canonical_last_line_no_own_line test_fetched_golangci_without_deny_list test_own_golangci_setting_stops test_source_repo_stops -- 2.54.0 From 917fe4bc1da4b9ec5816e4db0e9b21ff8e409083 Mon Sep 17 00:00:00 2001 From: clawbot Date: Tue, 10 Nov 2026 20:19:00 +0000 Subject: [PATCH 09/12] Rename script/install to script/fetch-policy By convention, and in scripts-to-rule-them-all, script/install installs the program itself. This script fetches the shared policy files into a repo. The make target, the test names and the messages take the new name. Behaviour is unchanged. Model: opus-5-5 --- Makefile | 6 +- README.md | 14 ++-- TODO.md | 8 +- prompts/EXISTING_REPO_CHECKLIST.md | 6 +- prompts/NEW_REPO_CHECKLIST.md | 7 +- prompts/REPO_POLICIES.md | 32 ++++---- script/{install => fetch-policy} | 16 ++-- script/update-policy | 14 ++-- test/update-policy.sh | 121 +++++++++++++++-------------- 9 files changed, 113 insertions(+), 111 deletions(-) rename script/{install => fetch-policy} (96%) diff --git a/Makefile b/Makefile index c22ff51..0ffeb6f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks install update-policy +.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks fetch-policy update-policy # Makefile targets are thin shims; the implementations live in script/ # per the scripts-to-rule-them-all pattern (see the Entrypoints section @@ -31,8 +31,8 @@ docker: hooks: @script/install-precommit -install: - @script/install +fetch-policy: + @script/fetch-policy update-policy: @script/update-policy diff --git a/README.md b/README.md index b4cffa7..9664209 100644 --- a/README.md +++ b/README.md @@ -141,13 +141,13 @@ alpine. We provide: `script/check` - `script/install-precommit` — installs the git pre-commit hook (our own extension); `make hooks` shims to it -- `script/install` — fetch the vendored policy files from this repo over HTTP - and write them into a repo (our own extension). It writes into the repo given - as its argument, or its own repo with no argument. It stops when its target is - this repo. -- `script/update-policy` — fetch the current `script/install`, run it on its own - repo and commit the result without pushing (our own extension). It is for the - repos that vendor the policy files. It stops when run here. +- `script/fetch-policy` — fetch the vendored policy files from this repo over + HTTP and write them into a repo (our own extension). It writes into the repo + given as its argument, or its own repo with no argument. It stops when its + target is this repo. +- `script/update-policy` — fetch the current `script/fetch-policy`, run it on + its own repo and commit the result without pushing (our own extension). It is + for the repos that vendor the policy files. It stops when run here. `make hooks` installs the pre-commit hook that runs `script/precommit`. diff --git a/TODO.md b/TODO.md index 31800dc..3f6cc77 100644 --- a/TODO.md +++ b/TODO.md @@ -21,11 +21,11 @@ fmt-check, and commit. # Completed Steps -- 2026-11-09: `script/install` writes the vendored policy files into a repo over - HTTP, and installs itself and `script/update-policy` there +- 2026-11-09: `script/fetch-policy` writes the vendored policy files into a repo + over HTTP, itself and `script/update-policy` among them (https://git.eeqj.de/sneak/prompts/issues/165). `make update-policy` now - fetches the current `script/install`, runs it and commits without pushing. The - template repositories get both scripts in their own issues. + fetches the current `script/fetch-policy`, runs it and commits without + pushing. The template repositories get both scripts in their own issues. - 2026-11-09: `script/update-policy` and `make update-policy` update a repo's vendored policy files with one command (https://git.eeqj.de/sneak/prompts/issues/162). They fetch the files from diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index fbc2214..a9c4d9e 100644 --- a/prompts/EXISTING_REPO_CHECKLIST.md +++ b/prompts/EXISTING_REPO_CHECKLIST.md @@ -211,8 +211,8 @@ with your task. `https://git.eeqj.de/sneak/prompts/raw/branch/main/script/` - [ ] `script/precommit` exists and the pre-commit hook (installed by `script/install-precommit`, shimmed by `make hooks`) runs it -- [ ] `script/install` and `script/update-policy` exist, identical to the model - scripts, and `make update-policy` shims to `script/update-policy`. +- [ ] `script/fetch-policy` and `script/update-policy` exist, identical to the + model scripts, and `make update-policy` shims to `script/update-policy`. `make update-policy` commits without pushing, so a person reviews and pushes the commit. A `.gitignore` or `.dockerignore` without the comment that ends the canonical file needs no preparation. The script moves each @@ -225,7 +225,7 @@ with your task. stops the same way on a line in `.golangci.yml` that the canonical file has never held, apart from comments and the repo's entries in the `deny` list of the `test-support` rule. It also stops on a repo's own - `script/install` or `script/update-policy`. + `script/fetch-policy` or `script/update-policy`. - [ ] README has an **Entrypoints** section documenting the `script/` entrypoints and linking the standard - [ ] `script/lint` and `script/test` each run diff --git a/prompts/NEW_REPO_CHECKLIST.md b/prompts/NEW_REPO_CHECKLIST.md index c5cd5a3..e9d5fcb 100644 --- a/prompts/NEW_REPO_CHECKLIST.md +++ b/prompts/NEW_REPO_CHECKLIST.md @@ -266,10 +266,11 @@ are thin shims calling them. Model scripts: - [ ] `script/install-precommit` — installs the pre-commit hook that runs `script/precommit` - [ ] `make hooks` — shims to `script/install-precommit` -- [ ] `script/install` — fetches the vendored policy files from the `prompts` - repo over HTTP and writes them into the repo; identical across repos +- [ ] `script/fetch-policy` — fetches the vendored policy files from the + `prompts` repo over HTTP and writes them into the repo; identical across + repos - [ ] `script/update-policy` / `make update-policy` — fetches the current - `script/install`, runs it on the repo and commits the result without + `script/fetch-policy`, runs it on the repo and commits the result without pushing; identical across repos - [ ] README **Entrypoints** section documents the scripts and links the standard diff --git a/prompts/REPO_POLICIES.md b/prompts/REPO_POLICIES.md index 0e2d201..f86f65d 100644 --- a/prompts/REPO_POLICIES.md +++ b/prompts/REPO_POLICIES.md @@ -110,7 +110,7 @@ style conventions are in separate documents: - `script/fmt-check` - `script/check` - `script/docker` - - `script/install` + - `script/fetch-policy` - `script/update-policy` The scripts must be POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they @@ -191,10 +191,10 @@ style conventions are in separate documents: `script/check`. - `script/install-precommit` installs the git pre-commit hook. The `make hooks` target shims to it. - - `script/install` fetches the vendored policy files from the `prompts` repo - over HTTP and writes them into the repo it is given, or its own repo with - no argument. - - `script/update-policy` fetches the current `script/install` from the + - `script/fetch-policy` fetches the vendored policy files from the `prompts` + repo over HTTP and writes them into the repo it is given, or its own repo + with no argument. + - `script/update-policy` fetches the current `script/fetch-policy` from the `prompts` repo, runs it on its own repo and commits the result. It does not push. The `make update-policy` target shims to it. - `script/projectname` (literally that filename) simply outputs the @@ -937,19 +937,19 @@ style conventions are in separate documents: feature branch. - One command brings a repo up to standards: `make update-policy`. It runs - `script/update-policy`, which fetches the current `script/install` from the - head of `main` in the `prompts` repo and runs it on the repo. It then commits - the files that changed and does not push, so a person reviews and pushes the - commit. + `script/update-policy`, which fetches the current `script/fetch-policy` from + the head of `main` in the `prompts` repo and runs it on the repo. It then + commits the files that changed and does not push, so a person reviews and + pushes the commit. - `script/install` fetches each vendored file from `prompts` over HTTP and - writes it into the repo. It skips a language's files when git tracks no + `script/fetch-policy` fetches each vendored file from `prompts` over HTTP + and writes it into the repo. It skips a language's files when git tracks no source file of that language. A file the repo lacks is written whole. A `prompts/NAME.md` goes to `docs/NAME.md`, unless the repo keeps it at - `NAME.md`. It also installs `script/install` and `script/update-policy` in - the repo, so a repo updated once can update itself. The list of vendored - files is at the top of `script/install`. The script replaces each file and - keeps only the repo's own entries: + `NAME.md`. It also writes `script/fetch-policy` and `script/update-policy` + into the repo, so a repo updated once can update itself. The list of + vendored files is at the top of `script/fetch-policy`. The script replaces + each file and keeps only the repo's own entries: - in `.gitignore`, `.editorconfig` and `.dockerignore`: the lines below the comment that ends the canonical file - in a `.gitignore` or `.dockerignore` lacking that comment: every entry the @@ -1182,7 +1182,7 @@ style conventions are in separate documents: - `Makefile` - `script/` entrypoints (`bootstrap`, `setup`, `projectname`, `test`, `lint`, `fmt`, `fmt-check`, `check`, `docker`, `cibuild`, `precommit`, - `install-precommit`, `install`, `update-policy`) + `install-precommit`, `fetch-policy`, `update-policy`) - `Dockerfile`, `.dockerignore` - `.gitea/workflows/check.yml` - Go: `go.mod`, `go.sum`, `.golangci.yml` diff --git a/script/install b/script/fetch-policy similarity index 96% rename from script/install rename to script/fetch-policy index 842f727..dc9d5e7 100755 --- a/script/install +++ b/script/fetch-policy @@ -1,10 +1,10 @@ #!/bin/sh -# script/install: write the current policy files from the prompts repo -# into a repo clone, over HTTP. It commits nothing; script/update-policy -# does that. Identical in all repos. Our own extension to -# scripts-to-rule-them-all. +# script/fetch-policy: write the current policy files from the prompts +# repo into a repo clone, over HTTP. It commits nothing; +# script/update-policy does that. Identical in all repos. Our own +# extension to scripts-to-rule-them-all. # -# Usage: script/install [path of a repo clone] +# Usage: script/fetch-policy [path of a repo clone] # Without a path it updates the repo this script sits in. # # Standard output is the full hash of the prompts commit, then each path @@ -50,12 +50,12 @@ prompts/GO_HTTP_SERVER_CONVENTIONS.md prompts/LLM_PROSE_TELLS.md prompts/NEW_REPO_CHECKLIST.md prompts/REPO_POLICIES.md -script/install +script/fetch-policy script/update-policy " die() { - echo "install: $*" >&2 + echo "fetch-policy: $*" >&2 exit 1 } @@ -226,7 +226,7 @@ $own" } main() { - [ "$#" -le 1 ] || die "usage: script/install [path of a repo clone]" + [ "$#" -le 1 ] || die "usage: script/fetch-policy [path of a repo clone]" target="${1:-$(dirname "$0")/..}" cd "$target" || die "no such directory: $target" diff --git a/script/update-policy b/script/update-policy index d58aa88..a30e0c9 100755 --- a/script/update-policy +++ b/script/update-policy @@ -1,12 +1,12 @@ #!/bin/sh # script/update-policy: bring this repo up to standards. It fetches the -# current script/install from the prompts repo over HTTP, runs it on this -# repo and commits the files it wrote. It does not push. Identical in all -# repos. Our own extension to scripts-to-rule-them-all. +# current script/fetch-policy from the prompts repo over HTTP, runs it +# on this repo and commits the files it wrote. It does not push. +# Identical in all repos. Our own extension to scripts-to-rule-them-all. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" -INSTALL_URL="https://git.eeqj.de/sneak/prompts/raw/branch/main/script/install" +FETCH_POLICY_URL="https://git.eeqj.de/sneak/prompts/raw/branch/main/script/fetch-policy" die() { echo "update-policy: $*" >&2 @@ -22,10 +22,10 @@ main() { trap 'rm -rf "$tmp"' EXIT curl --fail --silent --show-error --max-time 60 \ - --output "$tmp/install" "$INSTALL_URL" || - die "cannot fetch $INSTALL_URL" + --output "$tmp/fetch-policy" "$FETCH_POLICY_URL" || + die "cannot fetch $FETCH_POLICY_URL" - sh "$tmp/install" "$ROOT" > "$tmp/written" + sh "$tmp/fetch-policy" "$ROOT" > "$tmp/written" commit="$(head -n 1 "$tmp/written")" paths="$(tail -n +2 "$tmp/written")" diff --git a/test/update-policy.sh b/test/update-policy.sh index 79399f1..d902b0c 100755 --- a/test/update-policy.sh +++ b/test/update-policy.sh @@ -1,13 +1,13 @@ #!/bin/sh -# test/update-policy.sh: test script/install and script/update-policy on -# throwaway repos. Each scenario gets one temp directory holding a repo, -# its local bare origin, a local git repository of the canonical files -# in place of prompts, and a stub curl that serves them, so nothing -# touches the network. +# test/update-policy.sh: test script/fetch-policy and +# script/update-policy on throwaway repos. Each scenario gets one temp +# directory holding a repo, its local bare origin, a local git +# repository of the canonical files in place of prompts, and a stub curl +# that serves them, so nothing touches the network. set -eu SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd -P)/script" -INSTALL="$SCRIPT_DIR/install" +FETCH_POLICY="$SCRIPT_DIR/fetch-policy" UPDATE_POLICY="$SCRIPT_DIR/update-policy" dir="" @@ -60,8 +60,8 @@ case "$url" in "$HASH" 0000000000000000000000000000000000000000 > "$output" fi ;; - "$site/raw/branch/main/script/install") - serve_file script/install + "$site/raw/branch/main/script/fetch-policy") + serve_file script/fetch-policy ;; "$site/compare/$first...$HASH.patch") git -C "$canonical" format-patch --stdout --root "$HASH" > "$output" @@ -79,7 +79,7 @@ EOF write_canonical_files() { mkdir -p "$dir/canonical/prompts" "$dir/canonical/script" - cp "$INSTALL" "$UPDATE_POLICY" "$dir/canonical/script/" + cp "$FETCH_POLICY" "$UPDATE_POLICY" "$dir/canonical/script/" cat > "$dir/canonical/.gitignore" << 'EOF' *.log *.tmp @@ -208,7 +208,7 @@ start_scenario() { git -C "$dir/repo" remote add origin "$dir/origin.git" mkdir "$dir/repo/script" - cp "$INSTALL" "$UPDATE_POLICY" "$dir/repo/script/" + cp "$FETCH_POLICY" "$UPDATE_POLICY" "$dir/repo/script/" printf '#!/bin/sh\necho example\n' > "$dir/repo/script/projectname" chmod +x "$dir/repo/script/projectname" mkdir -p "$dir/repo/cmd/app" @@ -261,12 +261,12 @@ run_update_policy() { script/update-policy "$@") > "$dir/output" 2>&1 || status=$? } -# run_install : run the script in this source tree, from -# outside the repo it updates. -run_install() { +# run_fetch_policy : run the script in this source tree, +# from outside the repo it updates. +run_fetch_policy() { status=0 (cd "$dir" && PATH="$dir/bin:$PATH" TMPDIR="$dir/tmp" \ - "$INSTALL" "$@") > "$dir/output" 2>&1 || status=$? + "$FETCH_POLICY" "$@") > "$dir/output" 2>&1 || status=$? } head_of_repo() { @@ -478,26 +478,27 @@ test_nothing_changed() { expect_nothing_written } -test_target_lacking_install_gets_both_scripts() { - start_scenario "a repo lacking script/install gets it, and the current script/update-policy, in the commit" - rm "$dir/repo/script/install" +test_target_lacking_fetch_policy_gets_both_scripts() { + start_scenario "a repo lacking script/fetch-policy gets it, and the current script/update-policy, in the commit" + rm "$dir/repo/script/fetch-policy" echo "# An older version." >> "$dir/repo/script/update-policy" commit_and_push_fixture .prettierrc run_update_policy expect_success - expect_same_as_canonical script/install + expect_same_as_canonical script/fetch-policy expect_same_as_canonical script/update-policy - expect_committed_executable script/install + expect_committed_executable script/fetch-policy expect_committed_executable script/update-policy expect_new_commit_not_pushed - [ "$(files_in_commit)" = ".prettierrc script/install script/update-policy " ] || + [ "$(files_in_commit)" = ".prettierrc script/fetch-policy script/update-policy " ] || fail "the commit holds other files: $(files_in_commit)" } # The shell reads a script in blocks, so the padding makes it read the # last line only after main has returned. It finds that line only if -# script/install left the old file in place under a new name. The old -# file is an earlier canonical version, so script/install replaces it. +# script/fetch-policy left the old file in place under a new name. The +# old file is an earlier canonical version, so script/fetch-policy +# replaces it. test_update_policy_replaces_itself() { start_scenario "script/update-policy replaces its own file and still exits 0" { @@ -524,44 +525,44 @@ test_update_policy_replaces_itself() { expect_new_commit_not_pushed } -test_install_with_path_commits_nothing() { - start_scenario "script/install with a path writes into that clone and commits nothing" - rm "$dir/repo/script/install" "$dir/repo/script/update-policy" +test_fetch_policy_with_path_commits_nothing() { + start_scenario "script/fetch-policy with a path writes into that clone and commits nothing" + rm "$dir/repo/script/fetch-policy" "$dir/repo/script/update-policy" commit_and_push_fixture .prettierrc - run_install "$dir/repo" + run_fetch_policy "$dir/repo" expect_success expect_file .prettierrc << 'EOF' { "tabWidth": 4 } EOF - expect_same_as_canonical script/install + expect_same_as_canonical script/fetch-policy expect_same_as_canonical script/update-policy - expect_executable script/install + expect_executable script/fetch-policy expect_executable script/update-policy expect_no_commit [ "$(head -n 1 "$dir/output")" = "$HASH" ] || fail "the first line of output is not the hash" - expect_output "script/install" + expect_output "script/fetch-policy" [ -z "$(ls -A "$dir/tmp")" ] || fail "a temporary directory is left" } -test_install_without_script_directory() { - start_scenario "script/install with a path writes both scripts into a clone with no script directory" +test_fetch_policy_without_script_directory() { + start_scenario "script/fetch-policy with a path writes both scripts into a clone with no script directory" commit_and_push_fixture .prettierrc remove_from_fixture script - run_install "$dir/repo" + run_fetch_policy "$dir/repo" expect_success - expect_same_as_canonical script/install + expect_same_as_canonical script/fetch-policy expect_same_as_canonical script/update-policy - expect_executable script/install + expect_executable script/fetch-policy expect_executable script/update-policy expect_no_commit } -test_install_without_argument_updates_own_repo() { - start_scenario "script/install with no argument updates the repo it sits in" +test_fetch_policy_without_argument_updates_own_repo() { + start_scenario "script/fetch-policy with no argument updates the repo it sits in" commit_and_push_fixture .prettierrc status=0 - (cd "$dir" && PATH="$dir/bin:$PATH" repo/script/install) \ + (cd "$dir" && PATH="$dir/bin:$PATH" repo/script/fetch-policy) \ > "$dir/output" 2>&1 || status=$? expect_success expect_file .prettierrc << 'EOF' @@ -570,19 +571,19 @@ EOF expect_no_commit } -test_install_bad_target() { - start_scenario "script/install refuses a path that is not the top of a clone" +test_fetch_policy_bad_target() { + start_scenario "script/fetch-policy refuses a path that is not the top of a clone" commit_and_push_fixture .prettierrc - run_install "$dir/repo/script" + run_fetch_policy "$dir/repo/script" expect_failure expect_output "not the top of a git clone" - run_install "$dir/bin" + run_fetch_policy "$dir/bin" expect_failure expect_output "not the top of a git clone" - run_install "$dir/no-such-directory" + run_fetch_policy "$dir/no-such-directory" expect_failure expect_output "no such directory" - run_install "$dir/repo" extra + run_fetch_policy "$dir/repo" extra expect_failure expect_output "usage:" expect_nothing_written @@ -591,7 +592,7 @@ test_install_bad_target() { test_failed_fetch_stops() { for url in \ "/api/v1/repos/sneak/prompts/branches/main" \ - "/raw/branch/main/script/install" \ + "/raw/branch/main/script/fetch-policy" \ "/compare/" \ "/raw/commit/"; do start_scenario "a failed fetch of $url stops with nothing written" @@ -661,14 +662,14 @@ EOF } test_untracked_script_stops() { - start_scenario "an untracked script/install is not overwritten" - rm "$dir/repo/script/install" + start_scenario "an untracked script/fetch-policy is not overwritten" + rm "$dir/repo/script/fetch-policy" commit_and_push_fixture .prettierrc - echo "our own script" > "$dir/repo/script/install" - run_install "$dir/repo" + echo "our own script" > "$dir/repo/script/fetch-policy" + run_fetch_policy "$dir/repo" expect_failure - expect_output "script/install has uncommitted changes" - expect_file script/install << 'EOF' + expect_output "script/fetch-policy has uncommitted changes" + expect_file script/fetch-policy << 'EOF' our own script EOF expect_no_commit @@ -680,7 +681,7 @@ test_ignored_file_stops() { remove_from_fixture docs/API_POLICIES.md echo "docs/API_POLICIES.md" >> "$dir/repo/.git/info/exclude" echo "our own notes" > "$dir/repo/docs/API_POLICIES.md" - run_install "$dir/repo" + run_fetch_policy "$dir/repo" expect_failure expect_output "docs/API_POLICIES.md is ignored by git" expect_file docs/API_POLICIES.md << 'EOF' @@ -690,7 +691,7 @@ EOF } test_own_script_stops() { - for script in script/install script/update-policy; do + for script in script/fetch-policy script/update-policy; do start_scenario "a tracked $script with a line the canonical file has never held stops" echo "echo our own script" >> "$dir/repo/$script" commit_and_push_fixture .prettierrc @@ -848,9 +849,9 @@ test_former_canonical_gitignore_line_replaced() { } test_former_canonical_script_line_replaced() { - start_scenario "a script/install line an earlier canonical commit held is replaced" - expect_former_canonical_line_replaced script/install "echo an older install" - expect_same_as_canonical script/install + start_scenario "a script/fetch-policy line an earlier canonical commit held is replaced" + expect_former_canonical_line_replaced script/fetch-policy "echo an older version" + expect_same_as_canonical script/fetch-policy } # A line another file's history removed does not count as held by this @@ -878,12 +879,12 @@ main() { test_language_files_need_a_source_file test_javascript_source_brings_its_styleguide test_nothing_changed - test_target_lacking_install_gets_both_scripts + test_target_lacking_fetch_policy_gets_both_scripts test_update_policy_replaces_itself - test_install_with_path_commits_nothing - test_install_without_script_directory - test_install_without_argument_updates_own_repo - test_install_bad_target + test_fetch_policy_with_path_commits_nothing + test_fetch_policy_without_script_directory + test_fetch_policy_without_argument_updates_own_repo + test_fetch_policy_bad_target test_failed_fetch_stops test_one_missing_file_stops_all test_branch_response_without_hash_stops -- 2.54.0 From ed60414378bd06535662926af894960d41cb4cfd Mon Sep 17 00:00:00 2001 From: clawbot Date: Wed, 11 Nov 2026 00:00:28 +0000 Subject: [PATCH 10/12] Rename script/fetch-policy to script/commit-current-upstream-policy-docs sneak, on the PR: the script does more than fetch, and short names are not wanted. The make target, the test names, the variables and the messages take the new name. Behaviour is unchanged. Model: opus-5-5 --- Makefile | 6 +- README.md | 15 ++- TODO.md | 11 +- prompts/EXISTING_REPO_CHECKLIST.md | 31 ++--- prompts/NEW_REPO_CHECKLIST.md | 10 +- prompts/REPO_POLICIES.md | 43 +++--- ...cy => commit-current-upstream-policy-docs} | 17 +-- script/update-policy | 16 ++- test/update-policy.sh | 125 +++++++++--------- 9 files changed, 145 insertions(+), 129 deletions(-) rename script/{fetch-policy => commit-current-upstream-policy-docs} (94%) diff --git a/Makefile b/Makefile index 0ffeb6f..96f86de 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks fetch-policy update-policy +.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks commit-current-upstream-policy-docs update-policy # Makefile targets are thin shims; the implementations live in script/ # per the scripts-to-rule-them-all pattern (see the Entrypoints section @@ -31,8 +31,8 @@ docker: hooks: @script/install-precommit -fetch-policy: - @script/fetch-policy +commit-current-upstream-policy-docs: + @script/commit-current-upstream-policy-docs update-policy: @script/update-policy diff --git a/README.md b/README.md index 9664209..8ae847e 100644 --- a/README.md +++ b/README.md @@ -141,13 +141,14 @@ alpine. We provide: `script/check` - `script/install-precommit` — installs the git pre-commit hook (our own extension); `make hooks` shims to it -- `script/fetch-policy` — fetch the vendored policy files from this repo over - HTTP and write them into a repo (our own extension). It writes into the repo - given as its argument, or its own repo with no argument. It stops when its - target is this repo. -- `script/update-policy` — fetch the current `script/fetch-policy`, run it on - its own repo and commit the result without pushing (our own extension). It is - for the repos that vendor the policy files. It stops when run here. +- `script/commit-current-upstream-policy-docs` — fetch the vendored policy files + from this repo over HTTP and write them into a repo (our own extension). It + writes into the repo given as its argument, or its own repo with no argument. + It stops when its target is this repo. +- `script/update-policy` — fetch the current + `script/commit-current-upstream-policy-docs`, run it on its own repo and + commit the result without pushing (our own extension). It is for the repos + that vendor the policy files. It stops when run here. `make hooks` installs the pre-commit hook that runs `script/precommit`. diff --git a/TODO.md b/TODO.md index 3f6cc77..75300d7 100644 --- a/TODO.md +++ b/TODO.md @@ -21,11 +21,12 @@ fmt-check, and commit. # Completed Steps -- 2026-11-09: `script/fetch-policy` writes the vendored policy files into a repo - over HTTP, itself and `script/update-policy` among them - (https://git.eeqj.de/sneak/prompts/issues/165). `make update-policy` now - fetches the current `script/fetch-policy`, runs it and commits without - pushing. The template repositories get both scripts in their own issues. +- 2026-11-09: `script/commit-current-upstream-policy-docs` writes the vendored + policy files into a repo over HTTP, itself and `script/update-policy` among + them (https://git.eeqj.de/sneak/prompts/issues/165). `make update-policy` now + fetches the current `script/commit-current-upstream-policy-docs`, runs it and + commits without pushing. The template repositories get both scripts in their + own issues. - 2026-11-09: `script/update-policy` and `make update-policy` update a repo's vendored policy files with one command (https://git.eeqj.de/sneak/prompts/issues/162). They fetch the files from diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index a9c4d9e..38d57a7 100644 --- a/prompts/EXISTING_REPO_CHECKLIST.md +++ b/prompts/EXISTING_REPO_CHECKLIST.md @@ -211,21 +211,22 @@ with your task. `https://git.eeqj.de/sneak/prompts/raw/branch/main/script/` - [ ] `script/precommit` exists and the pre-commit hook (installed by `script/install-precommit`, shimmed by `make hooks`) runs it -- [ ] `script/fetch-policy` and `script/update-policy` exist, identical to the - model scripts, and `make update-policy` shims to `script/update-policy`. - `make update-policy` commits without pushing, so a person reviews and - pushes the commit. A `.gitignore` or `.dockerignore` without the comment - that ends the canonical file needs no preparation. The script moves each - line the canonical file has never held to below the canonical content. - Blank lines and comments are dropped. Before its first use, add that - comment to an `.editorconfig` that has lines of its own, and put those - lines below it. Otherwise the script stops before it writes anything, - naming the file and each line. In a file that has the comment, an entry - above it which the canonical file has never held also stops the script. It - stops the same way on a line in `.golangci.yml` that the canonical file - has never held, apart from comments and the repo's entries in the `deny` - list of the `test-support` rule. It also stops on a repo's own - `script/fetch-policy` or `script/update-policy`. +- [ ] `script/commit-current-upstream-policy-docs` and `script/update-policy` + exist, identical to the model scripts, and `make update-policy` shims to + `script/update-policy`. `make update-policy` commits without pushing, so a + person reviews and pushes the commit. A `.gitignore` or `.dockerignore` + without the comment that ends the canonical file needs no preparation. The + script moves each line the canonical file has never held to below the + canonical content. Blank lines and comments are dropped. Before its first + use, add that comment to an `.editorconfig` that has lines of its own, and + put those lines below it. Otherwise the script stops before it writes + anything, naming the file and each line. In a file that has the comment, + an entry above it which the canonical file has never held also stops the + script. It stops the same way on a line in `.golangci.yml` that the + canonical file has never held, apart from comments and the repo's entries + in the `deny` list of the `test-support` rule. It also stops on a repo's + own `script/commit-current-upstream-policy-docs` or + `script/update-policy`. - [ ] README has an **Entrypoints** section documenting the `script/` entrypoints and linking the standard - [ ] `script/lint` and `script/test` each run diff --git a/prompts/NEW_REPO_CHECKLIST.md b/prompts/NEW_REPO_CHECKLIST.md index e9d5fcb..7d848ba 100644 --- a/prompts/NEW_REPO_CHECKLIST.md +++ b/prompts/NEW_REPO_CHECKLIST.md @@ -266,12 +266,12 @@ are thin shims calling them. Model scripts: - [ ] `script/install-precommit` — installs the pre-commit hook that runs `script/precommit` - [ ] `make hooks` — shims to `script/install-precommit` -- [ ] `script/fetch-policy` — fetches the vendored policy files from the - `prompts` repo over HTTP and writes them into the repo; identical across - repos +- [ ] `script/commit-current-upstream-policy-docs` — fetches the vendored policy + files from the `prompts` repo over HTTP and writes them into the repo; + identical across repos - [ ] `script/update-policy` / `make update-policy` — fetches the current - `script/fetch-policy`, runs it on the repo and commits the result without - pushing; identical across repos + `script/commit-current-upstream-policy-docs`, runs it on the repo and + commits the result without pushing; identical across repos - [ ] README **Entrypoints** section documents the scripts and links the standard diff --git a/prompts/REPO_POLICIES.md b/prompts/REPO_POLICIES.md index f86f65d..b93efa6 100644 --- a/prompts/REPO_POLICIES.md +++ b/prompts/REPO_POLICIES.md @@ -110,7 +110,7 @@ style conventions are in separate documents: - `script/fmt-check` - `script/check` - `script/docker` - - `script/fetch-policy` + - `script/commit-current-upstream-policy-docs` - `script/update-policy` The scripts must be POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they @@ -191,12 +191,13 @@ style conventions are in separate documents: `script/check`. - `script/install-precommit` installs the git pre-commit hook. The `make hooks` target shims to it. - - `script/fetch-policy` fetches the vendored policy files from the `prompts` - repo over HTTP and writes them into the repo it is given, or its own repo - with no argument. - - `script/update-policy` fetches the current `script/fetch-policy` from the - `prompts` repo, runs it on its own repo and commits the result. It does - not push. The `make update-policy` target shims to it. + - `script/commit-current-upstream-policy-docs` fetches the vendored policy + files from the `prompts` repo over HTTP and writes them into the repo it + is given, or its own repo with no argument. + - `script/update-policy` fetches the current + `script/commit-current-upstream-policy-docs` from the `prompts` repo, runs + it on its own repo and commits the result. It does not push. The + `make update-policy` target shims to it. - `script/projectname` (literally that filename) simply outputs the project's name. @@ -937,19 +938,20 @@ style conventions are in separate documents: feature branch. - One command brings a repo up to standards: `make update-policy`. It runs - `script/update-policy`, which fetches the current `script/fetch-policy` from - the head of `main` in the `prompts` repo and runs it on the repo. It then - commits the files that changed and does not push, so a person reviews and - pushes the commit. + `script/update-policy`, which fetches the current + `script/commit-current-upstream-policy-docs` from the head of `main` in the + `prompts` repo and runs it on the repo. It then commits the files that changed + and does not push, so a person reviews and pushes the commit. - `script/fetch-policy` fetches each vendored file from `prompts` over HTTP - and writes it into the repo. It skips a language's files when git tracks no - source file of that language. A file the repo lacks is written whole. A - `prompts/NAME.md` goes to `docs/NAME.md`, unless the repo keeps it at - `NAME.md`. It also writes `script/fetch-policy` and `script/update-policy` - into the repo, so a repo updated once can update itself. The list of - vendored files is at the top of `script/fetch-policy`. The script replaces - each file and keeps only the repo's own entries: + `script/commit-current-upstream-policy-docs` fetches each vendored file from + `prompts` over HTTP and writes it into the repo. It skips a language's files + when git tracks no source file of that language. A file the repo lacks is + written whole. A `prompts/NAME.md` goes to `docs/NAME.md`, unless the repo + keeps it at `NAME.md`. It also writes + `script/commit-current-upstream-policy-docs` and `script/update-policy` into + the repo, so a repo updated once can update itself. The list of vendored + files is at the top of `script/commit-current-upstream-policy-docs`. The + script replaces each file and keeps only the repo's own entries: - in `.gitignore`, `.editorconfig` and `.dockerignore`: the lines below the comment that ends the canonical file - in a `.gitignore` or `.dockerignore` lacking that comment: every entry the @@ -1182,7 +1184,8 @@ style conventions are in separate documents: - `Makefile` - `script/` entrypoints (`bootstrap`, `setup`, `projectname`, `test`, `lint`, `fmt`, `fmt-check`, `check`, `docker`, `cibuild`, `precommit`, - `install-precommit`, `fetch-policy`, `update-policy`) + `install-precommit`, `commit-current-upstream-policy-docs`, + `update-policy`) - `Dockerfile`, `.dockerignore` - `.gitea/workflows/check.yml` - Go: `go.mod`, `go.sum`, `.golangci.yml` diff --git a/script/fetch-policy b/script/commit-current-upstream-policy-docs similarity index 94% rename from script/fetch-policy rename to script/commit-current-upstream-policy-docs index dc9d5e7..c4e600d 100755 --- a/script/fetch-policy +++ b/script/commit-current-upstream-policy-docs @@ -1,10 +1,10 @@ #!/bin/sh -# script/fetch-policy: write the current policy files from the prompts -# repo into a repo clone, over HTTP. It commits nothing; -# script/update-policy does that. Identical in all repos. Our own -# extension to scripts-to-rule-them-all. +# script/commit-current-upstream-policy-docs: write the current policy +# files from the prompts repo into a repo clone, over HTTP. It commits +# nothing; script/update-policy does that. Identical in all repos. Our +# own extension to scripts-to-rule-them-all. # -# Usage: script/fetch-policy [path of a repo clone] +# Usage: script/commit-current-upstream-policy-docs [path of a repo clone] # Without a path it updates the repo this script sits in. # # Standard output is the full hash of the prompts commit, then each path @@ -50,12 +50,12 @@ prompts/GO_HTTP_SERVER_CONVENTIONS.md prompts/LLM_PROSE_TELLS.md prompts/NEW_REPO_CHECKLIST.md prompts/REPO_POLICIES.md -script/fetch-policy +script/commit-current-upstream-policy-docs script/update-policy " die() { - echo "fetch-policy: $*" >&2 + echo "commit-current-upstream-policy-docs: $*" >&2 exit 1 } @@ -226,7 +226,8 @@ $own" } main() { - [ "$#" -le 1 ] || die "usage: script/fetch-policy [path of a repo clone]" + [ "$#" -le 1 ] || + die "usage: script/commit-current-upstream-policy-docs [path of a repo clone]" target="${1:-$(dirname "$0")/..}" cd "$target" || die "no such directory: $target" diff --git a/script/update-policy b/script/update-policy index a30e0c9..c4f40fd 100755 --- a/script/update-policy +++ b/script/update-policy @@ -1,12 +1,13 @@ #!/bin/sh # script/update-policy: bring this repo up to standards. It fetches the -# current script/fetch-policy from the prompts repo over HTTP, runs it -# on this repo and commits the files it wrote. It does not push. -# Identical in all repos. Our own extension to scripts-to-rule-them-all. +# current script/commit-current-upstream-policy-docs from the prompts +# repo over HTTP, runs it on this repo and commits the files it wrote. +# It does not push. Identical in all repos. Our own extension to +# scripts-to-rule-them-all. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" -FETCH_POLICY_URL="https://git.eeqj.de/sneak/prompts/raw/branch/main/script/fetch-policy" +COMMIT_CURRENT_UPSTREAM_POLICY_DOCS_URL="https://git.eeqj.de/sneak/prompts/raw/branch/main/script/commit-current-upstream-policy-docs" die() { echo "update-policy: $*" >&2 @@ -22,10 +23,11 @@ main() { trap 'rm -rf "$tmp"' EXIT curl --fail --silent --show-error --max-time 60 \ - --output "$tmp/fetch-policy" "$FETCH_POLICY_URL" || - die "cannot fetch $FETCH_POLICY_URL" + --output "$tmp/commit-current-upstream-policy-docs" \ + "$COMMIT_CURRENT_UPSTREAM_POLICY_DOCS_URL" || + die "cannot fetch $COMMIT_CURRENT_UPSTREAM_POLICY_DOCS_URL" - sh "$tmp/fetch-policy" "$ROOT" > "$tmp/written" + sh "$tmp/commit-current-upstream-policy-docs" "$ROOT" > "$tmp/written" commit="$(head -n 1 "$tmp/written")" paths="$(tail -n +2 "$tmp/written")" diff --git a/test/update-policy.sh b/test/update-policy.sh index d902b0c..9d18119 100755 --- a/test/update-policy.sh +++ b/test/update-policy.sh @@ -1,13 +1,13 @@ #!/bin/sh -# test/update-policy.sh: test script/fetch-policy and -# script/update-policy on throwaway repos. Each scenario gets one temp -# directory holding a repo, its local bare origin, a local git +# test/update-policy.sh: test script/commit-current-upstream-policy-docs +# and script/update-policy on throwaway repos. Each scenario gets one +# temp directory holding a repo, its local bare origin, a local git # repository of the canonical files in place of prompts, and a stub curl # that serves them, so nothing touches the network. set -eu SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd -P)/script" -FETCH_POLICY="$SCRIPT_DIR/fetch-policy" +COMMIT_CURRENT_UPSTREAM_POLICY_DOCS="$SCRIPT_DIR/commit-current-upstream-policy-docs" UPDATE_POLICY="$SCRIPT_DIR/update-policy" dir="" @@ -60,8 +60,8 @@ case "$url" in "$HASH" 0000000000000000000000000000000000000000 > "$output" fi ;; - "$site/raw/branch/main/script/fetch-policy") - serve_file script/fetch-policy + "$site/raw/branch/main/script/commit-current-upstream-policy-docs") + serve_file script/commit-current-upstream-policy-docs ;; "$site/compare/$first...$HASH.patch") git -C "$canonical" format-patch --stdout --root "$HASH" > "$output" @@ -79,7 +79,8 @@ EOF write_canonical_files() { mkdir -p "$dir/canonical/prompts" "$dir/canonical/script" - cp "$FETCH_POLICY" "$UPDATE_POLICY" "$dir/canonical/script/" + cp "$COMMIT_CURRENT_UPSTREAM_POLICY_DOCS" "$UPDATE_POLICY" \ + "$dir/canonical/script/" cat > "$dir/canonical/.gitignore" << 'EOF' *.log *.tmp @@ -208,7 +209,8 @@ start_scenario() { git -C "$dir/repo" remote add origin "$dir/origin.git" mkdir "$dir/repo/script" - cp "$FETCH_POLICY" "$UPDATE_POLICY" "$dir/repo/script/" + cp "$COMMIT_CURRENT_UPSTREAM_POLICY_DOCS" "$UPDATE_POLICY" \ + "$dir/repo/script/" printf '#!/bin/sh\necho example\n' > "$dir/repo/script/projectname" chmod +x "$dir/repo/script/projectname" mkdir -p "$dir/repo/cmd/app" @@ -261,12 +263,13 @@ run_update_policy() { script/update-policy "$@") > "$dir/output" 2>&1 || status=$? } -# run_fetch_policy : run the script in this source tree, -# from outside the repo it updates. -run_fetch_policy() { +# run_commit_current_upstream_policy_docs : run the script in +# this source tree, from outside the repo it updates. +run_commit_current_upstream_policy_docs() { status=0 (cd "$dir" && PATH="$dir/bin:$PATH" TMPDIR="$dir/tmp" \ - "$FETCH_POLICY" "$@") > "$dir/output" 2>&1 || status=$? + "$COMMIT_CURRENT_UPSTREAM_POLICY_DOCS" "$@") > "$dir/output" 2>&1 || + status=$? } head_of_repo() { @@ -478,27 +481,27 @@ test_nothing_changed() { expect_nothing_written } -test_target_lacking_fetch_policy_gets_both_scripts() { - start_scenario "a repo lacking script/fetch-policy gets it, and the current script/update-policy, in the commit" - rm "$dir/repo/script/fetch-policy" +test_target_lacking_commit_current_upstream_policy_docs_gets_both_scripts() { + start_scenario "a repo lacking script/commit-current-upstream-policy-docs gets it, and the current script/update-policy, in the commit" + rm "$dir/repo/script/commit-current-upstream-policy-docs" echo "# An older version." >> "$dir/repo/script/update-policy" commit_and_push_fixture .prettierrc run_update_policy expect_success - expect_same_as_canonical script/fetch-policy + expect_same_as_canonical script/commit-current-upstream-policy-docs expect_same_as_canonical script/update-policy - expect_committed_executable script/fetch-policy + expect_committed_executable script/commit-current-upstream-policy-docs expect_committed_executable script/update-policy expect_new_commit_not_pushed - [ "$(files_in_commit)" = ".prettierrc script/fetch-policy script/update-policy " ] || + [ "$(files_in_commit)" = ".prettierrc script/commit-current-upstream-policy-docs script/update-policy " ] || fail "the commit holds other files: $(files_in_commit)" } # The shell reads a script in blocks, so the padding makes it read the # last line only after main has returned. It finds that line only if -# script/fetch-policy left the old file in place under a new name. The -# old file is an earlier canonical version, so script/fetch-policy -# replaces it. +# script/commit-current-upstream-policy-docs left the old file in place +# under a new name. The old file is an earlier canonical version, so +# script/commit-current-upstream-policy-docs replaces it. test_update_policy_replaces_itself() { start_scenario "script/update-policy replaces its own file and still exits 0" { @@ -525,44 +528,46 @@ test_update_policy_replaces_itself() { expect_new_commit_not_pushed } -test_fetch_policy_with_path_commits_nothing() { - start_scenario "script/fetch-policy with a path writes into that clone and commits nothing" - rm "$dir/repo/script/fetch-policy" "$dir/repo/script/update-policy" +test_commit_current_upstream_policy_docs_with_path_commits_nothing() { + start_scenario "script/commit-current-upstream-policy-docs with a path writes into that clone and commits nothing" + rm "$dir/repo/script/commit-current-upstream-policy-docs" \ + "$dir/repo/script/update-policy" commit_and_push_fixture .prettierrc - run_fetch_policy "$dir/repo" + run_commit_current_upstream_policy_docs "$dir/repo" expect_success expect_file .prettierrc << 'EOF' { "tabWidth": 4 } EOF - expect_same_as_canonical script/fetch-policy + expect_same_as_canonical script/commit-current-upstream-policy-docs expect_same_as_canonical script/update-policy - expect_executable script/fetch-policy + expect_executable script/commit-current-upstream-policy-docs expect_executable script/update-policy expect_no_commit [ "$(head -n 1 "$dir/output")" = "$HASH" ] || fail "the first line of output is not the hash" - expect_output "script/fetch-policy" + expect_output "script/commit-current-upstream-policy-docs" [ -z "$(ls -A "$dir/tmp")" ] || fail "a temporary directory is left" } -test_fetch_policy_without_script_directory() { - start_scenario "script/fetch-policy with a path writes both scripts into a clone with no script directory" +test_commit_current_upstream_policy_docs_without_script_directory() { + start_scenario "script/commit-current-upstream-policy-docs with a path writes both scripts into a clone with no script directory" commit_and_push_fixture .prettierrc remove_from_fixture script - run_fetch_policy "$dir/repo" + run_commit_current_upstream_policy_docs "$dir/repo" expect_success - expect_same_as_canonical script/fetch-policy + expect_same_as_canonical script/commit-current-upstream-policy-docs expect_same_as_canonical script/update-policy - expect_executable script/fetch-policy + expect_executable script/commit-current-upstream-policy-docs expect_executable script/update-policy expect_no_commit } -test_fetch_policy_without_argument_updates_own_repo() { - start_scenario "script/fetch-policy with no argument updates the repo it sits in" +test_commit_current_upstream_policy_docs_without_argument_updates_own_repo() { + start_scenario "script/commit-current-upstream-policy-docs with no argument updates the repo it sits in" commit_and_push_fixture .prettierrc status=0 - (cd "$dir" && PATH="$dir/bin:$PATH" repo/script/fetch-policy) \ + (cd "$dir" && PATH="$dir/bin:$PATH" \ + repo/script/commit-current-upstream-policy-docs) \ > "$dir/output" 2>&1 || status=$? expect_success expect_file .prettierrc << 'EOF' @@ -571,19 +576,19 @@ EOF expect_no_commit } -test_fetch_policy_bad_target() { - start_scenario "script/fetch-policy refuses a path that is not the top of a clone" +test_commit_current_upstream_policy_docs_bad_target() { + start_scenario "script/commit-current-upstream-policy-docs refuses a path that is not the top of a clone" commit_and_push_fixture .prettierrc - run_fetch_policy "$dir/repo/script" + run_commit_current_upstream_policy_docs "$dir/repo/script" expect_failure expect_output "not the top of a git clone" - run_fetch_policy "$dir/bin" + run_commit_current_upstream_policy_docs "$dir/bin" expect_failure expect_output "not the top of a git clone" - run_fetch_policy "$dir/no-such-directory" + run_commit_current_upstream_policy_docs "$dir/no-such-directory" expect_failure expect_output "no such directory" - run_fetch_policy "$dir/repo" extra + run_commit_current_upstream_policy_docs "$dir/repo" extra expect_failure expect_output "usage:" expect_nothing_written @@ -592,7 +597,7 @@ test_fetch_policy_bad_target() { test_failed_fetch_stops() { for url in \ "/api/v1/repos/sneak/prompts/branches/main" \ - "/raw/branch/main/script/fetch-policy" \ + "/raw/branch/main/script/commit-current-upstream-policy-docs" \ "/compare/" \ "/raw/commit/"; do start_scenario "a failed fetch of $url stops with nothing written" @@ -662,14 +667,14 @@ EOF } test_untracked_script_stops() { - start_scenario "an untracked script/fetch-policy is not overwritten" - rm "$dir/repo/script/fetch-policy" + start_scenario "an untracked script/commit-current-upstream-policy-docs is not overwritten" + rm "$dir/repo/script/commit-current-upstream-policy-docs" commit_and_push_fixture .prettierrc - echo "our own script" > "$dir/repo/script/fetch-policy" - run_fetch_policy "$dir/repo" + echo "our own script" > "$dir/repo/script/commit-current-upstream-policy-docs" + run_commit_current_upstream_policy_docs "$dir/repo" expect_failure - expect_output "script/fetch-policy has uncommitted changes" - expect_file script/fetch-policy << 'EOF' + expect_output "script/commit-current-upstream-policy-docs has uncommitted changes" + expect_file script/commit-current-upstream-policy-docs << 'EOF' our own script EOF expect_no_commit @@ -681,7 +686,7 @@ test_ignored_file_stops() { remove_from_fixture docs/API_POLICIES.md echo "docs/API_POLICIES.md" >> "$dir/repo/.git/info/exclude" echo "our own notes" > "$dir/repo/docs/API_POLICIES.md" - run_fetch_policy "$dir/repo" + run_commit_current_upstream_policy_docs "$dir/repo" expect_failure expect_output "docs/API_POLICIES.md is ignored by git" expect_file docs/API_POLICIES.md << 'EOF' @@ -691,7 +696,8 @@ EOF } test_own_script_stops() { - for script in script/fetch-policy script/update-policy; do + for script in script/commit-current-upstream-policy-docs \ + script/update-policy; do start_scenario "a tracked $script with a line the canonical file has never held stops" echo "echo our own script" >> "$dir/repo/$script" commit_and_push_fixture .prettierrc @@ -849,9 +855,10 @@ test_former_canonical_gitignore_line_replaced() { } test_former_canonical_script_line_replaced() { - start_scenario "a script/fetch-policy line an earlier canonical commit held is replaced" - expect_former_canonical_line_replaced script/fetch-policy "echo an older version" - expect_same_as_canonical script/fetch-policy + start_scenario "a script/commit-current-upstream-policy-docs line an earlier canonical commit held is replaced" + expect_former_canonical_line_replaced \ + script/commit-current-upstream-policy-docs "echo an older version" + expect_same_as_canonical script/commit-current-upstream-policy-docs } # A line another file's history removed does not count as held by this @@ -879,12 +886,12 @@ main() { test_language_files_need_a_source_file test_javascript_source_brings_its_styleguide test_nothing_changed - test_target_lacking_fetch_policy_gets_both_scripts + test_target_lacking_commit_current_upstream_policy_docs_gets_both_scripts test_update_policy_replaces_itself - test_fetch_policy_with_path_commits_nothing - test_fetch_policy_without_script_directory - test_fetch_policy_without_argument_updates_own_repo - test_fetch_policy_bad_target + test_commit_current_upstream_policy_docs_with_path_commits_nothing + test_commit_current_upstream_policy_docs_without_script_directory + test_commit_current_upstream_policy_docs_without_argument_updates_own_repo + test_commit_current_upstream_policy_docs_bad_target test_failed_fetch_stops test_one_missing_file_stops_all test_branch_response_without_hash_stops -- 2.54.0 From f0ec2f38a8d934090e35a31778416152967799d4 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 11 Nov 2026 00:47:30 +0000 Subject: [PATCH 11/12] Rename the scripts that update the policy files script/commit-current-upstream-policy-docs is now script/write-upstream-policy-files: it writes the policy files and commits nothing. script/update-policy is now script/commit-upstream-policy-files, and make update-policy is now make commit-upstream-policy-files. test/update-policy.sh follows. Behaviour is unchanged. Model: opus-5-5 --- Dockerfile | 2 +- Makefile | 10 +- README.md | 18 +- TODO.md | 12 +- prompts/EXISTING_REPO_CHECKLIST.md | 36 +-- prompts/NEW_REPO_CHECKLIST.md | 13 +- prompts/REPO_POLICIES.md | 48 ++-- script/commit-upstream-policy-files | 45 ++++ script/update-policy | 45 ---- ...olicy-docs => write-upstream-policy-files} | 22 +- ...icy.sh => commit-upstream-policy-files.sh} | 221 +++++++++--------- 11 files changed, 240 insertions(+), 232 deletions(-) create mode 100755 script/commit-upstream-policy-files delete mode 100755 script/update-policy rename script/{commit-current-upstream-policy-docs => write-upstream-policy-files} (93%) rename test/{update-policy.sh => commit-upstream-policy-files.sh} (78%) diff --git a/Dockerfile b/Dockerfile index e490670..ef698ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,7 +42,7 @@ RUN script/bootstrap COPY . . -RUN timeout 90 test/update-policy.sh +RUN timeout 90 test/commit-upstream-policy-files.sh # Development environment, and the last stage: a plain `docker build .` # names no target and so builds this one. Nothing is wanted from the two diff --git a/Makefile b/Makefile index 96f86de..29f7056 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks commit-current-upstream-policy-docs update-policy +.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks write-upstream-policy-files commit-upstream-policy-files # Makefile targets are thin shims; the implementations live in script/ # per the scripts-to-rule-them-all pattern (see the Entrypoints section @@ -31,8 +31,8 @@ docker: hooks: @script/install-precommit -commit-current-upstream-policy-docs: - @script/commit-current-upstream-policy-docs +write-upstream-policy-files: + @script/write-upstream-policy-files -update-policy: - @script/update-policy +commit-upstream-policy-files: + @script/commit-upstream-policy-files diff --git a/README.md b/README.md index 8ae847e..c642584 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ alpine. We provide: - `script/test` — `docker build --build-arg CACHEBUST="$(date +%s)-$$" --target test --output type=cacheonly .`, building the `test` phase of the `Dockerfile`, which runs - `test/update-policy.sh`, without writing an image + `test/commit-upstream-policy-files.sh`, without writing an image - `script/lint` — `docker build --build-arg CACHEBUST="$(date +%s)-$$" --target lint --output type=cacheonly .`, building the `lint` phase, which runs prettier over the markdown files, @@ -141,14 +141,14 @@ alpine. We provide: `script/check` - `script/install-precommit` — installs the git pre-commit hook (our own extension); `make hooks` shims to it -- `script/commit-current-upstream-policy-docs` — fetch the vendored policy files - from this repo over HTTP and write them into a repo (our own extension). It - writes into the repo given as its argument, or its own repo with no argument. - It stops when its target is this repo. -- `script/update-policy` — fetch the current - `script/commit-current-upstream-policy-docs`, run it on its own repo and - commit the result without pushing (our own extension). It is for the repos - that vendor the policy files. It stops when run here. +- `script/write-upstream-policy-files` — fetch the vendored policy files from + this repo over HTTP and write them into a repo (our own extension). It writes + into the repo given as its argument, or its own repo with no argument. It + stops when its target is this repo. +- `script/commit-upstream-policy-files` — fetch the current + `script/write-upstream-policy-files`, run it on its own repo and commit the + result without pushing (our own extension). It is for the repos that vendor + the policy files. It stops when run here. `make hooks` installs the pre-commit hook that runs `script/precommit`. diff --git a/TODO.md b/TODO.md index 75300d7..d1b6885 100644 --- a/TODO.md +++ b/TODO.md @@ -21,12 +21,12 @@ fmt-check, and commit. # Completed Steps -- 2026-11-09: `script/commit-current-upstream-policy-docs` writes the vendored - policy files into a repo over HTTP, itself and `script/update-policy` among - them (https://git.eeqj.de/sneak/prompts/issues/165). `make update-policy` now - fetches the current `script/commit-current-upstream-policy-docs`, runs it and - commits without pushing. The template repositories get both scripts in their - own issues. +- 2026-11-09: `script/write-upstream-policy-files` writes the vendored policy + files into a repo over HTTP, itself and `script/commit-upstream-policy-files` + among them (https://git.eeqj.de/sneak/prompts/issues/165). + `make commit-upstream-policy-files` replaces `make update-policy`. It fetches + the current `script/write-upstream-policy-files`, runs it and commits without + pushing. The template repositories get both scripts in their own issues. - 2026-11-09: `script/update-policy` and `make update-policy` update a repo's vendored policy files with one command (https://git.eeqj.de/sneak/prompts/issues/162). They fetch the files from diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index 38d57a7..aee413f 100644 --- a/prompts/EXISTING_REPO_CHECKLIST.md +++ b/prompts/EXISTING_REPO_CHECKLIST.md @@ -205,28 +205,30 @@ with your task. - [ ] `Makefile` exists in root — reference `https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile` - [ ] Has targets: `test`, `lint`, `fmt`, `fmt-check`, `check`, `docker`, - `hooks`, `update-policy` + `hooks`, `commit-upstream-policy-files` - [ ] Target implementations live in `script/` (scripts-to-rule-them-all); Makefile targets are thin shims calling them — model scripts at `https://git.eeqj.de/sneak/prompts/raw/branch/main/script/` - [ ] `script/precommit` exists and the pre-commit hook (installed by `script/install-precommit`, shimmed by `make hooks`) runs it -- [ ] `script/commit-current-upstream-policy-docs` and `script/update-policy` - exist, identical to the model scripts, and `make update-policy` shims to - `script/update-policy`. `make update-policy` commits without pushing, so a - person reviews and pushes the commit. A `.gitignore` or `.dockerignore` - without the comment that ends the canonical file needs no preparation. The - script moves each line the canonical file has never held to below the - canonical content. Blank lines and comments are dropped. Before its first - use, add that comment to an `.editorconfig` that has lines of its own, and - put those lines below it. Otherwise the script stops before it writes - anything, naming the file and each line. In a file that has the comment, - an entry above it which the canonical file has never held also stops the - script. It stops the same way on a line in `.golangci.yml` that the - canonical file has never held, apart from comments and the repo's entries - in the `deny` list of the `test-support` rule. It also stops on a repo's - own `script/commit-current-upstream-policy-docs` or - `script/update-policy`. +- [ ] `script/write-upstream-policy-files` and + `script/commit-upstream-policy-files` exist, identical to the model + scripts, and `make commit-upstream-policy-files` shims to + `script/commit-upstream-policy-files`. `make commit-upstream-policy-files` + commits without pushing, so a person reviews and pushes the commit. A + `.gitignore` or `.dockerignore` without the comment that ends the + canonical file needs no preparation. The script moves each line the + canonical file has never held to below the canonical content. Blank lines + and comments are dropped. Before its first use, add that comment to an + `.editorconfig` that has lines of its own, and put those lines below it. + Otherwise the script stops before it writes anything, naming the file and + each line. In a file that has the comment, an entry above it which the + canonical file has never held also stops the script. It stops the same way + on a line in `.golangci.yml` that the canonical file has never held, apart + from comments and the repo's entries in the `deny` list of the + `test-support` rule. It also stops on a repo's own + `script/write-upstream-policy-files` or + `script/commit-upstream-policy-files`. - [ ] README has an **Entrypoints** section documenting the `script/` entrypoints and linking the standard - [ ] `script/lint` and `script/test` each run diff --git a/prompts/NEW_REPO_CHECKLIST.md b/prompts/NEW_REPO_CHECKLIST.md index 7d848ba..69660f0 100644 --- a/prompts/NEW_REPO_CHECKLIST.md +++ b/prompts/NEW_REPO_CHECKLIST.md @@ -266,12 +266,13 @@ are thin shims calling them. Model scripts: - [ ] `script/install-precommit` — installs the pre-commit hook that runs `script/precommit` - [ ] `make hooks` — shims to `script/install-precommit` -- [ ] `script/commit-current-upstream-policy-docs` — fetches the vendored policy - files from the `prompts` repo over HTTP and writes them into the repo; - identical across repos -- [ ] `script/update-policy` / `make update-policy` — fetches the current - `script/commit-current-upstream-policy-docs`, runs it on the repo and - commits the result without pushing; identical across repos +- [ ] `script/write-upstream-policy-files` — fetches the vendored policy files + from the `prompts` repo over HTTP and writes them into the repo; identical + across repos +- [ ] `script/commit-upstream-policy-files` / + `make commit-upstream-policy-files` — fetches the current + `script/write-upstream-policy-files`, runs it on the repo and commits the + result without pushing; identical across repos - [ ] README **Entrypoints** section documents the scripts and links the standard diff --git a/prompts/REPO_POLICIES.md b/prompts/REPO_POLICIES.md index b93efa6..6b82a78 100644 --- a/prompts/REPO_POLICIES.md +++ b/prompts/REPO_POLICIES.md @@ -93,7 +93,8 @@ style conventions are in separate documents: - `make check` (runs `test`, `lint`, `fmt-check`) - `make docker` - `make hooks` (installs pre-commit hook) - - `make update-policy` (brings the repo up to standards and commits) + - `make commit-upstream-policy-files` (brings the repo up to standards and + commits) A model Makefile is at `https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile`. @@ -110,8 +111,8 @@ style conventions are in separate documents: - `script/fmt-check` - `script/check` - `script/docker` - - `script/commit-current-upstream-policy-docs` - - `script/update-policy` + - `script/write-upstream-policy-files` + - `script/commit-upstream-policy-files` The scripts must be POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they run in minimal containers; alpine images, for example, have no bash. Locate @@ -191,13 +192,13 @@ style conventions are in separate documents: `script/check`. - `script/install-precommit` installs the git pre-commit hook. The `make hooks` target shims to it. - - `script/commit-current-upstream-policy-docs` fetches the vendored policy - files from the `prompts` repo over HTTP and writes them into the repo it - is given, or its own repo with no argument. - - `script/update-policy` fetches the current - `script/commit-current-upstream-policy-docs` from the `prompts` repo, runs - it on its own repo and commits the result. It does not push. The - `make update-policy` target shims to it. + - `script/write-upstream-policy-files` fetches the vendored policy files + from the `prompts` repo over HTTP and writes them into the repo it is + given, or its own repo with no argument. + - `script/commit-upstream-policy-files` fetches the current + `script/write-upstream-policy-files` from the `prompts` repo, runs it on + its own repo and commits the result. It does not push. The + `make commit-upstream-policy-files` target shims to it. - `script/projectname` (literally that filename) simply outputs the project's name. @@ -937,21 +938,22 @@ style conventions are in separate documents: - Make all changes on a feature branch. You can do whatever you want on a feature branch. -- One command brings a repo up to standards: `make update-policy`. It runs - `script/update-policy`, which fetches the current - `script/commit-current-upstream-policy-docs` from the head of `main` in the - `prompts` repo and runs it on the repo. It then commits the files that changed - and does not push, so a person reviews and pushes the commit. +- One command brings a repo up to standards: + `make commit-upstream-policy-files`. It runs + `script/commit-upstream-policy-files`, which fetches the current + `script/write-upstream-policy-files` from the head of `main` in the `prompts` + repo and runs it on the repo. It then commits the files that changed and does + not push, so a person reviews and pushes the commit. - `script/commit-current-upstream-policy-docs` fetches each vendored file from + `script/write-upstream-policy-files` fetches each vendored file from `prompts` over HTTP and writes it into the repo. It skips a language's files when git tracks no source file of that language. A file the repo lacks is written whole. A `prompts/NAME.md` goes to `docs/NAME.md`, unless the repo - keeps it at `NAME.md`. It also writes - `script/commit-current-upstream-policy-docs` and `script/update-policy` into - the repo, so a repo updated once can update itself. The list of vendored - files is at the top of `script/commit-current-upstream-policy-docs`. The - script replaces each file and keeps only the repo's own entries: + keeps it at `NAME.md`. It also writes `script/write-upstream-policy-files` + and `script/commit-upstream-policy-files` into the repo, so a repo updated + once can update itself. The list of vendored files is at the top of + `script/write-upstream-policy-files`. The script replaces each file and + keeps only the repo's own entries: - in `.gitignore`, `.editorconfig` and `.dockerignore`: the lines below the comment that ends the canonical file - in a `.gitignore` or `.dockerignore` lacking that comment: every entry the @@ -1184,8 +1186,8 @@ style conventions are in separate documents: - `Makefile` - `script/` entrypoints (`bootstrap`, `setup`, `projectname`, `test`, `lint`, `fmt`, `fmt-check`, `check`, `docker`, `cibuild`, `precommit`, - `install-precommit`, `commit-current-upstream-policy-docs`, - `update-policy`) + `install-precommit`, `write-upstream-policy-files`, + `commit-upstream-policy-files`) - `Dockerfile`, `.dockerignore` - `.gitea/workflows/check.yml` - Go: `go.mod`, `go.sum`, `.golangci.yml` diff --git a/script/commit-upstream-policy-files b/script/commit-upstream-policy-files new file mode 100755 index 0000000..66987f0 --- /dev/null +++ b/script/commit-upstream-policy-files @@ -0,0 +1,45 @@ +#!/bin/sh +# script/commit-upstream-policy-files: bring this repo up to standards. +# It fetches the current script/write-upstream-policy-files from the +# prompts repo over HTTP, runs it on this repo and commits the files it +# wrote. It does not push. Identical in all repos. Our own extension to +# scripts-to-rule-them-all. +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" +WRITE_UPSTREAM_POLICY_FILES_URL="https://git.eeqj.de/sneak/prompts/raw/branch/main/script/write-upstream-policy-files" + +die() { + echo "commit-upstream-policy-files: $*" >&2 + exit 1 +} + +main() { + [ "$#" -eq 0 ] || die "usage: script/commit-upstream-policy-files" + + cd "$ROOT" + + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + + curl --fail --silent --show-error --max-time 60 \ + --output "$tmp/write-upstream-policy-files" \ + "$WRITE_UPSTREAM_POLICY_FILES_URL" || + die "cannot fetch $WRITE_UPSTREAM_POLICY_FILES_URL" + + sh "$tmp/write-upstream-policy-files" "$ROOT" > "$tmp/written" + commit="$(head -n 1 "$tmp/written")" + paths="$(tail -n +2 "$tmp/written")" + + for path in $paths; do + git add -- "$path" + done + + if git diff --cached --quiet -- $paths; then + echo "commit-upstream-policy-files: already at prompts $commit, nothing to commit" + else + git commit -m "Update policy files from prompts $commit" -- $paths + fi +} + +main "$@" diff --git a/script/update-policy b/script/update-policy deleted file mode 100755 index c4f40fd..0000000 --- a/script/update-policy +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/sh -# script/update-policy: bring this repo up to standards. It fetches the -# current script/commit-current-upstream-policy-docs from the prompts -# repo over HTTP, runs it on this repo and commits the files it wrote. -# It does not push. Identical in all repos. Our own extension to -# scripts-to-rule-them-all. -set -eu - -ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" -COMMIT_CURRENT_UPSTREAM_POLICY_DOCS_URL="https://git.eeqj.de/sneak/prompts/raw/branch/main/script/commit-current-upstream-policy-docs" - -die() { - echo "update-policy: $*" >&2 - exit 1 -} - -main() { - [ "$#" -eq 0 ] || die "usage: script/update-policy" - - cd "$ROOT" - - tmp="$(mktemp -d)" - trap 'rm -rf "$tmp"' EXIT - - curl --fail --silent --show-error --max-time 60 \ - --output "$tmp/commit-current-upstream-policy-docs" \ - "$COMMIT_CURRENT_UPSTREAM_POLICY_DOCS_URL" || - die "cannot fetch $COMMIT_CURRENT_UPSTREAM_POLICY_DOCS_URL" - - sh "$tmp/commit-current-upstream-policy-docs" "$ROOT" > "$tmp/written" - commit="$(head -n 1 "$tmp/written")" - paths="$(tail -n +2 "$tmp/written")" - - for path in $paths; do - git add -- "$path" - done - - if git diff --cached --quiet -- $paths; then - echo "update-policy: already at prompts $commit, nothing to commit" - else - git commit -m "Update policy files from prompts $commit" -- $paths - fi -} - -main "$@" diff --git a/script/commit-current-upstream-policy-docs b/script/write-upstream-policy-files similarity index 93% rename from script/commit-current-upstream-policy-docs rename to script/write-upstream-policy-files index c4e600d..e76200e 100755 --- a/script/commit-current-upstream-policy-docs +++ b/script/write-upstream-policy-files @@ -1,10 +1,10 @@ #!/bin/sh -# script/commit-current-upstream-policy-docs: write the current policy -# files from the prompts repo into a repo clone, over HTTP. It commits -# nothing; script/update-policy does that. Identical in all repos. Our -# own extension to scripts-to-rule-them-all. +# script/write-upstream-policy-files: write the current policy files +# from the prompts repo into a repo clone, over HTTP. It commits +# nothing; script/commit-upstream-policy-files does that. Identical in +# all repos. Our own extension to scripts-to-rule-them-all. # -# Usage: script/commit-current-upstream-policy-docs [path of a repo clone] +# Usage: script/write-upstream-policy-files [path of a repo clone] # Without a path it updates the repo this script sits in. # # Standard output is the full hash of the prompts commit, then each path @@ -50,12 +50,12 @@ prompts/GO_HTTP_SERVER_CONVENTIONS.md prompts/LLM_PROSE_TELLS.md prompts/NEW_REPO_CHECKLIST.md prompts/REPO_POLICIES.md -script/commit-current-upstream-policy-docs -script/update-policy +script/write-upstream-policy-files +script/commit-upstream-policy-files " die() { - echo "commit-current-upstream-policy-docs: $*" >&2 + echo "write-upstream-policy-files: $*" >&2 exit 1 } @@ -227,7 +227,7 @@ $own" main() { [ "$#" -le 1 ] || - die "usage: script/commit-current-upstream-policy-docs [path of a repo clone]" + die "usage: script/write-upstream-policy-files [path of a repo clone]" target="${1:-$(dirname "$0")/..}" cd "$target" || die "no such directory: $target" @@ -285,8 +285,8 @@ main() { mkdir -p "$(dirname "$path")" case "$path" in script/*) - # A new name and mv, because script/update-policy may - # be the script running. + # A new name and mv, because the script running may be + # script/commit-upstream-policy-files. cp "$tmp/$path" "$path.new" chmod 755 "$path.new" mv "$path.new" "$path" diff --git a/test/update-policy.sh b/test/commit-upstream-policy-files.sh similarity index 78% rename from test/update-policy.sh rename to test/commit-upstream-policy-files.sh index 9d18119..0061b63 100755 --- a/test/update-policy.sh +++ b/test/commit-upstream-policy-files.sh @@ -1,14 +1,15 @@ #!/bin/sh -# test/update-policy.sh: test script/commit-current-upstream-policy-docs -# and script/update-policy on throwaway repos. Each scenario gets one -# temp directory holding a repo, its local bare origin, a local git -# repository of the canonical files in place of prompts, and a stub curl -# that serves them, so nothing touches the network. +# test/commit-upstream-policy-files.sh: test +# script/write-upstream-policy-files and script/commit-upstream-policy-files +# on throwaway repos. Each scenario gets one temp directory holding a +# repo, its local bare origin, a local git repository of the canonical +# files in place of prompts, and a stub curl that serves them, so nothing +# touches the network. set -eu SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd -P)/script" -COMMIT_CURRENT_UPSTREAM_POLICY_DOCS="$SCRIPT_DIR/commit-current-upstream-policy-docs" -UPDATE_POLICY="$SCRIPT_DIR/update-policy" +WRITE_UPSTREAM_POLICY_FILES="$SCRIPT_DIR/write-upstream-policy-files" +COMMIT_UPSTREAM_POLICY_FILES="$SCRIPT_DIR/commit-upstream-policy-files" dir="" trap 'rm -rf "$dir"' EXIT @@ -60,8 +61,8 @@ case "$url" in "$HASH" 0000000000000000000000000000000000000000 > "$output" fi ;; - "$site/raw/branch/main/script/commit-current-upstream-policy-docs") - serve_file script/commit-current-upstream-policy-docs + "$site/raw/branch/main/script/write-upstream-policy-files") + serve_file script/write-upstream-policy-files ;; "$site/compare/$first...$HASH.patch") git -C "$canonical" format-patch --stdout --root "$HASH" > "$output" @@ -79,7 +80,7 @@ EOF write_canonical_files() { mkdir -p "$dir/canonical/prompts" "$dir/canonical/script" - cp "$COMMIT_CURRENT_UPSTREAM_POLICY_DOCS" "$UPDATE_POLICY" \ + cp "$WRITE_UPSTREAM_POLICY_FILES" "$COMMIT_UPSTREAM_POLICY_FILES" \ "$dir/canonical/script/" cat > "$dir/canonical/.gitignore" << 'EOF' *.log @@ -209,7 +210,7 @@ start_scenario() { git -C "$dir/repo" remote add origin "$dir/origin.git" mkdir "$dir/repo/script" - cp "$COMMIT_CURRENT_UPSTREAM_POLICY_DOCS" "$UPDATE_POLICY" \ + cp "$WRITE_UPSTREAM_POLICY_FILES" "$COMMIT_UPSTREAM_POLICY_FILES" \ "$dir/repo/script/" printf '#!/bin/sh\necho example\n' > "$dir/repo/script/projectname" chmod +x "$dir/repo/script/projectname" @@ -257,18 +258,19 @@ remove_from_fixture() { before="$(git -C "$dir/repo" rev-parse HEAD)" } -run_update_policy() { +run_commit_upstream_policy_files() { status=0 (cd "$dir/repo" && PATH="$dir/bin:$PATH" TMPDIR="$dir/tmp" \ - script/update-policy "$@") > "$dir/output" 2>&1 || status=$? + script/commit-upstream-policy-files "$@") > "$dir/output" 2>&1 || + status=$? } -# run_commit_current_upstream_policy_docs : run the script in -# this source tree, from outside the repo it updates. -run_commit_current_upstream_policy_docs() { +# run_write_upstream_policy_files : run the script in this +# source tree, from outside the repo it updates. +run_write_upstream_policy_files() { status=0 (cd "$dir" && PATH="$dir/bin:$PATH" TMPDIR="$dir/tmp" \ - "$COMMIT_CURRENT_UPSTREAM_POLICY_DOCS" "$@") > "$dir/output" 2>&1 || + "$WRITE_UPSTREAM_POLICY_FILES" "$@") > "$dir/output" 2>&1 || status=$? } @@ -345,7 +347,7 @@ test_own_entries_kept() { start_scenario "own entries kept in all four files" commit_and_push_fixture .gitignore .dockerignore .editorconfig \ .golangci.yml .prettierrc REPO_POLICIES.md - run_update_policy + run_commit_upstream_policy_files expect_success expect_file .gitignore << 'EOF' @@ -414,7 +416,7 @@ test_files_the_repo_lacks_are_written_whole() { remove_from_fixture REPO_POLICIES.md .gitignore .golangci.yml .gitea \ docs/API_POLICIES.md docs/CODE_STYLEGUIDE_GO.md \ docs/CODE_STYLEGUIDE_JS.md docs/CODE_STYLEGUIDE_PYTHON.md - run_update_policy + run_commit_upstream_policy_files expect_success expect_same_as_canonical .gitignore @@ -446,7 +448,7 @@ test_language_files_need_a_source_file() { remove_from_fixture cmd .golangci.yml docs/CODE_STYLEGUIDE_GO.md \ docs/GO_HTTP_SERVER_CONVENTIONS.md docs/CODE_STYLEGUIDE_JS.md \ docs/CODE_STYLEGUIDE_PYTHON.md - run_update_policy + run_commit_upstream_policy_files expect_success expect_absent .golangci.yml expect_absent docs/CODE_STYLEGUIDE_GO.md @@ -462,7 +464,7 @@ test_javascript_source_brings_its_styleguide() { echo "export {}" > "$dir/repo/web.$extension" commit_and_push_fixture "web.$extension" remove_from_fixture docs/CODE_STYLEGUIDE_JS.md - run_update_policy + run_commit_upstream_policy_files expect_success expect_file docs/CODE_STYLEGUIDE_JS.md << 'EOF' new CODE_STYLEGUIDE_JS @@ -475,37 +477,37 @@ test_nothing_changed() { start_scenario "nothing changed" cp "$dir/canonical/.prettierrc" "$dir/repo/.prettierrc" commit_and_push_fixture .prettierrc - run_update_policy + run_commit_upstream_policy_files expect_success expect_output "nothing to commit" expect_nothing_written } -test_target_lacking_commit_current_upstream_policy_docs_gets_both_scripts() { - start_scenario "a repo lacking script/commit-current-upstream-policy-docs gets it, and the current script/update-policy, in the commit" - rm "$dir/repo/script/commit-current-upstream-policy-docs" - echo "# An older version." >> "$dir/repo/script/update-policy" +test_target_lacking_write_upstream_policy_files_gets_both_scripts() { + start_scenario "a repo lacking script/write-upstream-policy-files gets it, and the current script/commit-upstream-policy-files, in the commit" + rm "$dir/repo/script/write-upstream-policy-files" + echo "# An older version." >> "$dir/repo/script/commit-upstream-policy-files" commit_and_push_fixture .prettierrc - run_update_policy + run_commit_upstream_policy_files expect_success - expect_same_as_canonical script/commit-current-upstream-policy-docs - expect_same_as_canonical script/update-policy - expect_committed_executable script/commit-current-upstream-policy-docs - expect_committed_executable script/update-policy + expect_same_as_canonical script/write-upstream-policy-files + expect_same_as_canonical script/commit-upstream-policy-files + expect_committed_executable script/write-upstream-policy-files + expect_committed_executable script/commit-upstream-policy-files expect_new_commit_not_pushed - [ "$(files_in_commit)" = ".prettierrc script/commit-current-upstream-policy-docs script/update-policy " ] || + [ "$(files_in_commit)" = ".prettierrc script/commit-upstream-policy-files script/write-upstream-policy-files " ] || fail "the commit holds other files: $(files_in_commit)" } # The shell reads a script in blocks, so the padding makes it read the # last line only after main has returned. It finds that line only if -# script/commit-current-upstream-policy-docs left the old file in place -# under a new name. The old file is an earlier canonical version, so -# script/commit-current-upstream-policy-docs replaces it. -test_update_policy_replaces_itself() { - start_scenario "script/update-policy replaces its own file and still exits 0" +# script/write-upstream-policy-files left the old file in place under a +# new name. The old file is an earlier canonical version, so +# script/write-upstream-policy-files replaces it. +test_commit_upstream_policy_files_replaces_itself() { + start_scenario "script/commit-upstream-policy-files replaces its own file and still exits 0" { - cat "$dir/repo/script/update-policy" + cat "$dir/repo/script/commit-upstream-policy-files" i=0 while [ "$i" -lt 2000 ]; do echo "# Padding." @@ -513,61 +515,62 @@ test_update_policy_replaces_itself() { done echo "echo 'the old file ran to its end'" } > "$dir/merged" - cp "$dir/merged" "$dir/canonical/script/update-policy" + cp "$dir/merged" "$dir/canonical/script/commit-upstream-policy-files" commit_canonical - cp "$UPDATE_POLICY" "$dir/canonical/script/update-policy" + cp "$COMMIT_UPSTREAM_POLICY_FILES" \ + "$dir/canonical/script/commit-upstream-policy-files" commit_canonical - mv "$dir/merged" "$dir/repo/script/update-policy" - chmod 755 "$dir/repo/script/update-policy" + mv "$dir/merged" "$dir/repo/script/commit-upstream-policy-files" + chmod 755 "$dir/repo/script/commit-upstream-policy-files" commit_and_push_fixture - run_update_policy + run_commit_upstream_policy_files expect_success expect_output "the old file ran to its end" - expect_same_as_canonical script/update-policy - expect_executable script/update-policy + expect_same_as_canonical script/commit-upstream-policy-files + expect_executable script/commit-upstream-policy-files expect_new_commit_not_pushed } -test_commit_current_upstream_policy_docs_with_path_commits_nothing() { - start_scenario "script/commit-current-upstream-policy-docs with a path writes into that clone and commits nothing" - rm "$dir/repo/script/commit-current-upstream-policy-docs" \ - "$dir/repo/script/update-policy" +test_write_upstream_policy_files_with_path_commits_nothing() { + start_scenario "script/write-upstream-policy-files with a path writes into that clone and commits nothing" + rm "$dir/repo/script/write-upstream-policy-files" \ + "$dir/repo/script/commit-upstream-policy-files" commit_and_push_fixture .prettierrc - run_commit_current_upstream_policy_docs "$dir/repo" + run_write_upstream_policy_files "$dir/repo" expect_success expect_file .prettierrc << 'EOF' { "tabWidth": 4 } EOF - expect_same_as_canonical script/commit-current-upstream-policy-docs - expect_same_as_canonical script/update-policy - expect_executable script/commit-current-upstream-policy-docs - expect_executable script/update-policy + expect_same_as_canonical script/write-upstream-policy-files + expect_same_as_canonical script/commit-upstream-policy-files + expect_executable script/write-upstream-policy-files + expect_executable script/commit-upstream-policy-files expect_no_commit [ "$(head -n 1 "$dir/output")" = "$HASH" ] || fail "the first line of output is not the hash" - expect_output "script/commit-current-upstream-policy-docs" + expect_output "script/write-upstream-policy-files" [ -z "$(ls -A "$dir/tmp")" ] || fail "a temporary directory is left" } -test_commit_current_upstream_policy_docs_without_script_directory() { - start_scenario "script/commit-current-upstream-policy-docs with a path writes both scripts into a clone with no script directory" +test_write_upstream_policy_files_without_script_directory() { + start_scenario "script/write-upstream-policy-files with a path writes both scripts into a clone with no script directory" commit_and_push_fixture .prettierrc remove_from_fixture script - run_commit_current_upstream_policy_docs "$dir/repo" + run_write_upstream_policy_files "$dir/repo" expect_success - expect_same_as_canonical script/commit-current-upstream-policy-docs - expect_same_as_canonical script/update-policy - expect_executable script/commit-current-upstream-policy-docs - expect_executable script/update-policy + expect_same_as_canonical script/write-upstream-policy-files + expect_same_as_canonical script/commit-upstream-policy-files + expect_executable script/write-upstream-policy-files + expect_executable script/commit-upstream-policy-files expect_no_commit } -test_commit_current_upstream_policy_docs_without_argument_updates_own_repo() { - start_scenario "script/commit-current-upstream-policy-docs with no argument updates the repo it sits in" +test_write_upstream_policy_files_without_argument_updates_own_repo() { + start_scenario "script/write-upstream-policy-files with no argument updates the repo it sits in" commit_and_push_fixture .prettierrc status=0 (cd "$dir" && PATH="$dir/bin:$PATH" \ - repo/script/commit-current-upstream-policy-docs) \ + repo/script/write-upstream-policy-files) \ > "$dir/output" 2>&1 || status=$? expect_success expect_file .prettierrc << 'EOF' @@ -576,19 +579,19 @@ EOF expect_no_commit } -test_commit_current_upstream_policy_docs_bad_target() { - start_scenario "script/commit-current-upstream-policy-docs refuses a path that is not the top of a clone" +test_write_upstream_policy_files_bad_target() { + start_scenario "script/write-upstream-policy-files refuses a path that is not the top of a clone" commit_and_push_fixture .prettierrc - run_commit_current_upstream_policy_docs "$dir/repo/script" + run_write_upstream_policy_files "$dir/repo/script" expect_failure expect_output "not the top of a git clone" - run_commit_current_upstream_policy_docs "$dir/bin" + run_write_upstream_policy_files "$dir/bin" expect_failure expect_output "not the top of a git clone" - run_commit_current_upstream_policy_docs "$dir/no-such-directory" + run_write_upstream_policy_files "$dir/no-such-directory" expect_failure expect_output "no such directory" - run_commit_current_upstream_policy_docs "$dir/repo" extra + run_write_upstream_policy_files "$dir/repo" extra expect_failure expect_output "usage:" expect_nothing_written @@ -597,13 +600,13 @@ test_commit_current_upstream_policy_docs_bad_target() { test_failed_fetch_stops() { for url in \ "/api/v1/repos/sneak/prompts/branches/main" \ - "/raw/branch/main/script/commit-current-upstream-policy-docs" \ + "/raw/branch/main/script/write-upstream-policy-files" \ "/compare/" \ "/raw/commit/"; do start_scenario "a failed fetch of $url stops with nothing written" commit_and_push_fixture .gitignore .prettierrc REPO_POLICIES.md FAIL_URL="$url" - run_update_policy + run_commit_upstream_policy_files expect_failure expect_output "cannot fetch" expect_nothing_written @@ -614,7 +617,7 @@ test_one_missing_file_stops_all() { start_scenario "one file missing from prompts stops before any file is written" rm "$dir/canonical/prompts/REPO_POLICIES.md" commit_and_push_fixture .prettierrc REPO_POLICIES.md - run_update_policy + run_commit_upstream_policy_files expect_failure expect_output "cannot fetch" expect_nothing_written @@ -624,7 +627,7 @@ test_branch_response_without_hash_stops() { start_scenario "a branch response with no commit hash stops" commit_and_push_fixture .prettierrc BAD_BRANCH_JSON=yes - run_update_policy + run_commit_upstream_policy_files expect_failure expect_output "no commit hash" expect_nothing_written @@ -642,7 +645,7 @@ test_own_entry_above_closing_comment() { mv "$dir/merged" "$dir/repo/$file" commit_and_push_fixture .gitignore .dockerignore .editorconfig \ .prettierrc REPO_POLICIES.md - run_update_policy + run_commit_upstream_policy_files expect_failure expect_output "$file has entries" expect_output "/myapp" @@ -656,7 +659,7 @@ test_uncommitted_change_stops() { start_scenario "a carried file with uncommitted changes stops" commit_and_push_fixture .prettierrc REPO_POLICIES.md echo "uncommitted" >> "$dir/repo/REPO_POLICIES.md" - run_update_policy + run_commit_upstream_policy_files expect_failure expect_output "REPO_POLICIES.md has uncommitted changes" expect_file REPO_POLICIES.md << 'EOF' @@ -667,14 +670,14 @@ EOF } test_untracked_script_stops() { - start_scenario "an untracked script/commit-current-upstream-policy-docs is not overwritten" - rm "$dir/repo/script/commit-current-upstream-policy-docs" + start_scenario "an untracked script/write-upstream-policy-files is not overwritten" + rm "$dir/repo/script/write-upstream-policy-files" commit_and_push_fixture .prettierrc - echo "our own script" > "$dir/repo/script/commit-current-upstream-policy-docs" - run_commit_current_upstream_policy_docs "$dir/repo" + echo "our own script" > "$dir/repo/script/write-upstream-policy-files" + run_write_upstream_policy_files "$dir/repo" expect_failure - expect_output "script/commit-current-upstream-policy-docs has uncommitted changes" - expect_file script/commit-current-upstream-policy-docs << 'EOF' + expect_output "script/write-upstream-policy-files has uncommitted changes" + expect_file script/write-upstream-policy-files << 'EOF' our own script EOF expect_no_commit @@ -686,7 +689,7 @@ test_ignored_file_stops() { remove_from_fixture docs/API_POLICIES.md echo "docs/API_POLICIES.md" >> "$dir/repo/.git/info/exclude" echo "our own notes" > "$dir/repo/docs/API_POLICIES.md" - run_commit_current_upstream_policy_docs "$dir/repo" + run_write_upstream_policy_files "$dir/repo" expect_failure expect_output "docs/API_POLICIES.md is ignored by git" expect_file docs/API_POLICIES.md << 'EOF' @@ -696,12 +699,12 @@ EOF } test_own_script_stops() { - for script in script/commit-current-upstream-policy-docs \ - script/update-policy; do + for script in script/write-upstream-policy-files \ + script/commit-upstream-policy-files; do start_scenario "a tracked $script with a line the canonical file has never held stops" echo "echo our own script" >> "$dir/repo/$script" commit_and_push_fixture .prettierrc - run_update_policy + run_commit_upstream_policy_files expect_failure expect_output "$script has lines" expect_output "echo our own script" @@ -713,7 +716,7 @@ test_gitignore_lacks_canonical_last_line() { start_scenario ".gitignore lacking the canonical last line keeps the repo's own line below the canonical content" printf '*.log\n\n# A comment of our own.\n/myapp\n' > "$dir/repo/.gitignore" commit_and_push_fixture .gitignore .prettierrc - run_update_policy + run_commit_upstream_policy_files expect_success expect_file .gitignore << 'EOF' *.log @@ -736,7 +739,7 @@ test_editorconfig_lacks_canonical_last_line() { start_scenario ".editorconfig lacking the canonical last line with a line of its own stops" printf 'root = true\n[*.py]\nindent_size = 2\n' > "$dir/repo/.editorconfig" commit_and_push_fixture .editorconfig .prettierrc - run_update_policy + run_commit_upstream_policy_files expect_failure expect_output ".editorconfig lacks this line" expect_output "[*.py]" @@ -766,7 +769,7 @@ linters: - pkg: example.com/other EOF commit_and_push_fixture .golangci.yml .prettierrc - run_update_policy + run_commit_upstream_policy_files expect_failure expect_output "no deny list in its test-support rule" expect_nothing_written @@ -781,7 +784,7 @@ test_own_golangci_setting_stops() { - pattern: model.Operator EOF commit_and_push_fixture .golangci.yml .prettierrc - run_update_policy + run_commit_upstream_policy_files expect_failure expect_output ".golangci.yml has lines" expect_output "forbidigo:" @@ -795,7 +798,7 @@ test_source_repo_stops() { mkdir "$dir/repo/prompts" echo "policy" > "$dir/repo/prompts/REPO_POLICIES.md" commit_and_push_fixture .prettierrc prompts/REPO_POLICIES.md - run_update_policy + run_commit_upstream_policy_files expect_failure expect_output "this repo is the source of the policy files" expect_nothing_written @@ -806,7 +809,7 @@ test_staged_file_stays_out_of_commit() { commit_and_push_fixture .prettierrc echo "notes" > "$dir/repo/notes.txt" git -C "$dir/repo" add notes.txt - run_update_policy + run_commit_upstream_policy_files expect_success [ "$(files_in_commit)" = ".prettierrc " ] || fail "the commit holds more than the policy file" @@ -814,10 +817,10 @@ test_staged_file_stays_out_of_commit() { fail "notes.txt is no longer staged" } -test_update_policy_takes_no_argument() { - start_scenario "script/update-policy refuses an argument" +test_commit_upstream_policy_files_takes_no_argument() { + start_scenario "script/commit-upstream-policy-files refuses an argument" commit_and_push_fixture .prettierrc - run_update_policy "$HASH" + run_commit_upstream_policy_files "$HASH" expect_failure expect_output "usage:" expect_nothing_written @@ -836,7 +839,7 @@ expect_former_canonical_line_replaced() { mv "$dir/merged" "$dir/repo/$1" commit_and_push_fixture "$1" .prettierrc - run_update_policy + run_commit_upstream_policy_files expect_success if grep -q -F -x -e "$2" "$dir/repo/$1"; then fail "$1 still has: $2" @@ -855,10 +858,10 @@ test_former_canonical_gitignore_line_replaced() { } test_former_canonical_script_line_replaced() { - start_scenario "a script/commit-current-upstream-policy-docs line an earlier canonical commit held is replaced" + start_scenario "a script/write-upstream-policy-files line an earlier canonical commit held is replaced" expect_former_canonical_line_replaced \ - script/commit-current-upstream-policy-docs "echo an older version" - expect_same_as_canonical script/commit-current-upstream-policy-docs + script/write-upstream-policy-files "echo an older version" + expect_same_as_canonical script/write-upstream-policy-files } # A line another file's history removed does not count as held by this @@ -873,7 +876,7 @@ test_line_removed_from_another_file_still_stops() { { echo "*.old"; cat "$dir/repo/.gitignore"; } > "$dir/merged" mv "$dir/merged" "$dir/repo/.gitignore" commit_and_push_fixture .gitignore .prettierrc - run_update_policy + run_commit_upstream_policy_files expect_failure expect_output ".gitignore has entries" expect_output "*.old" @@ -886,12 +889,12 @@ main() { test_language_files_need_a_source_file test_javascript_source_brings_its_styleguide test_nothing_changed - test_target_lacking_commit_current_upstream_policy_docs_gets_both_scripts - test_update_policy_replaces_itself - test_commit_current_upstream_policy_docs_with_path_commits_nothing - test_commit_current_upstream_policy_docs_without_script_directory - test_commit_current_upstream_policy_docs_without_argument_updates_own_repo - test_commit_current_upstream_policy_docs_bad_target + test_target_lacking_write_upstream_policy_files_gets_both_scripts + test_commit_upstream_policy_files_replaces_itself + test_write_upstream_policy_files_with_path_commits_nothing + test_write_upstream_policy_files_without_script_directory + test_write_upstream_policy_files_without_argument_updates_own_repo + test_write_upstream_policy_files_bad_target test_failed_fetch_stops test_one_missing_file_stops_all test_branch_response_without_hash_stops @@ -908,12 +911,12 @@ main() { test_own_golangci_setting_stops test_source_repo_stops test_staged_file_stays_out_of_commit - test_update_policy_takes_no_argument + test_commit_upstream_policy_files_takes_no_argument test_former_canonical_golangci_line_replaced test_former_canonical_gitignore_line_replaced test_former_canonical_script_line_replaced test_line_removed_from_another_file_still_stops - echo "update-policy tests passed" + echo "commit-upstream-policy-files tests passed" } main "$@" -- 2.54.0 From da50899b23dc5f251a9e742382d89abf5683cb19 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 11 Nov 2026 01:11:39 +0000 Subject: [PATCH 12/12] Drop the test timeout from the Dockerfile; date the changed documents Time limits go only in the CI workflow, so the test phase runs test/commit-upstream-policy-files.sh with no timeout. REPO_POLICIES.md and both checklists change in this PR, so their last_modified is 2026-11-11. Model: opus-5-5 --- Dockerfile | 2 +- prompts/EXISTING_REPO_CHECKLIST.md | 2 +- prompts/NEW_REPO_CHECKLIST.md | 2 +- prompts/REPO_POLICIES.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index ef698ed..d823881 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,7 +42,7 @@ RUN script/bootstrap COPY . . -RUN timeout 90 test/commit-upstream-policy-files.sh +RUN test/commit-upstream-policy-files.sh # Development environment, and the last stage: a plain `docker build .` # names no target and so builds this one. Nothing is wanted from the two diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index aee413f..d329e28 100644 --- a/prompts/EXISTING_REPO_CHECKLIST.md +++ b/prompts/EXISTING_REPO_CHECKLIST.md @@ -1,6 +1,6 @@ --- title: Existing Repo Checklist -last_modified: 2026-11-09 +last_modified: 2026-11-11 --- Use this checklist when beginning work in a repo that may not yet conform to our diff --git a/prompts/NEW_REPO_CHECKLIST.md b/prompts/NEW_REPO_CHECKLIST.md index 69660f0..be79a8c 100644 --- a/prompts/NEW_REPO_CHECKLIST.md +++ b/prompts/NEW_REPO_CHECKLIST.md @@ -1,6 +1,6 @@ --- title: New Repo Checklist -last_modified: 2026-11-09 +last_modified: 2026-11-11 --- Use this checklist when creating a new repository from scratch. Follow the steps diff --git a/prompts/REPO_POLICIES.md b/prompts/REPO_POLICIES.md index 6b82a78..d1927e9 100644 --- a/prompts/REPO_POLICIES.md +++ b/prompts/REPO_POLICIES.md @@ -1,6 +1,6 @@ --- title: Repository Policies -last_modified: 2026-11-09 +last_modified: 2026-11-11 --- This document covers repository structure, tooling, and workflow standards. Code -- 2.54.0