koopa-admin-log/scripts/git-cadence-commit-push.sh
Hernâni Marques 140b3bd16f
docs: cadence 7–13 min jitter; commit often, push only at end
Soft range instead of fixed 9m; helper defaults to no push.
2026-07-17 20:04:35 +02:00

157 lines
5 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# git-cadence-commit.sh — optional local WIP commit on a soft cadence
#
# Model (koopa-admin-log / francpaysan-admin-log histories + ops preference):
# - message: area: short description
# - size: small logical steps; **prefer more commits over few**
# - interval: soft range **713 minutes** while work is open (not a strict timer;
# denser bursts of 02 min are fine for tiny follow-ups)
# - push: **only at the end of a work session** (CADENCE_PUSH=1), never by default
#
# Usage:
# ./scripts/git-cadence-commit-push.sh # commit only if dirty
# ./scripts/git-cadence-commit-push.sh /path/to/repo
# CADENCE_MSG='monitoring: wip ladder' ./scripts/git-cadence-commit-push.sh
# CADENCE_DRY=1 ./scripts/git-cadence-commit-push.sh
# CADENCE_PUSH=1 ./scripts/git-cadence-commit-push.sh # explicit push (end of work)
# CADENCE_MIN_AGE_S=420 # skip if last commit younger than this (default 0)
#
# Soft timer (713 min, not fixed): fire often, skip until age ∈ [7,13] min target.
# # agent/cron every 7m — script enforces random target in range when CADENCE_JITTER=1
# CADENCE_JITTER=1 ./scripts/git-cadence-commit-push.sh
#
set -euo pipefail
REPO="${1:-}"
if [ -z "$REPO" ]; then
REPO="$(cd "$(dirname "$0")/.." && pwd)"
fi
REPO="$(cd "$REPO" && pwd)"
cd "$REPO"
CADENCE_DRY="${CADENCE_DRY:-0}"
# Push only when explicitly requested (end of session) — default OFF
CADENCE_PUSH="${CADENCE_PUSH:-0}"
# Soft range 713 minutes (seconds)
CADENCE_RANGE_MIN_S="${CADENCE_RANGE_MIN_S:-420}" # 7 min
CADENCE_RANGE_MAX_S="${CADENCE_RANGE_MAX_S:-780}" # 13 min
CADENCE_JITTER="${CADENCE_JITTER:-0}"
CADENCE_MIN_AGE_S="${CADENCE_MIN_AGE_S:-0}"
SKIP_GLOBS=(
'secrets.env'
'**/secrets.env'
'**/*password*'
'**/*secret*'
'**/*.pem'
'**/*token*'
)
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "not a git repo: $REPO" >&2
exit 1
fi
branch=$(git rev-parse --abbrev-ref HEAD)
remote=$(git remote 2>/dev/null | head -1 || true)
: "${remote:=origin}"
# Jitter: only proceed if last commit is at least a random target in [7,13] min
if [ "$CADENCE_JITTER" = "1" ]; then
last=$(git log -1 --format=%ct 2>/dev/null || echo 0)
now=$(date +%s)
age=$((now - last))
# stable-ish target per hour so repeated 7m fires don't all commit
target=$(python3 -c "
import random, time
random.seed(int(time.time()) // 60) # changes every minute
print(random.randint(${CADENCE_RANGE_MIN_S}, ${CADENCE_RANGE_MAX_S}))
" 2>/dev/null || echo 600)
if [ "$age" -lt "$target" ]; then
echo "jitter wait · age=${age}s < target=${target}s (range ${CADENCE_RANGE_MIN_S}-${CADENCE_RANGE_MAX_S}s) · $REPO"
exit 0
fi
elif [ "${CADENCE_MIN_AGE_S}" -gt 0 ] 2>/dev/null; then
last=$(git log -1 --format=%ct 2>/dev/null || echo 0)
now=$(date +%s)
age=$((now - last))
if [ "$age" -lt "$CADENCE_MIN_AGE_S" ]; then
echo "min-age wait · age=${age}s < ${CADENCE_MIN_AGE_S}s · $REPO"
exit 0
fi
fi
do_push() {
local ahead
ahead=$(git rev-list --count "${remote}/${branch}..HEAD" 2>/dev/null || echo 0)
if [ "${ahead:-0}" -le 0 ]; then
echo "nothing to push · $REPO · $branch"
return 0
fi
echo "ahead of ${remote}/${branch} by $ahead"
if [ "$CADENCE_DRY" = "1" ]; then
echo "DRY: git push $remote $branch"
return 0
fi
if [ "$CADENCE_PUSH" != "1" ]; then
echo "skip push (CADENCE_PUSH!=1 — push only at end of work)"
return 0
fi
git push "$remote" "$branch" || {
echo "WARN: push failed for $REPO ($remote $branch)" >&2
return 0
}
echo "pushed · $remote $branch"
}
# Clean working tree → maybe push only
if git diff --quiet && git diff --cached --quiet \
&& [ -z "$(git ls-files --others --exclude-standard)" ]; then
echo "clean · $REPO · $branch (nothing to commit)"
do_push
exit 0
fi
git add -A
for g in "${SKIP_GLOBS[@]}"; do
git reset -q -- "$g" 2>/dev/null || true
done
while IFS= read -r f; do
case "$f" in
*secret*|*password*|*token*|secrets.env)
git reset -q -- "$f" 2>/dev/null || true
;;
esac
done < <(git diff --cached --name-only 2>/dev/null || true)
if git diff --cached --quiet; then
echo "only secret/skip paths dirty — not committing · $REPO"
exit 0
fi
msg="${CADENCE_MSG:-}"
if [ -z "$msg" ]; then
files=$(git diff --cached --name-only | head -20)
area="wip"
case "$files" in
*taler-monitoring*|*scripts/taler-monitoring*) area="monitoring" ;;
*landing*|*landings*|*configs/*landing*) area="landing" ;;
*docs*|*README*|*TODO*|*NOTES*) area="docs" ;;
*caddy*) area="caddy" ;;
*scripts*) area="scripts" ;;
esac
n=$(git diff --cached --name-only | wc -l | tr -d ' ')
sample=$(git diff --cached --name-only | head -1 | xargs -r basename)
msg="${area}: cadence ${n} file(s) · ${sample}"
fi
if [ "$CADENCE_DRY" = "1" ]; then
echo "DRY commit: $msg"
git diff --cached --stat
[ "$CADENCE_PUSH" = "1" ] && echo "DRY push: $remote $branch" || echo "DRY: no push"
exit 0
fi
git commit -m "$msg"
echo "committed · $msg"
do_push