Skip to content

Instantly share code, notes, and snippets.

@NickSdot
Last active July 6, 2024 14:23
Show Gist options
  • Save NickSdot/3b509895e1585fa6ba8f66ba534dd5c4 to your computer and use it in GitHub Desktop.
Save NickSdot/3b509895e1585fa6ba8f66ba534dd5c4 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Stages Functions
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
BranchAware::RunCommandsInAllBranches()
{
# clear Laravel bootstrap cache to avoid missing dependeny errors
find "$(pwd)/Framework/bootstrap/cache/" -name "*.php" -type f -delete
composer install
php artisan migrate
php artisan dusk:chrome-driver
# npm install
}
BranchAware::RunCommandsInNonExcludedBranches()
{
if BranchAware::Helper::SkipWhenUndesiredBranch; then
return
fi
# add your commands here
}
BranchAware::SetBranch()
{
BRANCH_NAME="$(git rev-parse --abbrev-ref HEAD)"
if [[ -z $BRANCH_NAME ]]; then
BranchAware::Exit 1 "⚠️ Error: Branch could not be detected."
fi
echo "🌿 BranchAware: Running for branch: ${BRANCH_NAME}"
export BRANCH_NAME
}
BranchAware::CreateAndPopulateDatabaseOrSkip()
{
if BranchAware::Helper::SkipWhenUndesiredBranch; then
return
fi
local connection_data
local -r connections=$(php artisan app:deployer:connections)
while IFS= read -r connection_data; do
local keyValuePair
# Values we want to set
local db_name=""
local db_user=""
local db_password=""
local db_host=""
local db_port=""
# Set the database credentials to the local
# vars for processing. Before we used eval
# on the whole line. But this does not
# play well with empty passwords.
for keyValuePair in $connection_data; do
local key
local value
key=${keyValuePair%%=*}
value=${keyValuePair#*=}
eval "$key='$value'"
done
# If the database file exists, and the current database
# name is not included, we do not handle the database.
if BranchAware::Helper::SkipWhenUndesiredDatabase "${db_name}"; then
echo "🚫 Skipping database, because it's not desired: ${db_name}"
continue
fi
local source_db_name=""
source_db_name="$(BranchAware::Helper::GetSourceDatabaseNameFromBranchAndDatabaseName "${BRANCH_NAME}" "${db_name}")"
# If the database already exists, we skip the rest
# of the function, but keep the script running.
if mysql -e "use ${db_name}" 2> /dev/null; then
echo "🚫 Skipping database, because it already exists: ${db_name}"
continue
fi
# Check if the current user exists, otherwise we
# create it. Then we create the new database and
# grant the user access to it.
if ! mysql -e "SELECT 1 FROM mysql.user WHERE user = '${db_user}'" 2> /dev/null; then
mysql -e "CREATE USER '${db_user}'@'${db_host}' IDENTIFIED BY '${db_password}';"
fi
mysql -e "CREATE DATABASE ${db_name};"
mysql -e "GRANT ALL PRIVILEGES ON ${db_name}.* TO '${db_user}'@'${db_host}';"
mysql -e "FLUSH PRIVILEGES;"
mysqldump --no-tablespaces -u "${db_user}" --password="${db_password}" --host="${db_host}" --port="${db_port}" "${source_db_name}" \
| mysql -u "${db_user}" --password="${db_password}" --host="${db_host}" --port="${db_port}" "${db_name}"
done <<< "$connections"
}
BranchAware::DropUnusedDatabases()
{
local connection_data
local -r branches=$(git branch --format="%(refname:short)")
local -r connections=$(php artisan app:deployer:connections)
while IFS= read -r connection_data; do
# Set the database credentials to
# the local vars for processing.
local keyValuePair
# Values we want to set
local db_name=""
local db_user=""
local db_password=""
for keyValuePair in $connection_data; do
local key
local value
key=${keyValuePair%%=*}
value=${keyValuePair#*=}
# Only set vars that are already
# declared above.
if declare -p "$key" 2> /dev/null | grep -q '^declare \-'; then
eval "$key='$value'"
fi
done
local branch_name
for branch_name in $branches; do
local source_db
source_db="$(BranchAware::Helper::GetSourceDatabaseNameFromBranchAndDatabaseName "${branch_name}" "${db_name}")"
if [[ $source_db == "${db_name}" ]]; then
continue
fi
local databases
local database_name
local match_found
databases=$(mysql -u "${db_user}" --password="${db_password}" \
-e "SHOW DATABASES LIKE '${source_db}\_%';" --skip-column-names 2> /dev/null)
for database_name in $databases; do
local existing_branch_name
local generated_branch_name
generated_branch_name="$(BranchAware::Helper::GetBranchNameFromDatabaseName "$database_name" "$source_db")"
# We are looping over all existing branches.
# if the generated branch name matches an
# existing branch name, the database is
# still in use.
match_found=0
for existing_branch_name in $branches; do
if [[ "$existing_branch_name" == "$generated_branch_name" ]]; then
match_found=1
break
fi
done
# If there was no match, the branch no longer
# exists, and we can delete the database
if [[ $match_found -eq 0 ]]; then
mysql -u "${db_user}" --password="${db_password}" -e "DROP DATABASE ${database_name};" 2> /dev/null
fi
done
done
done <<< "$connections"
}
BranchAware::Exit()
{
local -r code="${1-0}"
local -r message="${2-""}"
if [[ -n $message && $code -eq 0 ]]; then
printf "\n\033[0;32m%s\033[0m\n\n" "$message"
fi
if [[ -n $message && $code -eq 1 ]]; then
printf "\n\033[0;31m%s\033[0m\n\n" "$message"
fi
exit "${code}"
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Helper Functions
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
BranchAware::Helper::GetSourceDatabaseNameFromBranchAndDatabaseName()
{
local branch="${1:?"Branch name is not set."}"
local database="${2:?"Database name is not set."}"
local branchSuffix="_branch_${branch//[^a-zA-Z0-9]/_}"
echo -n "${database/$branchSuffix/}"
}
BranchAware::Helper::GetBranchNameFromDatabaseName()
{
local database_name="${1:?"Database name is not set."}"
local source_database_name="${2:?"Source database name is not set."}"
local branch="${database_name//"${source_database_name}_branch_"/}"
echo -n "${branch//_/-}"
}
BranchAware::Helper::SkipWhenUndesiredBranch()
{
if [[ "${BRANCH_NAME}" == "main" ]]; then
return 0
fi
# We allow to skip creating the database by naming
# convention. When the branche name starts/ends
# with an underscore '_' the database handling
# is disabled.
if [[ "${BRANCH_NAME}" =~ ^_|_$ ]]; then
return 0
fi
return 1
}
BranchAware::Helper::SkipWhenUndesiredDatabase()
{
local -r db_name="${1:?"Connection name is missing."}"
local -r database_file="./.deployer/local/databases"
local desired_database_name
if [[ ! -f $database_file ]]; then
return 1
fi
while IFS= read -r desired_database_name; do
local matched=0
if [[ $db_name == "$desired_database_name"* ]]; then
matched=1
break
fi
done < "$database_file"
if [[ $matched -eq 1 ]]; then
return 1
fi
return 0
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Run Main Program
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
BranchAware::Execute()
{
BranchAware::SetBranch
BranchAware::CreateAndPopulateDatabaseOrSkip
BranchAware::RunCommandsInAllBranches
BranchAware::RunCommandsInNonExcludedBranches
BranchAware::DropUnusedDatabases
BranchAware::Exit 0 "🚀 BranchAware: All done."
}
BranchAware::Execute "$@"
<?php
declare(strict_types=1);
namespace Common;
use Business\MultiDomain\DetectEnvironment;
use function exec;
use function in_array;
use function str_ends_with;
use function str_replace;
use function trim;
class BranchAware
{
public static function databaseName(string $environmentKey, string $default = null): string
{
$defaultDatabaseName = getenv($environmentKey) ?: $default ?? throw new \InvalidArgumentException(
'No default database name provided.'
); // todo: custom exception
if (false === in_array(DetectEnvironment::fromGlobals(), ['local', 'dev'])) {
return $defaultDatabaseName;
}
if ('main' === $currentBranch = self::currentBranch()) {
return $defaultDatabaseName;
}
// By convention, we use a trailing underscore to indicate that the
// current branch will not use its own database.
if (true === str_starts_with($currentBranch, '_') || true === str_ends_with($currentBranch, '_')) {
return $defaultDatabaseName;
}
$currentBranchSanitised = str_replace('-', '_', $currentBranch);
return "{$defaultDatabaseName}_branch_{$currentBranchSanitised}";
}
private static function currentBranch(): string
{
return trim(exec('git rev-parse --abbrev-ref HEAD'));
}
}
<?php
declare(strict_types=1);
namespace Business\MultiDomain;
use Symfony\Component\Console\Input\ArgvInput;
readonly final class DetectEnvironment
{
public static function fromGlobals(): string
{
$environment = getenv('APP_ENV') ?: 'production';
if (false === self::isRunningInConsole()) {
return $environment;
}
return (new ArgvInput())->getParameterOption('--env', null) ?? $environment;
}
private static function isRunningInConsole(): bool
{
return false !== getenv('APP_RUNNING_IN_CONSOLE') || 'cli' === \PHP_SAPI || 'phpdbg' === \PHP_SAPI;
}
}
<?php
declare(strict_types=1);
namespace Console\Deployer;
use Business\MultiDomain\DetectEnvironment;
use Common\Result;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
class GetDatabaseConnectionCredentials extends Command
{
protected $signature = 'app:deployer:connections';
protected $description = 'Returns the database connection credentials.';
protected $help = 'The result can be used in a shell script.';
public function handle(): int
{
if (false === in_array(DetectEnvironment::fromGlobals(), [ 'local', 'dev' ])) {
return Result::Failure->value;
}
$databaseConfig = Config::get('database');
// output connection credentials to be
// consumed by shell script.
foreach ($databaseConfig['connections'] as $connection) {
// note: each value is followed by an empty space
echo "db_name={$connection['database']} ";
echo "db_user={$connection['username']} ";
echo "db_host={$connection['host']} ";
echo "db_port={$connection['port']} ";
echo "db_password={$connection['password']} ";
echo PHP_EOL;
}
return Result::Success->value;
}
}
@xtrasmal
Copy link

xtrasmal commented Jul 6, 2024

@NickSdot not in need of such a thing. But it is nice to see other people's problems and ways they try to tackle it.

@xtrasmal
Copy link

xtrasmal commented Jul 6, 2024

A posteriori..
after our conversation I will bookmark this in case of locally raging shitstorms.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment