From ebdd967e344a29a797e6b661c555e1c597ea6717 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 9 Nov 2026 07:30:32 +0000 Subject: [PATCH] Add script/update-policy to re-vendor policy files (closes #162) One command fetches the vendored files from prompts at a commit, keeps a repo's own entries, commits and pushes. The canonical .dockerignore gains the closing comment the script needs. Model: opus-5-5 --- .dockerignore | 4 + Makefile | 5 +- README.md | 3 + TODO.md | 6 + prompts/EXISTING_REPO_CHECKLIST.md | 9 +- prompts/NEW_REPO_CHECKLIST.md | 7 +- prompts/REPO_POLICIES.md | 16 ++- script/update-policy | 188 +++++++++++++++++++++++++++++ 8 files changed, 232 insertions(+), 6 deletions(-) create mode 100755 script/update-policy diff --git a/.dockerignore b/.dockerignore index 7f133c5..d3cc0a8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -76,3 +76,7 @@ **/.idea **/.vscode **/*.sublime-* + +# This repository's own entries, such as its build outputs, go below +# this comment, and a re-vendor keeps them. Anchor a binary built at the +# root: `/myapp`, never `**/myapp`, which also matches `cmd/myapp/`. diff --git a/Makefile b/Makefile index dde6752..6488473 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks +.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks update-policy # Makefile targets are thin shims; the implementations live in script/ # per the scripts-to-rule-them-all pattern (see the Entrypoints section @@ -30,3 +30,6 @@ docker: hooks: @script/install-precommit + +update-policy: + @script/update-policy diff --git a/README.md b/README.md index e2cdfe0..12a4873 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,9 @@ 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); run in a repo that vendors them, 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 d68a307..3eb1e5b 100644 --- a/TODO.md +++ b/TODO.md @@ -21,6 +21,12 @@ fmt-check, and commit. # Completed Steps +- 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 + `prompts`, keep the repo's own entries, commit and push. The canonical + `.dockerignore` now ends with the comment that the repo's own entries go + below. The template repositories get the script in their own issues. - 2026-11-07: Go code is now formatted with `gofmt` and then `golines -m 77` (https://git.eeqj.de/sneak/prompts/issues/160). `prompts/REPO_POLICIES.md` says how `script/fmt`, `script/fmt-check` and `script/bootstrap` do it and diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index 51933a7..465ae3a 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-07 +last_modified: 2026-11-09 --- Use this checklist when beginning work in a repo that may not yet conform to our @@ -205,12 +205,17 @@ 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` + `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/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. Entries above that comment are + replaced. - [ ] 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 0332076..ff769b1 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-07 +last_modified: 2026-11-09 --- Use this checklist when creating a new repository from scratch. Follow the steps @@ -62,6 +62,8 @@ Template files can be fetched from: `CLAUDE.md` or `.claude/`, and never separate memory files - [ ] `Dockerfile` and `.dockerignore` — fetch `.dockerignore` from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore` + - The repo's own entries go at the end of the file, where a re-vendor keeps + them. - Extend `.dockerignore` with the repo's own host-built artifacts, giving every depth-independent pattern a `**/` prefix — but write a repo-root binary anchored, `/myapp` and never `**/myapp`, which would also match @@ -264,6 +266,9 @@ 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 + 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 ffceb30..076afae 100644 --- a/prompts/REPO_POLICIES.md +++ b/prompts/REPO_POLICIES.md @@ -1,6 +1,6 @@ --- title: Repository Policies -last_modified: 2026-11-07 +last_modified: 2026-11-09 --- This document covers repository structure, tooling, and workflow standards. Code @@ -109,6 +109,7 @@ style conventions are in separate documents: - `script/fmt-check` - `script/check` - `script/docker` + - `script/update-policy` 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 @@ -927,6 +928,17 @@ 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: + - 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 + `test-support` depguard rule + - `.golangci.yml` is standardized. The vendored copy in a consuming repo must _NEVER_ be modified by an agent. Fetch it from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml` and keep it @@ -1152,7 +1164,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-precommit`, `update-policy`) - `Dockerfile`, `.dockerignore` - `.gitea/workflows/check.yml` - Go: `go.mod`, `go.sum`, `.golangci.yml` diff --git a/script/update-policy b/script/update-policy new file mode 100755 index 0000000..9ad17f0 --- /dev/null +++ b/script/update-policy @@ -0,0 +1,188 @@ +#!/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 +# - .golangci.yml: the entries the repo added to the deny list of the +# test-support rule +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 +" + +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 +} + +# keep_lines_below_last : append to the fetched file +# whatever follows its last line in the local file. +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 + 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. +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 + } + + /^ *test-support:/ { in_rule = 1 } + 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" +} + +main() { + 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 + + 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" + carried="$carried $path" + done + [ -n "$carried" ] || die "this repo carries none of the vendored files" + + 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" + git add -- "$path" + done + + if git diff --cached --quiet -- $carried; then + echo "update-policy: already at prompts $commit, nothing to commit" + exit 0 + fi + + git commit -m "Update policy files from prompts $commit" -- $carried + git push origin HEAD || + die "push failed; the commit exists locally, so fix that and run git push" +} + +main "$@"