From 572e4c151c2a16d239d5b357c58a1cc309c688f7 Mon Sep 17 00:00:00 2001 From: S Date: Sat, 28 Feb 2026 21:35:03 -0500 Subject: [PATCH] chore: snapshot current workspace changes --- README.md | 4 +- USAGE_GUIDE.md | 28 +++++++ setup/bitwarden_to_env.sh | 159 ++++++++++++++++++++++++++++++++++++++ setup/env_to_bitwarden.sh | 121 +++++++++++++++++++++++++++++ setup/macbook.sh | 2 +- 5 files changed, 312 insertions(+), 2 deletions(-) create mode 100755 setup/bitwarden_to_env.sh create mode 100755 setup/env_to_bitwarden.sh diff --git a/README.md b/README.md index 86fa918..91b925b 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ gitea-migration/ │ ├── unraid.sh # Remote prerequisites (static binaries) │ ├── fedora.sh # Remote prerequisites (dnf packages) │ ├── cross_host_ssh.sh # SSH key exchange between Unraid and Fedora +│ ├── env_to_bitwarden.sh # Export .env to Bitwarden JSON import format +│ ├── bitwarden_to_env.sh # Restore .env from Bitwarden CLI │ └── cleanup.sh # Manifest-driven rollback of setup ├── templates/ # Config templates (.tpl + envsubst) │ ├── app.ini.tpl @@ -213,7 +215,7 @@ The Let's Encrypt renewal cron is added via `crontab` on Unraid. Unraid is not d | Machine | Requirements | |---------|-------------| -| MacBook | macOS, Homebrew, jq >= 1.6, curl >= 7.70, git >= 2.30, shellcheck >= 0.8, gh >= 2.0 | +| MacBook | macOS, Homebrew, jq >= 1.6, curl >= 7.70, git >= 2.30, shellcheck >= 0.8, gh >= 2.0, bw >= 2.0 | | Unraid | Linux, Docker >= 20.0, docker-compose >= 2.0, jq >= 1.6, existing Nginx container | | Fedora | Linux with dnf, Docker CE >= 20.0, docker-compose >= 2.0, jq >= 1.6 | | Network | MacBook can SSH to both servers, DNS A record pointing to Unraid for HTTPS | diff --git a/USAGE_GUIDE.md b/USAGE_GUIDE.md index 7530eaa..c2aeb9d 100644 --- a/USAGE_GUIDE.md +++ b/USAGE_GUIDE.md @@ -562,6 +562,34 @@ Add a cron job on the MacBook (or any machine with SSH access to both servers): 0 2 * * * cd /path/to/gitea-migration && ./backup/backup_primary.sh >> /var/log/gitea-backup.log 2>&1 ``` +### Backing up .env to Bitwarden + +The `.env` file is gitignored (it contains passwords and tokens) but is needed for ongoing operations — teardown, runner management, backup/restore, and token rotation. If you lose it, you lose the ability to manage the migration. + +**Export .env to Bitwarden:** + +```bash +./setup/env_to_bitwarden.sh -o bitwarden-import.json +``` + +This creates a Bitwarden-importable JSON file with a secure note called `gitea-migration-env`. Each `.env` variable becomes a custom field — passwords and tokens are marked as hidden fields. + +Import it: Bitwarden Web Vault → Tools → Import Data → Format: Bitwarden (json) → Upload the file. Delete `bitwarden-import.json` from disk afterward. + +**Restore .env from Bitwarden:** + +```bash +bw unlock # unlock your vault first +./setup/bitwarden_to_env.sh --bw # fetches just the one item +./preflight.sh # validate the restored .env +``` + +The script uses `.env.example` as a template to preserve section headers and comments. Run preflight afterward to confirm all variables are correct and connectivity works. + +**Important**: Do not use `bw export` (full vault export) to get the data — it dumps your entire vault to a plaintext JSON file on disk. The `--bw` flag fetches only the `gitea-migration-env` item. + +**After cleanup**: `teardown_all.sh --cleanup` uninstalls the `bw` CLI from your Mac, but the secure note remains in your Bitwarden vault. Reinstall with `brew install bitwarden-cli` if you need to restore later. + --- ## Runner Management diff --git a/setup/bitwarden_to_env.sh b/setup/bitwarden_to_env.sh new file mode 100755 index 0000000..243179d --- /dev/null +++ b/setup/bitwarden_to_env.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +set -euo pipefail +# Restore .env from a Bitwarden JSON export (vault export or single-item JSON). + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +source "$PROJECT_ROOT/lib/common.sh" + +ITEM_NAME="gitea-migration-env" +INPUT_FILE="" +OUTPUT_FILE="$PROJECT_ROOT/.env" +TEMPLATE_FILE="$PROJECT_ROOT/.env.example" +USE_BW_CLI=false + +usage() { + cat >&2 </dev/null; then + log_error "jq is required but not installed" + exit 1 +fi + +# ── Retrieve the Bitwarden item ────────────────────────────────────────────── + +item_json="" + +if $USE_BW_CLI; then + if ! command -v bw &>/dev/null; then + log_error "Bitwarden CLI (bw) not found — install it or use -f with a JSON export" + exit 1 + fi + log_info "Fetching '$ITEM_NAME' from Bitwarden CLI..." + if ! item_json=$(bw get item "$ITEM_NAME" 2>/dev/null); then + log_error "Failed to fetch '$ITEM_NAME'. Is your vault unlocked? (bw unlock)" + exit 1 + fi + +elif [[ -n "$INPUT_FILE" ]]; then + if [[ ! -f "$INPUT_FILE" ]]; then + log_error "File not found: $INPUT_FILE" + exit 1 + fi + + # Full vault export (has .items array) vs single-item JSON (has .fields directly) + if jq -e '.items' "$INPUT_FILE" >/dev/null 2>&1; then + if ! item_json=$(jq -e --arg name "$ITEM_NAME" \ + '.items[] | select(.name == $name)' "$INPUT_FILE" 2>/dev/null); then + log_error "Item '$ITEM_NAME' not found in $INPUT_FILE" + log_info "Available items:" + jq -r '.items[].name' "$INPUT_FILE" >&2 || true + exit 1 + fi + elif jq -e '.fields' "$INPUT_FILE" >/dev/null 2>&1; then + item_json=$(cat "$INPUT_FILE") + else + log_error "Unrecognised JSON format — expected a Bitwarden export or single-item JSON" + exit 1 + fi + +else + log_error "Specify -f FILE or --bw. Run with -h for help." + exit 1 +fi + +# ── Extract fields ─────────────────────────────────────────────────────────── + +fields_json=$(printf '%s' "$item_json" | jq -e '.fields // empty') || { + log_error "No custom fields found in the Bitwarden item" + exit 1 +} + +field_count=$(printf '%s' "$fields_json" | jq 'length') +log_info "Found $field_count fields in '$ITEM_NAME'" + +# ── Reconstruct .env ───────────────────────────────────────────────────────── + +if [[ -f "$TEMPLATE_FILE" ]]; then + log_info "Using .env.example as template (preserves comments and section structure)" + + output="" + while IFS= read -r line || [[ -n "$line" ]]; do + # Pass through comments and blank lines unchanged + if [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]]; then + output+="$line"$'\n' + continue + fi + + key="${line%%=*}" + + # Look up this key in the Bitwarden fields + bw_value=$(printf '%s' "$fields_json" | \ + jq -r --arg k "$key" '.[] | select(.name == $k) | .value // empty') + + if [[ -n "$bw_value" ]]; then + # Quote values that contain shell-sensitive characters + if [[ "$bw_value" =~ [[:space:]\#\$\|\&\;\(\)\<\>] ]]; then + output+="${key}=\"${bw_value}\""$'\n' + else + output+="${key}=${bw_value}"$'\n' + fi + else + # No value in Bitwarden — keep the template line as-is (placeholder) + output+="$line"$'\n' + fi + done < "$TEMPLATE_FILE" + + # Append any Bitwarden fields that aren't in the template (custom additions) + extra_header_written=false + while IFS= read -r key; do + if ! grep -q "^${key}=" "$TEMPLATE_FILE" 2>/dev/null; then + if ! $extra_header_written; then + output+=$'\n'"# --- Additional fields (not in .env.example) ---"$'\n' + extra_header_written=true + fi + value=$(printf '%s' "$fields_json" | \ + jq -r --arg k "$key" '.[] | select(.name == $k) | .value') + output+="${key}=${value}"$'\n' + fi + done < <(printf '%s' "$fields_json" | jq -r '.[].name') + + printf '%s' "$output" > "$OUTPUT_FILE" + +else + log_warn "No .env.example found — writing flat key=value list (no comments)" + printf '%s' "$fields_json" | jq -r '.[] | "\(.name)=\(.value)"' > "$OUTPUT_FILE" +fi + +log_success "Restored .env to $OUTPUT_FILE ($field_count variables)" +log_warn "Review the file before use — check for placeholder values that may need updating" diff --git a/setup/env_to_bitwarden.sh b/setup/env_to_bitwarden.sh new file mode 100755 index 0000000..ab34761 --- /dev/null +++ b/setup/env_to_bitwarden.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +set -euo pipefail +# Convert .env to Bitwarden JSON import format (secure note with custom fields). + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +source "$PROJECT_ROOT/lib/common.sh" + +ITEM_NAME="gitea-migration-env" +ENV_FILE="$PROJECT_ROOT/.env" +OUTPUT_FILE="" + +usage() { + cat >&2 </dev/null; then + log_error "jq is required but not installed" + exit 1 +fi + +# Patterns for fields that should be hidden (type=1) in Bitwarden +sensitive_re="PASSWORD|TOKEN|SECRET|_KEY" + +# Build a TSV of key\tvalue\ttype lines, then convert to JSON in one jq call. +# This avoids calling jq in a loop. +tsv="" +while IFS= read -r line || [[ -n "$line" ]]; do + # Skip comments and blank lines + [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue + + key="${line%%=*}" + value="${line#*=}" + + # Strip surrounding quotes + if [[ "$value" =~ ^\"(.*)\"$ ]]; then + value="${BASH_REMATCH[1]}" + elif [[ "$value" =~ ^\'(.*)\'$ ]]; then + value="${BASH_REMATCH[1]}" + fi + + # Strip inline comments (space + #) + value="${value%%[[:space:]]#*}" + # Trim trailing whitespace + value="${value%"${value##*[![:space:]]}"}" + + field_type=0 + if [[ "$key" =~ $sensitive_re ]]; then + field_type=1 + fi + + # Append as null-delimited triple (safe for any value content) + tsv+="${key}"$'\x00'"${value}"$'\x00'"${field_type}"$'\x00' +done < "$ENV_FILE" + +# Convert null-delimited triples into JSON fields array +fields_json=$(printf '%s' "$tsv" | \ + jq -Rs ' + split("\u0000") | + # drop trailing empty element from final delimiter + if .[-1] == "" then .[:-1] else . end | + [ range(0; length; 3) as $i | + { name: .[$i], value: .[$i+1], type: (.[$i+2] | tonumber) } + ] + ') + +field_count=$(printf '%s' "$fields_json" | jq 'length') + +# Build the complete Bitwarden import envelope +import_json=$(jq -n \ + --arg name "$ITEM_NAME" \ + --argjson fields "$fields_json" \ + '{ + encrypted: false, + folders: [], + items: [ + { + type: 2, + name: $name, + notes: "Gitea migration .env — exported by env_to_bitwarden.sh", + favorite: false, + secureNote: { type: 0 }, + fields: $fields + } + ] + }') + +if [[ -n "$OUTPUT_FILE" ]]; then + printf '%s\n' "$import_json" > "$OUTPUT_FILE" + log_success "Wrote $field_count fields to $OUTPUT_FILE" + log_info "Import via: Bitwarden → Tools → Import Data → Bitwarden (json)" +else + printf '%s\n' "$import_json" +fi diff --git a/setup/macbook.sh b/setup/macbook.sh index 12ce7d0..c21ab68 100755 --- a/setup/macbook.sh +++ b/setup/macbook.sh @@ -34,7 +34,7 @@ log_success "Homebrew found" # -------------------------------------------------------------------------- # Brew packages # -------------------------------------------------------------------------- -BREW_PACKAGES=(jq curl gettext shellcheck gh) +BREW_PACKAGES=(jq curl gettext shellcheck gh bitwarden-cli) for pkg in "${BREW_PACKAGES[@]}"; do if brew list "$pkg" &>/dev/null; then