Add script/install; script/update-policy runs it and commits (closes #165)
check / check (push) Successful in 31s
check / check (push) Successful in 31s
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
This commit is contained in:
1 parent
936ead8425
commit
454f3410a1
9 files changed
+598
-335
No files matched your search
Executable
+246
@@ -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 <url> <output file>
|
||||
fetch() {
|
||||
curl --fail --silent --show-error --max-time 60 --output "$2" "$1" ||
|
||||
die "cannot fetch $1"
|
||||
}
|
||||
|
||||
# local_path <path in prompts>: 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 <patch> <path in prompts>: 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 <fetched> <local> [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 <fetched>.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 <fetched> <local>: 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 <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. 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 "$@"
|
||||
+16
-195
@@ -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 <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
|
||||
}
|
||||
|
||||
# local_only_lines <fetched> <local> [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 <fetched>.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 <fetched> <local>: 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 <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. 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 "$@"
|
||||
Reference in new issue
Block a user