From a3acabb88feaccda7cb8f0d8654a702c73148737 Mon Sep 17 00:00:00 2001 From: Matteo Cherubini Date: Sat, 20 Jun 2026 22:23:57 +0200 Subject: [PATCH] feat: Implement 'genome-raw-commit' automation script --- deploy/nexus/genome-raw-commit.sh | 101 ++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 deploy/nexus/genome-raw-commit.sh diff --git a/deploy/nexus/genome-raw-commit.sh b/deploy/nexus/genome-raw-commit.sh new file mode 100644 index 0000000..3989346 --- /dev/null +++ b/deploy/nexus/genome-raw-commit.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# genome-raw-commit +# +# Commits raw files synchronized by Syncthing into the vault and pushes them to origin/. +# - Committer = n8n-bot (robotic identity responsible for pushing) +# - Author = deduced from the Syncthing device ID (modifiedBy field), resolved via .authors.json. +# Falls back to default values if unknown. +# - One commit per author/device to ensure clear attribution. +# - No-op if no changes are present. Excludes infrastructure files and private folders. + +set -euo pipefail +genome="${1:?usage: genome-raw-commit }" + +# Input validation to prevent path or URL traversal inside the script +[[ "$genome" =~ ^[a-z0-9][a-z0-9-]{0,63}$ ]] || { echo '{"status":"error","reason":"invalid genome name"}'; exit 1; } + +set -a; . "${HOME}/.config/knowledge-genome.env"; set +a +: "${GENOME_VAULTS_ROOT:=/srv/genome-vaults}" +: "${GENOME_BASE:=develop}" +: "${FORGEJO_USER:=n8n-bot}" +: "${FORGEJO_HOST:=127.0.0.1:3001}" +: "${FORGEJO_OWNER:=Keru}" +: "${SYNCTHING_URL:=http://127.0.0.1:8384}" +: "${COMMITTER_NAME:=n8n-bot}" +: "${COMMITTER_EMAIL:=n8n-bot@homelab}" +: "${DEFAULT_AUTHOR_NAME:=Unknown}" +: "${DEFAULT_AUTHOR_EMAIL:=unknown@syncthing}" + +vault="${GENOME_VAULTS_ROOT}/${genome}" +fid="${genome}-public" +authors_map="${GENOME_VAULTS_ROOT}/.authors.json" +clone_url="http://${FORGEJO_USER}@${FORGEJO_HOST}/${FORGEJO_OWNER}/${genome}.git" +export GIT_ASKPASS=/usr/local/bin/genome-askpass + +[[ -d "${vault}/.git" ]] || { printf '{"status":"error","reason":"vault absent","genome":"%s"}\n' "$genome"; exit 1; } +cd "$vault" +git config user.name "$COMMITTER_NAME" +git config user.email "$COMMITTER_EMAIL" +git config commit.gpgsign false + +# Scope restricted to raw/ directory. raw/.stignore is omitted via .git/info/exclude +git add -A -- raw/ +git reset -q -- raw/.stignore 2>/dev/null || true + +if git diff --cached --quiet; then + printf '{"status":"noop","genome":"%s"}\n' "$genome" + exit 0 +fi + +# Map Syncthing device ID to author information (name, email) +resolve_dev() { + # $1 = file path relative to the vault root (e.g., raw/file.txt) + [[ -z "${SYNCTHING_API_KEY:-}" ]] && return 0 + curl -fsS -H "X-API-Key: ${SYNCTHING_API_KEY}" --get "${SYNCTHING_URL}/rest/db/file" \ + --data-urlencode "folder=${fid}" --data-urlencode "file=${1#raw/}" 2>/dev/null \ + | jq -r '.local.modifiedBy // empty' 2>/dev/null || true +} + +author_for_dev() { + # $1 = device ID + local dev="$1" name="$DEFAULT_AUTHOR_NAME" email="$DEFAULT_AUTHOR_EMAIL" + if [[ -n "$dev" && -f "$authors_map" ]] && jq -e --arg d "$dev" '.[$d]' "$authors_map" >/dev/null 2>&1; then + name="$(jq -r --arg d "$dev" '.[$d].name' "$authors_map")" + email="$(jq -r --arg d "$dev" '.[$d].email' "$authors_map")" + fi + printf '%s\t%s\t%s' "$name" "$email" "${dev:-unknown}" +} + +# Group staged files by author identity +declare -A G_FILES G_NAME G_EMAIL G_DEV +while IFS= read -r f; do + [[ -z "$f" ]] && continue + dev="$(resolve_dev "$f")" + IFS=$'\t' read -r aname aemail adev <<< "$(author_for_dev "$dev")" + key="${aname} <${aemail}>" + G_FILES["$key"]+="${f}"$'\n' + G_NAME["$key"]="$aname"; G_EMAIL["$key"]="$aemail"; G_DEV["$key"]="$adev" +done < <(git diff --cached --name-only -- raw/) + +ts="$(date +%Y-%m-%dT%H:%M:%S%z)" +commits=0; summary="" +for key in "${!G_FILES[@]}"; do + mapfile -t files < <(printf '%s' "${G_FILES[$key]}") + short="$(printf '%s\n' "${files[@]}" | sed 's#^raw/##' | paste -sd, -)" + msg="$(printf 'raw(%s): sync %s\n\nAdded-by-device: %s\nSyncthing-device-id: %s\nSource: syncthing-autocommit\nSynced-at: %s\n' \ + "$genome" "$short" "${G_DEV[$key]}" "${G_DEV[$key]}" "$ts")" + git commit -q --author="$key" -m "$msg" -- "${files[@]}" + commits=$((commits+1)) + summary="${summary}${summary:+; }${G_NAME[$key]}:${short}" +done + +# Fetch updates from origin to merge upstream modifications before pushing +git fetch -q origin +if git show-ref --verify --quiet "refs/remotes/origin/${GENOME_BASE}"; then + git rebase -q "origin/${GENOME_BASE}" \ + || { git rebase --abort 2>/dev/null || true; printf '{"status":"error","reason":"rebase-conflict","genome":"%s"}\n' "$genome"; exit 1; } +fi +git push -q "$clone_url" "HEAD:${GENOME_BASE}" + +printf '{"status":"ok","genome":"%s","base":"%s","commits":%d","head":"%s","summary":"%s"}\n' \ + "$genome" "$GENOME_BASE" $commits "$(git rev-parse --short HEAD)" "$summary"