#!/usr/bin/env bash set -Eeuo pipefail # Build a source bundle ZIP while excluding credentials, private keys, # local state, generated artifacts and other likely secrets. # # Usage: # ./bundle.sh # ./bundle.sh path/to/output.zip # # Run from anywhere inside the project; the script resolves its own directory. ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT" DEFAULT_OUT=0 if [[ $# -gt 0 ]]; then OUT="$1" else DEFAULT_OUT=1 OUT="SHiNE-bundle-$(date +%Y.%m.%d-%H.%M.%S).zip" fi case "$OUT" in /*) ;; *) OUT="$ROOT/$OUT" ;; esac if ! command -v zip >/dev/null 2>&1; then echo "ERROR: 'zip' is required." >&2 exit 1 fi TMP="$(mktemp -d)" LIST="$TMP/files.txt" SAFE_LIST="$TMP/safe-files.txt" trap 'rm -rf "$TMP"' EXIT # Paths / filenames that must never be bundled. is_denied_path() { local p="/$1" case "$p" in */.git/*|*/.git|\ */.gradle/*|*/.gradle|\ */.gradle-home/*|*/.gradle-home|\ */.idea/*|*/.idea|\ */.vscode/*|*/.vscode|\ */node_modules/*|*/node_modules|\ */target/*|*/target|\ */build/*|*/build|\ */out/*|*/out|\ */bin/*|*/bin|\ */logs/*|*/logs|\ */data/*|*/data|\ */test-ledger/*|*/test-ledger|\ */.anchor/*|*/.anchor|\ */.yarn/*|*/.yarn|\ */.vendor/*|*/.vendor|\ */.agents/*|*/.agents|\ */.codex/*|*/.codex|\ */.claude/*|*/.claude|\ */deploy/backup/archive/*|\ */scripts/*/runs/*|\ */scripts/*/keypairs/*|\ */keys/*|\ */.git-local-backup/*) return 0 ;; esac local base="${p##*/}" local lower lower="$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]')" case "$lower" in .env|.env.*|\ .debug-token|\ .npmrc|.pypirc|.netrc|\ credentials|credentials.*|\ secrets|secrets.*|\ secret|secret.*|\ id_rsa|id_dsa|id_ecdsa|id_ed25519|\ *.pem|*.key|*.p12|*.pfx|*.jks|*.keystore|\ *keypair*.json|\ service-account*.json|\ firebase-adminsdk*.json|\ google-services.json|\ validator.log) return 0 ;; esac case "$lower" in *.class|*.jar|*.war|*.ear|*.o|*.a|*.so|*.dll|*.dylib|\ *.elf|*.map|*.uf2|*.bin|*.merged.bin|\ *.log|*.bak|*.bak.png|*.tmp|*.swp|*.swo|\ .ds_store) return 0 ;; esac return 1 } # Collect files. Prefer Git because it naturally avoids most ignored local files. if command -v git >/dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then git -C "$ROOT" ls-files -co --exclude-standard -z > "$TMP/files.z" else find "$ROOT" -type f -print0 > "$TMP/files.z" fi # Convert to project-relative paths and enforce hard deny rules. : > "$LIST" while IFS= read -r -d '' f; do if [[ "$f" = /* ]]; then rel="${f#"$ROOT"/}" else rel="$f" fi [[ "$rel" == "$OUT" ]] && continue [[ -z "$rel" ]] && continue if is_denied_path "$rel"; then continue fi printf '%s\n' "$rel" >> "$LIST" done < "$TMP/files.z" sort -u "$LIST" -o "$LIST" # Always include Gradle wrapper bootstrap, even though generic JARs are denied. if [[ -f "$ROOT/SHiNE-server/gradle/wrapper/gradle-wrapper.jar" ]]; then if ! grep -Fxq 'SHiNE-server/gradle/wrapper/gradle-wrapper.jar' "$LIST"; then printf '%s\n' 'SHiNE-server/gradle/wrapper/gradle-wrapper.jar' >> "$LIST" sort -u "$LIST" -o "$LIST" fi fi # Content scan: fail closed on common credential/private-key patterns. # We scan only text-ish files; grep -I skips binary data. SECRET_RE='-----BEGIN ([A-Z0-9 ]+ )?PRIVATE KEY-----|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,}|(^|[^A-Za-z0-9])(password|passwd|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key)[[:space:]]*[:=][[:space:]]*["'\'']?[^${[:space:]]{][^[:space:]]{7,}' : > "$SAFE_LIST" found_secret=0 while IFS= read -r rel; do [[ -f "$ROOT/$rel" ]] || continue # Files that contain examples/templates can legitimately mention secret keys # with placeholders. They are scanned too, but placeholder-looking values # are less likely to match the regex above. if LC_ALL=C grep -IEnq "$SECRET_RE" "$ROOT/$rel" 2>/dev/null; then echo "BLOCKED: possible secret in $rel" >&2 LC_ALL=C grep -IEn "$SECRET_RE" "$ROOT/$rel" 2>/dev/null \ | sed -E 's/(:[[:space:]]*).*/\1[REDACTED]/' \ | head -n 3 >&2 || true found_secret=1 continue fi printf '%s\n' "$rel" >> "$SAFE_LIST" done < "$LIST" if (( found_secret != 0 )); then echo >&2 echo "Bundle NOT created because possible secrets were detected." >&2 echo "Move secrets to ignored/local files or adjust the scanner only after review." >&2 exit 2 fi if [[ ! -s "$SAFE_LIST" ]]; then echo "ERROR: no files left to bundle." >&2 exit 3 fi if (( DEFAULT_OUT == 1 )); then rm -f -- "$ROOT"/SHiNE-bundle-*.zip else rm -f -- "$OUT" fi ( cd "$ROOT" zip -q -9 "$OUT" -@ < "$SAFE_LIST" ) echo "Created: $OUT" echo "Files: $(wc -l < "$SAFE_LIST" | tr -d ' ')" echo "Size: $(du -h "$OUT" | awk '{print $1}')"