check / check (push) Successful in 20s
A repo's own lint setting outside the test-support deny list was deleted, and the deletion pushed with exit 0. The script now stops and names each such line. The check is one function that both merge steps call. An indented # line now counts as a comment, as YAML needs. Without --fail the stub curl now writes an error page and exits 0, and the bad-hash scenario checks for the fetch error. Model: opus-5-5
520 lines
15 KiB
Bash
Executable File
520 lines
15 KiB
Bash
Executable File
#!/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, the canonical files and a stub curl that serves them, so
|
|
# nothing touches the network.
|
|
set -eu
|
|
|
|
UPDATE_POLICY="$(cd "$(dirname "$0")/.." && pwd -P)/script/update-policy"
|
|
|
|
dir=""
|
|
trap 'rm -rf "$dir"' EXIT
|
|
|
|
fail() {
|
|
echo "FAIL: $scenario: $*" >&2
|
|
cat "$dir/output" >&2
|
|
exit 1
|
|
}
|
|
|
|
write_stub_curl() {
|
|
mkdir "$dir/bin"
|
|
cat > "$dir/bin/curl" << 'EOF'
|
|
#!/bin/sh
|
|
previous=""
|
|
fail_flag=""
|
|
for argument in "$@"; do
|
|
[ "$previous" = "--output" ] && output="$argument"
|
|
[ "$argument" = "--fail" ] && fail_flag=yes
|
|
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
|
|
[ -n "$fail_flag" ] && exit 22
|
|
echo "404 page not found" > "$output"
|
|
exit 0
|
|
fi
|
|
cp "$source" "$output"
|
|
EOF
|
|
chmod +x "$dir/bin/curl"
|
|
}
|
|
|
|
write_canonical_files() {
|
|
mkdir -p "$dir/canonical/prompts"
|
|
cat > "$dir/canonical/.gitignore" << 'EOF'
|
|
*.log
|
|
*.tmp
|
|
|
|
# Own entries go below this comment.
|
|
EOF
|
|
cat > "$dir/canonical/.dockerignore" << 'EOF'
|
|
**/*.log
|
|
**/*.tmp
|
|
|
|
# Own entries go below this comment.
|
|
EOF
|
|
cat > "$dir/canonical/.editorconfig" << 'EOF'
|
|
root = true
|
|
[*]
|
|
indent_size = 4
|
|
|
|
# Own sections go below this comment.
|
|
EOF
|
|
cat > "$dir/canonical/.golangci.yml" << 'EOF'
|
|
linters:
|
|
settings:
|
|
depguard:
|
|
rules:
|
|
test-support:
|
|
deny:
|
|
- pkg: net/http/httptest
|
|
desc: canonical
|
|
- pkg: net/http/cgi
|
|
desc: added canonically
|
|
other:
|
|
deny:
|
|
- pkg: example.com/other
|
|
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"
|
|
}
|
|
|
|
# write_outdated_files: this repo's files, one canonical version behind,
|
|
# with the repo's own entries in the places the script keeps them. The
|
|
# comment above the closing comment in .gitignore is replaced, not kept.
|
|
write_outdated_files() {
|
|
cat > "$dir/repo/.gitignore" << 'EOF'
|
|
*.log
|
|
|
|
# An outdated comment.
|
|
# Own entries go below this comment.
|
|
/myapp
|
|
EOF
|
|
cat > "$dir/repo/.dockerignore" << 'EOF'
|
|
**/*.log
|
|
|
|
# Own entries go below this comment.
|
|
/myapp
|
|
EOF
|
|
cat > "$dir/repo/.editorconfig" << 'EOF'
|
|
root = true
|
|
[*]
|
|
|
|
# Own sections go below this comment.
|
|
[*.py]
|
|
indent_size = 2
|
|
EOF
|
|
cat > "$dir/repo/.golangci.yml" << 'EOF'
|
|
linters:
|
|
settings:
|
|
depguard:
|
|
rules:
|
|
test-support:
|
|
deny:
|
|
- pkg: net/http/httptest
|
|
desc: canonical
|
|
# Our own test helpers.
|
|
- pkg: example.com/app/internal/apptest
|
|
desc: own entry
|
|
other:
|
|
deny:
|
|
- pkg: example.com/other
|
|
EOF
|
|
echo '{ "tabWidth": 2 }' > "$dir/repo/.prettierrc"
|
|
echo "old policy" > "$dir/repo/REPO_POLICIES.md"
|
|
}
|
|
|
|
# start_scenario <name>: make a new temp directory holding a repo with
|
|
# the outdated files, not yet committed, and an empty origin.
|
|
start_scenario() {
|
|
scenario="$1"
|
|
# The one commit the stub curl serves; every other commit gets a 404.
|
|
export HASH=1111111111111111111111111111111111111111
|
|
rm -rf "$dir"
|
|
dir="$(mktemp -d)"
|
|
: > "$dir/output"
|
|
write_stub_curl
|
|
write_canonical_files
|
|
|
|
git init -q --bare -b main "$dir/origin.git"
|
|
git init -q -b main "$dir/repo"
|
|
git -C "$dir/repo" config user.name "Test"
|
|
git -C "$dir/repo" config user.email "test@example.com"
|
|
git -C "$dir/repo" remote add origin "$dir/origin.git"
|
|
|
|
mkdir "$dir/repo/script"
|
|
cp "$UPDATE_POLICY" "$dir/repo/script/update-policy"
|
|
printf '#!/bin/sh\necho example\n' > "$dir/repo/script/projectname"
|
|
chmod +x "$dir/repo/script/projectname"
|
|
write_outdated_files
|
|
}
|
|
|
|
# commit_and_push_fixture <paths...>: commit those paths, remove the
|
|
# rest of the outdated files, and record the commit as $before.
|
|
commit_and_push_fixture() {
|
|
(
|
|
cd "$dir/repo"
|
|
git add script "$@"
|
|
git commit -q -m "Fixture"
|
|
git push -q origin HEAD
|
|
git clean -q -f -x
|
|
)
|
|
before="$(git -C "$dir/repo" rev-parse HEAD)"
|
|
}
|
|
|
|
run_update_policy() {
|
|
status=0
|
|
(cd "$dir/repo" && PATH="$dir/bin:$PATH" 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"
|
|
}
|
|
|
|
head_of_repo() {
|
|
git -C "$dir/repo" rev-parse HEAD
|
|
}
|
|
|
|
head_of_origin() {
|
|
git -C "$dir/origin.git" rev-parse refs/heads/main
|
|
}
|
|
|
|
expect_success() {
|
|
[ "$status" -eq 0 ] || fail "exited $status"
|
|
}
|
|
|
|
expect_failure() {
|
|
[ "$status" -ne 0 ] || fail "exited 0"
|
|
}
|
|
|
|
expect_output() {
|
|
grep -q -F -e "$1" "$dir/output" || fail "output lacks: $1"
|
|
}
|
|
|
|
expect_output_without() {
|
|
if grep -q -F -e "$1" "$dir/output"; then
|
|
fail "output has: $1"
|
|
fi
|
|
}
|
|
|
|
# expect_file <path>: the file must equal standard input.
|
|
expect_file() {
|
|
cat > "$dir/expected"
|
|
diff "$dir/expected" "$dir/repo/$1" >&2 || fail "$1 is wrong"
|
|
}
|
|
|
|
expect_absent() {
|
|
[ ! -e "$dir/repo/$1" ] || fail "$1 exists"
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
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"
|
|
expect_success
|
|
|
|
expect_file .gitignore << 'EOF'
|
|
*.log
|
|
*.tmp
|
|
|
|
# Own entries go below this comment.
|
|
/myapp
|
|
EOF
|
|
expect_file .dockerignore << 'EOF'
|
|
**/*.log
|
|
**/*.tmp
|
|
|
|
# Own entries go below this comment.
|
|
/myapp
|
|
EOF
|
|
expect_file .editorconfig << 'EOF'
|
|
root = true
|
|
[*]
|
|
indent_size = 4
|
|
|
|
# Own sections go below this comment.
|
|
[*.py]
|
|
indent_size = 2
|
|
EOF
|
|
expect_file .golangci.yml << 'EOF'
|
|
linters:
|
|
settings:
|
|
depguard:
|
|
rules:
|
|
test-support:
|
|
deny:
|
|
- pkg: net/http/httptest
|
|
desc: canonical
|
|
- pkg: net/http/cgi
|
|
desc: added canonically
|
|
# Our own test helpers.
|
|
- pkg: example.com/app/internal/apptest
|
|
desc: own entry
|
|
other:
|
|
deny:
|
|
- pkg: example.com/other
|
|
EOF
|
|
expect_file .prettierrc << 'EOF'
|
|
{ "tabWidth": 4 }
|
|
EOF
|
|
expect_file REPO_POLICIES.md << '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"
|
|
git -C "$dir/repo" log -1 --format=%s | grep -q -F -e "$HASH" ||
|
|
fail "the commit message does not name the hash"
|
|
}
|
|
|
|
test_files_not_carried_are_skipped() {
|
|
start_scenario "files the repo does not carry 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"
|
|
expect_success
|
|
|
|
expect_file docs/REPO_POLICIES.md << 'EOF'
|
|
new policy
|
|
EOF
|
|
expect_absent REPO_POLICIES.md
|
|
expect_absent API_POLICIES.md
|
|
expect_absent docs/API_POLICIES.md
|
|
expect_absent .gitignore
|
|
expect_absent .dockerignore
|
|
expect_absent .editorconfig
|
|
expect_absent .golangci.yml
|
|
expect_absent .prettierrc
|
|
[ "$(head_of_origin)" = "$(head_of_repo)" ] ||
|
|
fail "the commit was 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"
|
|
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_push_failure() {
|
|
start_scenario "push failure exits non-zero"
|
|
commit_and_push_fixture .prettierrc
|
|
reject_pushes
|
|
run_update_policy "$HASH"
|
|
expect_failure
|
|
expect_output "push failed"
|
|
[ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit"
|
|
}
|
|
|
|
# Each of these files, with the repo's own entries above the closing
|
|
# comment. The comment added with them must not be named.
|
|
test_own_entry_above_closing_comment() {
|
|
for file in .gitignore .dockerignore .editorconfig; do
|
|
start_scenario "own entries above the closing comment of $file stop"
|
|
{
|
|
printf '/myapp\n/.tool/\n\n# A comment of our own.\n'
|
|
cat "$dir/repo/$file"
|
|
} > "$dir/merged"
|
|
mv "$dir/merged" "$dir/repo/$file"
|
|
commit_and_push_fixture .gitignore .dockerignore .editorconfig \
|
|
.prettierrc REPO_POLICIES.md
|
|
run_update_policy "$HASH"
|
|
expect_failure
|
|
expect_output "$file has entries"
|
|
expect_output "/myapp"
|
|
expect_output "/.tool/"
|
|
expect_output_without "A comment of our own."
|
|
expect_nothing_written
|
|
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"
|
|
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"
|
|
}
|
|
|
|
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"
|
|
expect_failure
|
|
expect_output ".gitignore lacks this line"
|
|
expect_nothing_written
|
|
}
|
|
|
|
test_fetched_golangci_without_deny_list() {
|
|
start_scenario "a fetched .golangci.yml with no deny list stops"
|
|
cat > "$dir/canonical/.golangci.yml" << 'EOF'
|
|
linters:
|
|
settings:
|
|
depguard:
|
|
rules:
|
|
test-support:
|
|
files:
|
|
- "**/*.go"
|
|
other:
|
|
deny:
|
|
- pkg: example.com/other
|
|
EOF
|
|
commit_and_push_fixture .golangci.yml .prettierrc
|
|
run_update_policy "$HASH"
|
|
expect_failure
|
|
expect_output "no deny list in its test-support rule"
|
|
expect_nothing_written
|
|
}
|
|
|
|
test_own_golangci_setting_stops() {
|
|
start_scenario "a .golangci.yml setting the canonical file lacks stops"
|
|
cat >> "$dir/repo/.golangci.yml" << 'EOF'
|
|
# A comment of our own.
|
|
forbidigo:
|
|
forbid:
|
|
- pattern: model.Operator
|
|
EOF
|
|
commit_and_push_fixture .golangci.yml .prettierrc
|
|
run_update_policy "$HASH"
|
|
expect_failure
|
|
expect_output ".golangci.yml has lines"
|
|
expect_output "forbidigo:"
|
|
expect_output "- pattern: model.Operator"
|
|
expect_output_without "A comment of our own."
|
|
expect_nothing_written
|
|
}
|
|
|
|
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"
|
|
expect_failure
|
|
expect_output "this repo is the source of the policy files"
|
|
expect_nothing_written
|
|
}
|
|
|
|
test_staged_file_stays_out_of_commit() {
|
|
start_scenario "a file staged before the script starts stays out of its commit"
|
|
commit_and_push_fixture .prettierrc
|
|
echo "notes" > "$dir/repo/notes.txt"
|
|
git -C "$dir/repo" add notes.txt
|
|
run_update_policy "$HASH"
|
|
expect_success
|
|
[ "$(git -C "$dir/repo" show --name-only --format= HEAD)" = ".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"
|
|
commit_and_push_fixture .prettierrc
|
|
run_update_policy "${HASH%?}"
|
|
expect_failure
|
|
expect_output "not a full commit hash"
|
|
expect_nothing_written
|
|
}
|
|
|
|
# git is pointed at a local repository in place of the prompts remote,
|
|
# and the stub curl serves the head of that repository's main.
|
|
test_no_hash_uses_head_of_main() {
|
|
start_scenario "with no hash the script uses the head of main"
|
|
commit_and_push_fixture .prettierrc
|
|
git init -q --bare -b main "$dir/prompts.git"
|
|
git -C "$dir/repo" push -q "$dir/prompts.git" HEAD:refs/heads/main
|
|
git -C "$dir/repo" config "url.$dir/prompts.git.insteadOf" \
|
|
"https://git.eeqj.de/sneak/prompts.git"
|
|
HASH="$before"
|
|
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"
|
|
}
|
|
|
|
main() {
|
|
test_own_entries_kept
|
|
test_files_not_carried_are_skipped
|
|
test_nothing_changed
|
|
test_bad_commit_hash
|
|
test_push_failure
|
|
test_own_entry_above_closing_comment
|
|
test_retry_after_failed_push
|
|
test_uncommitted_change_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
|
|
echo "update-policy tests passed"
|
|
}
|
|
|
|
main "$@"
|