160 lines
6.0 KiB
Bash
Executable File
160 lines
6.0 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# Publish a history-free snapshot of a branch to the public repository.
|
|
#
|
|
# The public repo never receives the private history: each publish is ONE new
|
|
# commit whose tree is the chosen ref minus the paths in .public-exclude. Its
|
|
# only parent is the previous public snapshot, so the public history is just
|
|
# the list of snapshots.
|
|
#
|
|
# Before anything is pushed, the snapshot is scanned for secrets: the values of
|
|
# PASSWORD/SECRET/KEY/TOKEN variables in the local .env, private keys, service
|
|
# account files and common token formats. Any hit aborts the publish.
|
|
#
|
|
# Usage (from the repository root):
|
|
# scripts/publish-public-snapshot.sh [ref] # dry run: build + scan only
|
|
# scripts/publish-public-snapshot.sh [ref] --push # build, scan and push
|
|
#
|
|
# ref defaults to main. Plain `git push public` is disabled on purpose
|
|
# (remote.public.pushurl), so this script is the only way to publish.
|
|
|
|
set -euo pipefail
|
|
|
|
PUBLIC_URL=https://gitea.thesesidesup.nl/ronald/timebank-cc-public.git
|
|
PUBLIC_BRANCH=main
|
|
|
|
REF=main
|
|
PUSH=0
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--push) PUSH=1 ;;
|
|
-*) echo "Unknown option: $arg" >&2; exit 2 ;;
|
|
*) REF=$arg ;;
|
|
esac
|
|
done
|
|
|
|
cd "$(git rev-parse --show-toplevel)"
|
|
|
|
COMMIT=$(git rev-parse --verify "$REF^{commit}")
|
|
SHORT=$(git rev-parse --short "$COMMIT")
|
|
echo "Source: $REF ($SHORT)"
|
|
|
|
# --- Build the tree without the excluded paths ------------------------------
|
|
INDEX=$(mktemp)
|
|
trap 'rm -f "$INDEX"' EXIT
|
|
export GIT_INDEX_FILE=$INDEX
|
|
|
|
git read-tree "$COMMIT"
|
|
|
|
EXCLUDES=()
|
|
ALLOWED=() # '!path' lines: reviewed, exempt from the file-name check
|
|
if git cat-file -e "$COMMIT:.public-exclude" 2>/dev/null; then
|
|
while IFS= read -r line; do
|
|
line=${line%%#*}
|
|
line=$(printf '%s' "$line" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')
|
|
case "$line" in
|
|
'') ;;
|
|
'!'*) ALLOWED+=("${line#!}") ;;
|
|
*) EXCLUDES+=("$line") ;;
|
|
esac
|
|
done < <(git show "$COMMIT:.public-exclude")
|
|
fi
|
|
for p in "${EXCLUDES[@]}"; do
|
|
git rm -r -q --cached --ignore-unmatch -- "$p" >/dev/null
|
|
done
|
|
|
|
TREE=$(git write-tree)
|
|
unset GIT_INDEX_FILE
|
|
echo "Excluded: ${EXCLUDES[*]:-(nothing)}"
|
|
echo "Files in snapshot: $(git ls-tree -r --name-only "$TREE" | wc -l)"
|
|
|
|
# --- Secret scan --------------------------------------------------------------
|
|
hits=0
|
|
report() { echo " SECRET: $1"; hits=$((hits + 1)); }
|
|
|
|
# 1. Values of secret-like variables in the local .env (never printed). In a
|
|
# linked worktree without its own .env, use the main checkout's.
|
|
ENV_FILE=.env
|
|
[ -f "$ENV_FILE" ] || ENV_FILE="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")/.env"
|
|
if [ -f "$ENV_FILE" ]; then
|
|
echo "Scanning for values from: $ENV_FILE"
|
|
else
|
|
echo "WARNING: no .env found, skipping the value scan"
|
|
fi
|
|
if [ -f "$ENV_FILE" ]; then
|
|
while IFS= read -r line; do
|
|
key=${line%%=*}
|
|
val=$(printf '%s' "${line#*=}" | sed -E 's/[[:space:]]+#.*//; s/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/')
|
|
[ ${#val} -ge 8 ] || continue
|
|
case "$val" in *'${'*|null|true|false) continue ;; esac
|
|
# Client-side keys are public by design (they ship in the JS bundle).
|
|
case "$key" in VITE_*|MIX_*|PUSHER_APP_KEY|REVERB_APP_KEY) continue ;; esac
|
|
while IFS= read -r f; do
|
|
report "value of $key from .env in ${f#*:}"
|
|
done < <(git grep -l -F -e "$val" "$TREE" -- || true)
|
|
done < <(grep -E '^[A-Z0-9_]*(PASSWORD|PASS|SECRET|KEY|TOKEN|DSN)[A-Z0-9_]*=' "$ENV_FILE" || true)
|
|
fi
|
|
|
|
# 2. Files that should never be public.
|
|
while IFS= read -r f; do
|
|
for a in "${ALLOWED[@]}"; do [ "$f" = "$a" ] && continue 2; done
|
|
report "sensitive file name: $f"
|
|
done < <(git ls-tree -r --name-only "$TREE" \
|
|
| grep -E '(^|/)\.env$|(^|/)\.env\.(local|production|prod|backup)|\.pem$|\.p12$|\.pfx$|(^|/)id_(rsa|ed25519|ecdsa)$|firebase-adminsdk|\.htpasswd|\.sql(\.gz)?$' || true)
|
|
|
|
# 3. Common secret formats. Placeholders (your_..., ..._here, example) are skipped.
|
|
PATTERNS=(
|
|
-e "-----BEGIN [A-Z ]*PRIVATE KEY-----"
|
|
-e "-p'[^'\$ <]{6,}'"
|
|
-e "MYSQL_PWD=[^\$\" <]{6,}"
|
|
-e "IDENTIFIED BY '[^'<\$]{6,}'"
|
|
-e "AKIA[0-9A-Z]{16}"
|
|
-e "sk_live_[0-9a-zA-Z]{10,}"
|
|
-e "gh[pousr]_[A-Za-z0-9]{30,}"
|
|
-e "glpat-[A-Za-z0-9_-]{20}"
|
|
-e "xox[abprs]-[A-Za-z0-9-]{10,}"
|
|
-e "AIza[0-9A-Za-z_-]{35}"
|
|
-e "://[^/ :@\"'<]+:[^/ @\"'\$<]{6,}@"
|
|
)
|
|
while IFS= read -r m; do
|
|
m=${m#*:} # strip the tree id
|
|
report "pattern match in ${m%%:*}:$(cut -d: -f2 <<<"$m") (line not shown)"
|
|
done < <(git grep -n -I -E "${PATTERNS[@]}" "$TREE" -- ':!*.lock' ':!package-lock.json' \
|
|
| grep -v -i -E 'your[_-]|_here|example|placeholder|changeme|xxxx|\.\.\.' || true)
|
|
|
|
if [ $hits -ne 0 ]; then
|
|
echo
|
|
echo "ABORTED: $hits possible secret(s) in the snapshot. Fix them in $REF, or add the"
|
|
echo "path to .public-exclude, then run again. Nothing was pushed."
|
|
exit 1
|
|
fi
|
|
echo "Secret scan: clean"
|
|
|
|
# --- Commit on top of the previous public snapshot ----------------------------
|
|
PARENT=$(git ls-remote "$PUBLIC_URL" "refs/heads/$PUBLIC_BRANCH" | awk '{print $1}')
|
|
PARENT_ARGS=()
|
|
if [ -n "$PARENT" ]; then
|
|
git fetch -q "$PUBLIC_URL" "$PUBLIC_BRANCH"
|
|
if [ "$(git rev-parse "$PARENT^{tree}")" = "$TREE" ]; then
|
|
echo "Public repo already has exactly this content. Nothing to do."
|
|
exit 0
|
|
fi
|
|
PARENT_ARGS=(-p "$PARENT")
|
|
echo "Previous public snapshot: $(git log -1 --format='%h %s' "$PARENT")"
|
|
else
|
|
echo "Public repo is empty: this becomes the first commit."
|
|
fi
|
|
|
|
MSG="Snapshot of $REF at $SHORT ($(git log -1 --format=%cd --date=short "$COMMIT"))"
|
|
SNAP=$(git commit-tree "$TREE" "${PARENT_ARGS[@]}" -m "$MSG")
|
|
echo "Snapshot commit: $(git rev-parse --short "$SNAP") \"$MSG\""
|
|
|
|
if [ $PUSH -eq 1 ]; then
|
|
git push "$PUBLIC_URL" "$SNAP:refs/heads/$PUBLIC_BRANCH"
|
|
echo "Published to $PUBLIC_URL ($PUBLIC_BRANCH)"
|
|
else
|
|
echo
|
|
echo "Dry run, nothing pushed. Inspect with: git ls-tree -r --name-only $SNAP | less"
|
|
echo "Publish with: $0 $REF --push"
|
|
fi
|