#!/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 **7–13 minutes** while work is open (not a strict timer; # denser bursts of 0–2 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 (7–13 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 7–13 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