Align project policies and simplify the release workflow
This commit is contained in:
145
scripts/release-check.sh
Executable file
145
scripts/release-check.sh
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/bin/sh
|
||||
# Run local checks without publishing or changing files.
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
go test -race -vet=off ./...
|
||||
go vet ./...
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
def example(name):
|
||||
return json.loads(subprocess.check_output(
|
||||
["go", "run", "./examples/go-library/" + name], text=True))
|
||||
|
||||
prepared = example("prepare")
|
||||
assert prepared["prompt_id"] == "example.prepare", prepared
|
||||
assert prepared["selected_profile"] == "offline-example", prepared
|
||||
assert prepared["model"] == "offline-model", prepared
|
||||
assert prepared["message_count"] > 0, prepared
|
||||
result = example("run")
|
||||
assert result["output"] == "Ada finished the migration review.", result
|
||||
assert result["is_valid"] is True, result
|
||||
assert result["validation_status"] == "passed", result
|
||||
assert result["model"] == "offline-model", result
|
||||
assert result["total_tokens"] == 18, result
|
||||
PY
|
||||
|
||||
unformatted=$(
|
||||
git ls-files '*.go' |
|
||||
while IFS= read -r go_file
|
||||
do
|
||||
gofmt -l "$go_file"
|
||||
done
|
||||
)
|
||||
test -z "$unformatted"
|
||||
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.parse import unquote
|
||||
|
||||
root = Path.cwd().resolve()
|
||||
markdown_files = [
|
||||
root / name
|
||||
for name in subprocess.check_output(
|
||||
["git", "ls-files", "*.md"], text=True
|
||||
).splitlines()
|
||||
]
|
||||
link_pattern = re.compile(r"!?\[[^]]*\]\(([^)]+)\)")
|
||||
heading_pattern = re.compile(r"^#{1,6}\s+(.+?)\s*#*\s*$")
|
||||
scheme_pattern = re.compile(r"^[a-z][a-z0-9+.-]*:", re.IGNORECASE)
|
||||
|
||||
|
||||
def markdown_lines(path):
|
||||
in_fence = False
|
||||
fence = ""
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
stripped = line.lstrip()
|
||||
marker = stripped[:3]
|
||||
if marker in {"```", "~~~"}:
|
||||
if not in_fence:
|
||||
in_fence = True
|
||||
fence = marker
|
||||
elif marker == fence:
|
||||
in_fence = False
|
||||
fence = ""
|
||||
continue
|
||||
if not in_fence:
|
||||
yield line
|
||||
|
||||
|
||||
anchor_cache = {}
|
||||
|
||||
|
||||
def anchors(path):
|
||||
if path in anchor_cache:
|
||||
return anchor_cache[path]
|
||||
found = set()
|
||||
counts = {}
|
||||
for line in markdown_lines(path):
|
||||
match = heading_pattern.match(line)
|
||||
if not match:
|
||||
continue
|
||||
heading = re.sub(r"<[^>]+>", "", match.group(1)).replace("`", "")
|
||||
base = re.sub(r"[^\w\- ]", "", heading.lower()).replace(" ", "-")
|
||||
count = counts.get(base, 0)
|
||||
counts[base] = count + 1
|
||||
found.add(base if count == 0 else f"{base}-{count}")
|
||||
anchor_cache[path] = found
|
||||
return found
|
||||
|
||||
|
||||
failures = []
|
||||
for source in markdown_files:
|
||||
text = "\n".join(markdown_lines(source))
|
||||
for match in link_pattern.finditer(text):
|
||||
target = match.group(1).strip()
|
||||
if target.startswith("<") and target.endswith(">"):
|
||||
target = target[1:-1]
|
||||
if scheme_pattern.match(target) or target.startswith("//"):
|
||||
continue
|
||||
path_text, separator, fragment = target.partition("#")
|
||||
destination = source if not path_text else source.parent / unquote(path_text)
|
||||
try:
|
||||
destination = destination.resolve()
|
||||
destination.relative_to(root)
|
||||
except ValueError:
|
||||
failures.append(f"{source.relative_to(root)}: escapes repository: {target}")
|
||||
continue
|
||||
if not destination.exists():
|
||||
failures.append(f"{source.relative_to(root)}: missing target: {target}")
|
||||
continue
|
||||
if separator and destination.suffix.lower() == ".md":
|
||||
fragment = unquote(fragment).lower()
|
||||
if fragment not in anchors(destination):
|
||||
failures.append(f"{source.relative_to(root)}: missing anchor: {target}")
|
||||
|
||||
if failures:
|
||||
print("\n".join(failures), file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
|
||||
case "$(go env GOWORK)" in
|
||||
''|off) ;;
|
||||
*) printf '%s\n' 'an active Go workspace is not allowed' >&2; exit 1 ;;
|
||||
esac
|
||||
test -z "$(git ls-files go.work go.work.sum)"
|
||||
test ! -e vendor
|
||||
if grep -Eq '^[[:space:]]*replace([[:space:]]|\()' go.mod
|
||||
then
|
||||
printf '%s\n' 'go.mod contains a replacement' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git diff --check
|
||||
git diff --cached --check
|
||||
credential_pattern='-----BEGIN ([A-Z0-9]+ )?PRIV''ATE KEY-----|AKI''A[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{36,}|sk-[A-Za-z0-9]{32,}'
|
||||
if git grep -nEI -e "$credential_pattern" -- .
|
||||
then
|
||||
printf '%s\n' 'possible credential found' >&2
|
||||
exit 1
|
||||
fi
|
||||
57
scripts/release.sh
Executable file
57
scripts/release.sh
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/bin/sh
|
||||
# Validate one committed candidate and publish only its selected annotated tag.
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
release_version=${1:?usage: scripts/release.sh vMAJOR.MINOR.PATCH}
|
||||
printf '%s\n' "$release_version" |
|
||||
grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
|
||||
release_commit=$(git rev-parse --verify 'HEAD^{commit}')
|
||||
|
||||
check_candidate() {
|
||||
test "$(git branch --show-current)" = main
|
||||
test -z "$(git status --porcelain)"
|
||||
test "$(git rev-parse HEAD)" = "$release_commit"
|
||||
git fetch origin main
|
||||
test "$(git rev-parse refs/remotes/origin/main)" = "$release_commit"
|
||||
if git show-ref --verify --quiet "refs/tags/$release_version"; then
|
||||
printf '%s\n' "local tag already exists: $release_version" >&2
|
||||
exit 1
|
||||
fi
|
||||
remote_refs=$(git ls-remote --tags origin "refs/tags/$release_version" "refs/tags/$release_version^{}")
|
||||
test -z "$remote_refs"
|
||||
}
|
||||
|
||||
check_candidate
|
||||
python3 - "$release_version" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
version = sys.argv[1]
|
||||
note = Path("docs/releases") / (version + ".md")
|
||||
lines = note.read_text(encoding="utf-8").splitlines()
|
||||
assert lines and lines[0] == "# Promptkit " + version, f"wrong release title: {note}"
|
||||
headings = set()
|
||||
fence = None
|
||||
for line in lines:
|
||||
marker = line.lstrip()[:3]
|
||||
if marker in {"```", "~~~"}:
|
||||
if fence is None:
|
||||
fence = marker
|
||||
elif fence == marker:
|
||||
fence = None
|
||||
elif fence is None:
|
||||
headings.add(line)
|
||||
for section in ("Summary", "Compatibility", "Upgrade", "Changes"):
|
||||
assert "## " + section in headings, f"missing {section}: {note}"
|
||||
PY
|
||||
|
||||
./scripts/release-check.sh
|
||||
check_candidate
|
||||
release_note=$(git show "$release_commit:docs/releases/$release_version.md")
|
||||
printf '%s\n\nValidated commit: %s\n' "$release_note" "$release_commit" |
|
||||
git tag --annotate --cleanup=verbatim "$release_version" --file - "$release_commit"
|
||||
test "$(git cat-file -t "refs/tags/$release_version")" = tag
|
||||
test "$(git rev-parse "refs/tags/$release_version^{commit}")" = "$release_commit"
|
||||
git push origin "refs/tags/$release_version:refs/tags/$release_version"
|
||||
printf '%s\n' "Released $release_version. No CI or module-availability wait is required."
|
||||
Reference in New Issue
Block a user