generated from exteraGramPlugins/pluginBase
[no bump] Enable git hooks automatically on first bundle run
Publish plugin / publish (push) Successful in 27s
Publish plugin / publish (push) Successful in 27s
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:
1 parent
fc76a20420
commit
b2105ca753
4 files changed
+99
-7
No files matched your search
@@ -46,7 +46,7 @@ tools/
|
|||||||
verify_bundle.py offline harness that proves a built bundle behaves correctly, no phone needed
|
verify_bundle.py offline harness that proves a built bundle behaves correctly, no phone needed
|
||||||
release.py publishes a built bundle to a Gitea release (used by CI)
|
release.py publishes a built bundle to a Gitea release (used by CI)
|
||||||
.gitea/workflows/ CI that builds, verifies and publishes the .plugin to Releases
|
.gitea/workflows/ CI that builds, verifies and publishes the .plugin to Releases
|
||||||
scripts/ bash automation for the ADB debug loop (see below)
|
scripts/ ADB debug-loop automation and the git hooks (see below)
|
||||||
docs/sdk-notes.md condensed SDK API surface gathered from the official docs
|
docs/sdk-notes.md condensed SDK API surface gathered from the official docs
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -65,6 +65,8 @@ a private import hook (scoped to a per-module `__builtins__` dict, never touchin
|
|||||||
files were still on disk. `--dev` (the default) embeds absolute local paths so a debugger
|
files were still on disk. `--dev` (the default) embeds absolute local paths so a debugger
|
||||||
resolves breakpoints against your real source files; `--release` embeds repo-relative paths
|
resolves breakpoints against your real source files; `--release` embeds repo-relative paths
|
||||||
for distribution. Both modes produce byte-reproducible output for a given `src/` + `plugin.json`.
|
for distribution. Both modes produce byte-reproducible output for a given `src/` + `plugin.json`.
|
||||||
|
Its first run in a clone also enables the repo's git hooks (see
|
||||||
|
[Git hooks and versioning](#git-hooks-and-versioning)).
|
||||||
|
|
||||||
## Device prerequisites
|
## Device prerequisites
|
||||||
|
|
||||||
@@ -97,6 +99,43 @@ standard `ANDROID_SERIAL` env var for device selection.
|
|||||||
- `scripts/fetch-sdk-stubs.sh [--beta]` — downloads the latest SDK type stubs into `stubs/`
|
- `scripts/fetch-sdk-stubs.sh [--beta]` — downloads the latest SDK type stubs into `stubs/`
|
||||||
for editor autocomplete.
|
for editor autocomplete.
|
||||||
|
|
||||||
|
## Git hooks and versioning
|
||||||
|
|
||||||
|
`scripts/pre-commit` bumps the patch number of `version` in `plugin.json` on every commit
|
||||||
|
(`1.0.0` → `1.0.1`) and stages it into that same commit. `scripts/post-commit` re-stages
|
||||||
|
`plugin.json` afterwards, because commits of a file selection (`git commit -- <paths>`, which
|
||||||
|
some IDEs use) are built from a temporary index and would otherwise leave it looking modified.
|
||||||
|
|
||||||
|
**The hooks enable themselves.** Git never activates hooks from repository content, so a
|
||||||
|
fresh clone, or a repo created from this template, starts without them. The first run of
|
||||||
|
`tools/bundle.py` fixes that by running `git config core.hooksPath scripts`, and every
|
||||||
|
workflow runs it: directly, or through `scripts/deploy.sh` and `scripts/watch.sh`. That
|
||||||
|
includes `--check`, so after cloning run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/bundle.py --check
|
||||||
|
```
|
||||||
|
|
||||||
|
once, before your first commit. A commit made before any build runs without the hooks.
|
||||||
|
`bundle.py` leaves `core.hooksPath` alone when it is already set (so pointing it elsewhere on
|
||||||
|
purpose sticks), skips the step in CI (whenever `CI` is set) and outside a git work tree, and
|
||||||
|
never fails a build over it. To turn the hooks off for a clone, run
|
||||||
|
`git config core.hooksPath .git/hooks`; `git config --unset core.hooksPath` only lasts
|
||||||
|
until the next build.
|
||||||
|
|
||||||
|
The bump is skipped when:
|
||||||
|
|
||||||
|
- the commit message contains `[no bump]`, `[skip bump]`, `[no version bump]` or
|
||||||
|
`[skip version]` (case-insensitive; `nobump`, `no-bump` and `no_bump` also work),
|
||||||
|
- `NO_BUMP=1` (or `GIT_NO_BUMP=1`) is set, e.g. `NO_BUMP=1 git commit`, which is the only way
|
||||||
|
for commits written in an editor, since their message doesn't exist yet when the hook runs,
|
||||||
|
- `version` in `plugin.json` already differs from `HEAD` (you bumped it by hand, e.g. for a
|
||||||
|
minor or major release),
|
||||||
|
- a merge, rebase, cherry-pick or revert is in progress.
|
||||||
|
|
||||||
|
Because every version change publishes a release (see below), a normal commit + push to
|
||||||
|
`main` ships a new patch release. Use a stop word for commits that should not.
|
||||||
|
|
||||||
## Publishing a release
|
## Publishing a release
|
||||||
|
|
||||||
`.gitea/workflows/release.yml` builds, verifies and publishes the `.plugin` to the repo's
|
`.gitea/workflows/release.yml` builds, verifies and publishes the `.plugin` to the repo's
|
||||||
@@ -111,7 +150,8 @@ The `version` field in `plugin.json` is the single source of truth:
|
|||||||
| the same `version` | rebuilds the existing release, replacing its attachment |
|
| the same `version` | rebuilds the existing release, replacing its attachment |
|
||||||
|
|
||||||
So every push leaves a downloadable build attached to the current version, and bumping
|
So every push leaves a downloadable build attached to the current version, and bumping
|
||||||
`version` is what cuts a new release. Pushing a `v*` tag by hand still works and uses that
|
`version` is what cuts a new release. With the git hooks enabled every commit bumps it,
|
||||||
|
so every push of a normal commit cuts a new patch release. Pushing a `v*` tag by hand still works and uses that
|
||||||
tag verbatim, as does a manual run (Actions → Publish plugin → Run workflow).
|
tag verbatim, as does a manual run (Actions → Publish plugin → Run workflow).
|
||||||
|
|
||||||
Every path runs `--check`, builds with `--release`, and gates the upload on
|
Every path runs `--check`, builds with `--release`, and gates the upload on
|
||||||
@@ -143,9 +183,11 @@ you want when publishing needs to reach beyond this repo.
|
|||||||
|
|
||||||
## Writing your own plugin
|
## Writing your own plugin
|
||||||
|
|
||||||
1. Edit `plugin.json` (id, name, description, etc.).
|
1. Run `python3 tools/bundle.py --check` once. It enables the git hooks that auto-bump the
|
||||||
2. Write your plugin under `src/`, with `src/main.py` as the entry module (absolute imports
|
version on commit (see [Git hooks and versioning](#git-hooks-and-versioning)).
|
||||||
|
2. Edit `plugin.json` (id, name, description, etc.).
|
||||||
|
3. Write your plugin under `src/`, with `src/main.py` as the entry module (absolute imports
|
||||||
only — it's executed at plugin-module top level, not inside a package).
|
only — it's executed at plugin-module top level, not inside a package).
|
||||||
3. `python3 tools/bundle.py --release && python3 tools/verify_bundle.py dist/<id>.plugin`
|
4. `python3 tools/bundle.py --release && python3 tools/verify_bundle.py dist/<id>.plugin`
|
||||||
(substitute your `id` from `plugin.json`).
|
(substitute your `id` from `plugin.json`).
|
||||||
4. `bash scripts/deploy.sh --enable` with a device connected.
|
5. `bash scripts/deploy.sh --enable` with a device connected.
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Companion to scripts/pre-commit.
|
# Companion to scripts/pre-commit. Both are enabled per clone by tools/bundle.py
|
||||||
|
# (it sets `core.hooksPath scripts` on its first run).
|
||||||
#
|
#
|
||||||
# `git commit -- <pathspec>` (also how some IDEs commit a file selection) builds
|
# `git commit -- <pathspec>` (also how some IDEs commit a file selection) builds
|
||||||
# the commit from a temporary index. The pre-commit bump therefore lands in the
|
# the commit from a temporary index. The pre-commit bump therefore lands in the
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Increment the patch component of plugin.json's version as part of the commit.
|
"""Increment the patch component of plugin.json's version as part of the commit.
|
||||||
|
|
||||||
|
Enabled per clone by tools/bundle.py, which sets `core.hooksPath scripts` on its
|
||||||
|
first run (git never enables hooks from repository content by itself).
|
||||||
|
|
||||||
Runs as a pre-commit hook, which is the only commit hook whose `git add` is
|
Runs as a pre-commit hook, which is the only commit hook whose `git add` is
|
||||||
picked up by the commit currently being created:
|
picked up by the commit currently being created:
|
||||||
|
|
||||||
|
|||||||
Executable → Regular
+46
@@ -2,6 +2,9 @@
|
|||||||
"""Bundle a multi-file exteraGram plugin (plugin.json + src/) into a single .plugin file.
|
"""Bundle a multi-file exteraGram plugin (plugin.json + src/) into a single .plugin file.
|
||||||
|
|
||||||
Usage: python3 tools/bundle.py [--dev|--release] [--out DIR] [--check]
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -9,13 +12,18 @@ import argparse
|
|||||||
import ast
|
import ast
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import zlib
|
import zlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
SRC_DIR = REPO_ROOT / "src"
|
SRC_DIR = REPO_ROOT / "src"
|
||||||
|
HOOKS_DIR = "scripts"
|
||||||
|
HOOK_NAMES = ("pre-commit", "post-commit")
|
||||||
PLUGIN_JSON = REPO_ROOT / "plugin.json"
|
PLUGIN_JSON = REPO_ROOT / "plugin.json"
|
||||||
|
|
||||||
RECOGNIZED_KEYS = {
|
RECOGNIZED_KEYS = {
|
||||||
@@ -300,6 +308,43 @@ def assemble_bundle(data: dict, modules: dict, release: bool) -> str:
|
|||||||
return text
|
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):
|
def parse_args(argv):
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Bundle a multi-file exteraGram plugin into a single .plugin file."
|
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:
|
def main(argv=None) -> int:
|
||||||
args = parse_args(sys.argv[1:] if argv is None else argv)
|
args = parse_args(sys.argv[1:] if argv is None else argv)
|
||||||
|
ensure_git_hooks()
|
||||||
release = bool(args.release)
|
release = bool(args.release)
|
||||||
try:
|
try:
|
||||||
data = load_metadata()
|
data = load_metadata()
|
||||||
|
|||||||
Reference in new issue
Block a user