From 4251163b73e80c15ea67571f0e8cc737df53611d Mon Sep 17 00:00:00 2001 From: James Loh Date: Fri, 27 Jun 2025 17:46:07 +1000 Subject: [PATCH] Add script to migrate existing Ghost config - Ghost requires env variables to be in a special `__` format which is hard to construct manually - This script automates that for ease of migration - Currently doesn't meet our ESLint rules in the slightest so will need tweaking --- scripts/config-to-env.js | 142 +++++++++++++++++++++++++++++++++++++++ scripts/migrate.sh | 19 +++++- 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 scripts/config-to-env.js diff --git a/scripts/config-to-env.js b/scripts/config-to-env.js new file mode 100644 index 0000000..54dccc6 --- /dev/null +++ b/scripts/config-to-env.js @@ -0,0 +1,142 @@ +const fs = require('fs'); +const path = require('path'); + +// Hardcoded exclusions - add any default exclusions here +// Can be exact matches or prefixes (to exclude entire sections) +const HARDCODED_EXCLUSIONS = [ + // We don't want the database, server, logging, process or paths + // entries since they're not relevant in Docker anymore + 'database', + 'server', + 'logging', + 'process', + 'paths', + // We don't need URL because its already set in our env + 'url', +]; + +// Parse command line arguments +function parseArgs() { + const args = process.argv.slice(2); + const options = { + configFile: null, + exclude: [], + include: null + }; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--exclude' && i + 1 < args.length) { + options.exclude = args[i + 1].split(',').map(s => s.trim()); + i++; // Skip next argument + } else if (args[i] === '--include' && i + 1 < args.length) { + options.include = args[i + 1].split(',').map(s => s.trim()); + i++; // Skip next argument + } else if (!args[i].startsWith('--')) { + options.configFile = args[i]; + } + } + + if (!options.configFile) { + console.error('Usage: node config-to-env.js [--exclude key1,key2] [--include key1,key2]'); + process.exit(1); + } + + return options; +} + +// Flatten nested JSON object to environment variable format +function flattenObject(obj, prefix = '') { + const result = {}; + + for (const [key, value] of Object.entries(obj)) { + const newKey = prefix ? `${prefix}__${key}` : key; + + if (value === null || value === undefined) { + // Skip null/undefined values + continue; + } else if (typeof value === 'object' && !Array.isArray(value)) { + // Recursively flatten nested objects + Object.assign(result, flattenObject(value, newKey)); + } else if (Array.isArray(value)) { + // Convert arrays to JSON strings + result[newKey] = JSON.stringify(value); + } else if (typeof value === 'boolean') { + // Convert booleans to lowercase strings + result[newKey] = value.toString(); + } else { + // Store primitive values as-is + result[newKey] = value.toString(); + } + } + + return result; +} + +// Format value for shell output +function formatValue(value) { + // If value contains spaces, newlines, or quotes, wrap in quotes + if (typeof value === 'string' && (value.includes(' ') || value.includes('\n') || value.includes('"') || value.includes("'"))) { + // Escape any existing double quotes + value = value.replace(/"/g, '\\"'); + return `"${value}"`; + } + return value; +} + +// Main function +function main() { + const options = parseArgs(); + + // Read and parse config file + let config; + try { + const configContent = fs.readFileSync(options.configFile, 'utf8'); + config = JSON.parse(configContent); + } catch (error) { + console.error(`Error reading config file: ${error.message}`); + process.exit(1); + } + + // Flatten the configuration + const flattened = flattenObject(config); + + // Combine exclusions + const allExclusions = new Set([...HARDCODED_EXCLUSIONS, ...options.exclude]); + + // Helper function to check if key matches any exclusion pattern + function isExcluded(key, exclusions) { + for (const exclusion of exclusions) { + // Check for exact match + if (key === exclusion) { + return true; + } + // Check if key starts with exclusion pattern (prefix matching) + // This allows excluding entire sections like 'database' or 'database__connection' + if (key.startsWith(exclusion + '__')) { + return true; + } + } + return false; + } + + // Filter and output environment variables + for (const [key, value] of Object.entries(flattened)) { + // If include list is specified, only include those keys + if (options.include && !options.include.includes(key)) { + continue; + } + + // Skip excluded keys (with prefix matching) + if (isExcluded(key, allExclusions)) { + continue; + } + + // Output in KEY=VALUE format + console.log(`${key}=${formatValue(value)}`); + } +} + +// Run the script +if (require.main === module) { + main(); +} diff --git a/scripts/migrate.sh b/scripts/migrate.sh index ba13bd2..0693d0f 100644 --- a/scripts/migrate.sh +++ b/scripts/migrate.sh @@ -109,8 +109,25 @@ echo " MySQL is ready!" echo "Importing database..." docker compose exec -T db sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" $MYSQL_DATABASE' < "${PWD}/data/ghost_import.sql" +echo "The migrator will now try and import your existing configuration: " + +# Run the config-to-env.js script +node "${PWD}/scripts/config-to-env.js" "${current_location}/config.production.json" + +read -rp 'Are you happy with the configuration? [y/n]: ' confirm +case "$confirm" in + [yY][eE][sS]|[yY]) + echo "Adding configuration to .env..." + node "${PWD}/scripts/config-to-env.js" "${current_location}/config.production.json" >> "${PWD}/.env" + ;; + *) + echo "Skipping, you can manually add the configuration to .env later" + echo "Note: Mail configuration is not imported, if you send email you will need to set it up manually" + ;; +esac + # Starting Ghost container -echo "Starting Ghost and containers..." +echo "Starting Ghost..." docker compose up ghost -d read -rp 'Would you like to start Caddy? This will stop your existing Nginx installation. [y/n]: ' confirm