Author SHA1 Message Date
sneak b6d5d2d6f8 Add script/update-policy to re-vendor policy files (closes #162)
check / check (push) Successful in 17s
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
2026-11-09 07:33:11 +00:00
clawbot d673785d33 Format Go with gofmt, then golines -m 77 (#161)
check / check (push) Successful in 24s
2026-11-07 05:21:32 +01:00
9 changed files with 278 additions and 17 deletions

No files matched your search

+4
View File
@@ -76,3 +76,7 @@
**/.idea **/.idea
**/.vscode **/.vscode
**/*.sublime-* **/*.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/`.
+4 -1
View File
@@ -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/ # Makefile targets are thin shims; the implementations live in script/
# per the scripts-to-rule-them-all pattern (see the Entrypoints section # per the scripts-to-rule-them-all pattern (see the Entrypoints section
@@ -30,3 +30,6 @@ docker:
hooks: hooks:
@script/install-precommit @script/install-precommit
update-policy:
@script/update-policy
+3
View File
@@ -141,6 +141,9 @@ alpine. We provide:
`script/check` `script/check`
- `script/install-precommit` — installs the git pre-commit hook (our own - `script/install-precommit` — installs the git pre-commit hook (our own
extension); `make hooks` shims to it 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
`make hooks` installs the pre-commit hook that runs `script/precommit`. `make hooks` installs the pre-commit hook that runs `script/precommit`.
+11
View File
@@ -21,6 +21,17 @@ fmt-check, and commit.
# Completed Steps # 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
pins `golines`. The Go style guide and both checklists say the same. `lll`
stays at 88, since `golines` leaves long strings and comments as they are.
- 2026-11-06: The pinning rule in `prompts/REPO_POLICIES.md` now opens with its - 2026-11-06: The pinning rule in `prompts/REPO_POLICIES.md` now opens with its
scope: what we download and install ourselves in scripts and Dockerfiles scope: what we download and install ourselves in scripts and Dockerfiles
(https://git.eeqj.de/sneak/prompts/issues/154). Distribution packages are (https://git.eeqj.de/sneak/prompts/issues/154). Distribution packages are
+8 -5
View File
@@ -1,13 +1,16 @@
--- ---
title: Code Styleguide — Go title: Code Styleguide — Go
last_modified: 2026-10-18 last_modified: 2026-11-07
--- ---
1. Try to hard wrap long lines at 77 characters or less. 1. Format Go code with `gofmt` and then `golines -m 77`, through `make fmt`.
Don't commit anything `make fmt` would change. The only exception is when
committing things that aren't yet syntactically valid, which should only
happen pre-v0.0.1 or on a non-`main` branch.
1. Don't commit anything that hasn't been `go fmt`'d. The only exception is 1. Hard wrap long lines at 77 characters or less. `golines` does this for code.
when committing things that aren't yet syntactically valid, which should Wrap comments and long string literals by hand, since `golines` leaves them
only happen pre-v0.0.1 or on a non-`main` branch. as they are.
1. Even if you are planning to deal with only positive integers, use 1. Even if you are planning to deal with only positive integers, use
`int`/`int64` types instead of `uint`/`uint64` types. This is for `int`/`int64` types instead of `uint`/`uint64` types. This is for
+14 -4
View File
@@ -1,6 +1,6 @@
--- ---
title: Existing Repo Checklist title: Existing Repo Checklist
last_modified: 2026-11-06 last_modified: 2026-11-09
--- ---
Use this checklist when beginning work in a repo that may not yet conform to our 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 - [ ] `Makefile` exists in root — reference
`https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile` `https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile`
- [ ] Has targets: `test`, `lint`, `fmt`, `fmt-check`, `check`, `docker`, - [ ] Has targets: `test`, `lint`, `fmt`, `fmt-check`, `check`, `docker`,
`hooks` `hooks`, `update-policy`
- [ ] Target implementations live in `script/` (scripts-to-rule-them-all); - [ ] Target implementations live in `script/` (scripts-to-rule-them-all);
Makefile targets are thin shims calling them — model scripts at Makefile targets are thin shims calling them — model scripts at
`https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>` `https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>`
- [ ] `script/precommit` exists and the pre-commit hook (installed by - [ ] `script/precommit` exists and the pre-commit hook (installed by
`script/install-precommit`, shimmed by `make hooks`) runs it `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/` - [ ] README has an **Entrypoints** section documenting the `script/`
entrypoints and linking the standard entrypoints and linking the standard
- [ ] `script/lint` and `script/test` each run - [ ] `script/lint` and `script/test` each run
@@ -259,8 +264,13 @@ with your task.
# Formatting # Formatting
- [ ] Platform-standard formatter is configured (`black`, `prettier`, `go fmt`) - [ ] Platform-standard formatter is configured (`black`, `prettier`, and for Go
- [ ] Default formatter config, only exception: four-space indents (except Go) `gofmt` then `golines -m 77`)
- [ ] Default formatter config, except four-space indents (not Go),
`proseWrap: always` for Markdown, and the Go 77-column limit
- [ ] Go: `script/bootstrap` installs `golines` with `go install` pinned to a
commit hash; `script/fmt-check` fails when `gofmt` or `golines -m 77`
would change a file
- [ ] All files pass `make fmt-check` - [ ] All files pass `make fmt-check`
# Git Hygiene # Git Hygiene
+10 -3
View File
@@ -1,6 +1,6 @@
--- ---
title: New Repo Checklist title: New Repo Checklist
last_modified: 2026-11-06 last_modified: 2026-11-09
--- ---
Use this checklist when creating a new repository from scratch. Follow the steps 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 `CLAUDE.md` or `.claude/`, and never separate memory files
- [ ] `Dockerfile` and `.dockerignore` — fetch `.dockerignore` from - [ ] `Dockerfile` and `.dockerignore` — fetch `.dockerignore` from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore` `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 - Extend `.dockerignore` with the repo's own host-built artifacts, giving
every depth-independent pattern a `**/` prefix — but write a repo-root every depth-independent pattern a `**/` prefix — but write a repo-root
binary anchored, `/myapp` and never `**/myapp`, which would also match binary anchored, `/myapp` and never `**/myapp`, which would also match
@@ -233,9 +235,11 @@ are thin shims calling them. Model scripts:
which writes no image. No lint verdict may come from a host invocation of which writes no image. No lint verdict may come from a host invocation of
the linter. the linter.
- [ ] `script/fmt` / `make fmt` — formats code (writes; native, never in a - [ ] `script/fmt` / `make fmt` — formats code (writes; native, never in a
container) container). Go code gets `gofmt`, then `golines -m 77`.
- [ ] `script/fmt-check` / `make fmt-check` — checks formatting (read-only; - [ ] `script/fmt-check` / `make fmt-check` — checks formatting (read-only;
native) native). Fails when `gofmt` or `golines -m 77` would change a Go file.
- [ ] Go: `script/bootstrap` installs `golines` with `go install` pinned to a
commit hash
- [ ] `script/check` / `make check` — runs `test`, `lint`, `fmt-check`; must not - [ ] `script/check` / `make check` — runs `test`, `lint`, `fmt-check`; must not
modify files modify files
- [ ] `script/projectname` — outputs the project name (used by `script/docker` - [ ] `script/projectname` — outputs the project name (used by `script/docker`
@@ -262,6 +266,9 @@ are thin shims calling them. Model scripts:
- [ ] `script/install-precommit` — installs the pre-commit hook that runs - [ ] `script/install-precommit` — installs the pre-commit hook that runs
`script/precommit` `script/precommit`
- [ ] `make hooks` — shims to `script/install-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 - [ ] README **Entrypoints** section documents the scripts and links the
standard standard
+36 -4
View File
@@ -1,6 +1,6 @@
--- ---
title: Repository Policies title: Repository Policies
last_modified: 2026-11-06 last_modified: 2026-11-09
--- ---
This document covers repository structure, tooling, and workflow standards. Code This document covers repository structure, tooling, and workflow standards. Code
@@ -109,6 +109,7 @@ style conventions are in separate documents:
- `script/fmt-check` - `script/fmt-check`
- `script/check` - `script/check`
- `script/docker` - `script/docker`
- `script/update-policy`
The scripts must be POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they 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 run in minimal containers; alpine images, for example, have no bash. Locate
@@ -675,11 +676,31 @@ style conventions are in separate documents:
- Use platform-standard formatters: - Use platform-standard formatters:
- `black` for Python - `black` for Python
- `prettier` for JS/CSS/Markdown/HTML - `prettier` for JS/CSS/Markdown/HTML
- `go fmt` for Go - `gofmt`, then `golines -m 77`, for Go
Always use default configuration, with two exceptions: Always use default configuration, with three exceptions:
- four-space indents (except Go) - four-space indents (except Go)
- `proseWrap: always` for Markdown (hard-wrap at 80 columns) - `proseWrap: always` for Markdown (hard-wrap at 80 columns)
- a 77-column limit for Go, set by `golines -m 77`
`script/fmt` runs `gofmt` and then `golines -m 77` on Go code.
`script/fmt-check` fails when either of them would change a file. `golines`
comes from `github.com/segmentio/golines`. `script/bootstrap` installs it
with `go install`, pinned to a commit hash as below:
```sh
# golines v0.13.0, 2025-08-21
go install github.com/segmentio/golines@8f32f0f7e89c30f572c7f2cd3b2a48016b9d8bbf
```
`script/bootstrap` reads the installed version from the `mod` line of
`go version -m "$(command -v golines)"`, which reports `v0.13.0`. It does
not use `golines --version`, which prints `golines vdev` for a binary that
`go install` built.
`golines` cannot shorten every line: long string literals and comments stay
as they are. golangci-lint's `lll` limit therefore stays at 88 columns, so
it fails only lines far past the formatter's limit.
Documentation and writing repos (Markdown, HTML, CSS) should also have Documentation and writing repos (Markdown, HTML, CSS) should also have
`.prettierrc` and `.prettierignore`. `.prettierrc` and `.prettierignore`.
@@ -907,6 +928,17 @@ style conventions are in separate documents:
- Make all changes on a feature branch. You can do whatever you want on a - Make all changes on a feature branch. You can do whatever you want on a
feature branch. 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 <hash>`. 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 - `.golangci.yml` is standardized. The vendored copy in a consuming repo must
_NEVER_ be modified by an agent. Fetch it from _NEVER_ be modified by an agent. Fetch it from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml` and keep it `https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml` and keep it
@@ -1132,7 +1164,7 @@ style conventions are in separate documents:
- `Makefile` - `Makefile`
- `script/` entrypoints (`bootstrap`, `setup`, `projectname`, `test`, - `script/` entrypoints (`bootstrap`, `setup`, `projectname`, `test`,
`lint`, `fmt`, `fmt-check`, `check`, `docker`, `cibuild`, `precommit`, `lint`, `fmt`, `fmt-check`, `check`, `docker`, `cibuild`, `precommit`,
`install-precommit`) `install-precommit`, `update-policy`)
- `Dockerfile`, `.dockerignore` - `Dockerfile`, `.dockerignore`
- `.gitea/workflows/check.yml` - `.gitea/workflows/check.yml`
- Go: `go.mod`, `go.sum`, `.golangci.yml` - Go: `go.mod`, `go.sum`, `.golangci.yml`
+188
View File
@@ -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 <path in prompts>: 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 <fetched> <local>: 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 <fetched> <local>: 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 "$@"