[no bump] Enable git hooks automatically on first bundle run
Publish plugin / publish (push) Successful in 3s

tools/bundle.py now sets `core.hooksPath scripts` the first time it runs in a
clone (skipped in CI, outside a git work tree, or when core.hooksPath is
already set), so the version auto-bump hooks work in fresh clones and repos
created from the template without a manual setup step. README documents the
hooks, the self-enabling behaviour and the bump stop words.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
lyuksovannyyandClaude Opus 5.5 committed 2026-09-23 12:56:23 +02:00
1 parent e01dc843b2
commit 1bd5c72ce5
4 files changed
+99 -7

No files matched your search

Executable → Regular
+46
View File
@@ -2,6 +2,9 @@
"""Bundle a multi-file exteraGram plugin (plugin.json + src/) into a single .plugin file.
Usage: python3 tools/bundle.py [--dev|--release] [--out DIR] [--check]
Also enables the repo's git hooks (scripts/pre-commit, scripts/post-commit) on
first run; see ensure_git_hooks().
"""
from __future__ import annotations
@@ -9,13 +12,18 @@ import argparse
import ast
import base64
import json
import os
import re
import stat
import subprocess
import sys
import zlib
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
SRC_DIR = REPO_ROOT / "src"
HOOKS_DIR = "scripts"
HOOK_NAMES = ("pre-commit", "post-commit")
PLUGIN_JSON = REPO_ROOT / "plugin.json"
RECOGNIZED_KEYS = {
@@ -300,6 +308,43 @@ def assemble_bundle(data: dict, modules: dict, release: bool) -> str:
return text
def ensure_git_hooks() -> None:
"""Point git at scripts/ for hooks, once per clone.
Git never enables hooks from repository content, so a fresh clone (or a
repo created from the template) starts without them. Every workflow runs
this bundler first, directly or via scripts/deploy.sh and scripts/watch.sh,
which makes it the one place that can switch them on without an extra
setup step. Skipped in CI, outside a git work tree, and when
core.hooksPath is already set (including to something else on purpose).
Never fails the build.
"""
if os.environ.get("CI"):
return
hooks = [REPO_ROOT / HOOKS_DIR / name for name in HOOK_NAMES]
if not any(h.is_file() for h in hooks):
return
def git(*args):
return subprocess.run(["git", *args], cwd=REPO_ROOT, capture_output=True, text=True)
try:
top = git("rev-parse", "--show-toplevel")
if top.returncode != 0 or Path(top.stdout.strip()).resolve() != REPO_ROOT:
return
if git("config", "--get", "core.hooksPath").stdout.strip():
return
# Git silently skips hooks that are not executable.
for hook in hooks:
if hook.is_file():
mode = hook.stat().st_mode
hook.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
if git("config", "core.hooksPath", HOOKS_DIR).returncode == 0:
print(f"enabled git hooks from {HOOKS_DIR}/ (git config core.hooksPath {HOOKS_DIR})", file=sys.stderr)
except (OSError, subprocess.SubprocessError):
pass
def parse_args(argv):
parser = argparse.ArgumentParser(
description="Bundle a multi-file exteraGram plugin into a single .plugin file."
@@ -318,6 +363,7 @@ def parse_args(argv):
def main(argv=None) -> int:
args = parse_args(sys.argv[1:] if argv is None else argv)
ensure_git_hooks()
release = bool(args.release)
try:
data = load_metadata()