chore: snapshot current workspace changes
This commit is contained in:
@@ -72,6 +72,8 @@ gitea-migration/
|
|||||||
│ ├── unraid.sh # Remote prerequisites (static binaries)
|
│ ├── unraid.sh # Remote prerequisites (static binaries)
|
||||||
│ ├── fedora.sh # Remote prerequisites (dnf packages)
|
│ ├── fedora.sh # Remote prerequisites (dnf packages)
|
||||||
│ ├── cross_host_ssh.sh # SSH key exchange between Unraid and Fedora
|
│ ├── 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
|
│ └── cleanup.sh # Manifest-driven rollback of setup
|
||||||
├── templates/ # Config templates (.tpl + envsubst)
|
├── templates/ # Config templates (.tpl + envsubst)
|
||||||
│ ├── app.ini.tpl
|
│ ├── 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| Network | MacBook can SSH to both servers, DNS A record pointing to Unraid for HTTPS |
|
||||||
|
|||||||
@@ -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
|
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
|
## Runner Management
|
||||||
|
|||||||
159
setup/bitwarden_to_env.sh
Executable file
159
setup/bitwarden_to_env.sh
Executable file
@@ -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 <<EOF
|
||||||
|
Usage: $(basename "$0") [-f FILE | --bw] [-o FILE] [-n NAME]
|
||||||
|
|
||||||
|
Restore .env from a Bitwarden export.
|
||||||
|
|
||||||
|
Modes (pick one):
|
||||||
|
-f FILE Bitwarden JSON export file (full vault export or single-item)
|
||||||
|
--bw Fetch directly via Bitwarden CLI (requires 'bw' and unlocked vault)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-o FILE Output path (default: $OUTPUT_FILE)
|
||||||
|
-n NAME Item name to find in export (default: $ITEM_NAME)
|
||||||
|
-h Show this help
|
||||||
|
|
||||||
|
To export from Bitwarden:
|
||||||
|
Web vault: Tools → Export Vault → Format: .json
|
||||||
|
CLI: bw export --format json --output vault-export.json
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-f) INPUT_FILE="$2"; shift 2 ;;
|
||||||
|
--bw) USE_BW_CLI=true; shift ;;
|
||||||
|
-o) OUTPUT_FILE="$2"; shift 2 ;;
|
||||||
|
-n) ITEM_NAME="$2"; shift 2 ;;
|
||||||
|
-h) usage ;;
|
||||||
|
*) log_error "Unknown option: $1"; usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! command -v jq &>/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"
|
||||||
121
setup/env_to_bitwarden.sh
Executable file
121
setup/env_to_bitwarden.sh
Executable file
@@ -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 <<EOF
|
||||||
|
Usage: $(basename "$0") [-o FILE] [-n NAME]
|
||||||
|
|
||||||
|
Convert .env to Bitwarden-importable JSON (secure note with custom fields).
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-o FILE Write to file instead of stdout
|
||||||
|
-n NAME Bitwarden item name (default: $ITEM_NAME)
|
||||||
|
-h Show this help
|
||||||
|
|
||||||
|
Import the output via:
|
||||||
|
Bitwarden Web Vault → Tools → Import Data → Format: Bitwarden (json) → Upload
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while getopts "o:n:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
o) OUTPUT_FILE="$OPTARG" ;;
|
||||||
|
n) ITEM_NAME="$OPTARG" ;;
|
||||||
|
h) usage ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
log_error ".env not found at $ENV_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v jq &>/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
|
||||||
@@ -34,7 +34,7 @@ log_success "Homebrew found"
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# Brew packages
|
# Brew packages
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
BREW_PACKAGES=(jq curl gettext shellcheck gh)
|
BREW_PACKAGES=(jq curl gettext shellcheck gh bitwarden-cli)
|
||||||
|
|
||||||
for pkg in "${BREW_PACKAGES[@]}"; do
|
for pkg in "${BREW_PACKAGES[@]}"; do
|
||||||
if brew list "$pkg" &>/dev/null; then
|
if brew list "$pkg" &>/dev/null; then
|
||||||
|
|||||||
Reference in New Issue
Block a user