safe-migrate 0.8.0

Check PostgreSQL migrations against a synchronized database baseline
Documentation
#!/bin/sh
set -eu

usage() {
    printf '%s\n' \
        'usage: action-baseline validate <sync> <no-cache> <encrypted-cache> <key-available> <cache> <schemas> <baseline> <mode> <advisory> <path> <config> <output-dir>' \
        '       action-baseline validate-config <config> <encrypted-cache> <no-cache>' \
        '       action-baseline sync <binary> <cache> <config> <schemas>' >&2
    exit 2
}

validate_boolean() {
    name=$1
    value=$2
    case "$value" in
        true|false) ;;
        *)
            printf '%s must be true or false, got: %s\n' "$name" "$value" >&2
            exit 1
            ;;
    esac
}

validate_single_line() {
    name=$1
    value=$2
    newline='
'
    carriage_return=$(printf '\r')
    case "$value" in
        *"$newline"*|*"$carriage_return"*)
            printf '%s must not contain CR or LF characters\n' "$name" >&2
            exit 1
            ;;
    esac
}

command=${1-}
case "$command" in
    validate)
        [ "$#" -eq 13 ] || usage
        sync=$2
        no_cache=$3
        encrypted_cache=$4
        key_available=$5
        cache=$6
        schemas=$7
        baseline=$8
        mode=$9
        advisory=${10}
        path=${11}
        config=${12}
        output_dir=${13}

        validate_boolean sync "$sync"
        validate_boolean no-cache "$no_cache"
        validate_boolean encrypted-cache "$encrypted_cache"
        validate_boolean key-available "$key_available"
        validate_boolean advisory "$advisory"
        validate_single_line cache "$cache"
        validate_single_line schemas "$schemas"
        validate_single_line baseline "$baseline"
        validate_single_line mode "$mode"
        validate_single_line path "$path"
        validate_single_line config "$config"
        validate_single_line output-dir "$output_dir"

        case "$mode" in
            auto)
                if [ -z "$path" ] && [ "$sync" = true ]; then
                    mode='sync-only'
                elif [ -f "$path" ]; then
                    mode=lint
                elif [ -d "$path" ]; then
                    mode=lint-chain
                else
                    printf 'path does not exist or is not a file or directory: %s\n' \
                        "$path" >&2
                    exit 1
                fi
                ;;
            lint)
                [ -f "$path" ] || {
                    printf 'lint path does not exist or is not a file: %s\n' "$path" >&2
                    exit 1
                }
                ;;
            sync-only)
                [ "$sync" = true ] && [ -z "$path" ] || {
                    printf '%s\n' \
                        'sync-only requires sync to be true and path to be empty' >&2
                    exit 1
                }
                ;;
            lint-chain)
                [ -d "$path" ] || {
                    printf 'lint-chain path does not exist or is not a directory: %s\n' \
                        "$path" >&2
                    exit 1
                }
                ;;
            *)
                printf 'mode must be lint or lint-chain, got: %s\n' "$mode" >&2
                exit 1
                ;;
        esac
        [ -n "$output_dir" ] || {
            printf '%s\n' 'output-dir must not be empty' >&2
            exit 1
        }
        case "$output_dir" in
            /*|[A-Za-z]:[\\/]*|*\\*)
                printf '%s\n' 'output-dir must be a relative path inside the workspace' >&2
                exit 1
                ;;
            .|..|./*|../*|*/.|*/..|*/./*|*/../*|*//*|*/)
                printf '%s\n' \
                    'output-dir must not contain empty, dot, or parent path segments' >&2
                exit 1
                ;;
        esac
        old_ifs=$IFS
        IFS=/
        set -f
        # Intentional IFS split after glob expansion is disabled.
        # shellcheck disable=SC2086
        set -- $output_dir
        set +f
        IFS=$old_ifs
        for segment do
            case "$segment" in
                ''|.|..)
                    printf '%s\n' \
                        'output-dir must not contain empty, dot, or parent path segments' >&2
                    exit 1
                    ;;
            esac
        done
        output_cursor=.
        old_ifs=$IFS
        IFS=/
        set -f
        # Intentional IFS split after glob expansion is disabled.
        # shellcheck disable=SC2086
        set -- $output_dir
        set +f
        IFS=$old_ifs
        for segment do
            output_cursor="$output_cursor/$segment"
            if [ -L "$output_cursor" ]; then
                printf 'output-dir must not traverse a symlink: %s\n' \
                    "$output_cursor" >&2
                exit 1
            fi
        done

        if [ "$no_cache" = true ] && {
            [ "$sync" = true ] || [ -n "$cache" ] || [ -n "$schemas" ];
        }; then
            printf '%s\n' \
                'no-cache cannot be combined with cache, sync, or schemas' >&2
            exit 1
        fi
        if [ -n "$schemas" ] && [ "$sync" != true ]; then
            printf '%s\n' 'schemas requires sync to be true' >&2
            exit 1
        fi
        if [ "$no_cache" != true ] && [ "$encrypted_cache" = true ] && \
            [ "$key_available" != true ]; then
            printf '%s\n' \
                'encrypted-cache requires SAFE_MIGRATE_CACHE_KEY; use no-cache only for an explicit degraded analysis' >&2
            exit 1
        fi
        [ -n "$baseline" ] || {
            printf '%s\n' 'baseline must not be empty' >&2
            exit 1
        }
        case "$baseline" in
            .|..)
                printf '%s\n' 'baseline must be a name, not a path segment' >&2
                exit 1
                ;;
        esac
        case "$baseline" in
            *[!A-Za-z0-9._-]*)
                printf '%s\n' \
                    'baseline may contain only letters, digits, dot, underscore, and hyphen' >&2
                exit 1
                ;;
        esac
        [ "${#baseline}" -le 100 ] || {
            printf '%s\n' 'baseline must be at most 100 characters' >&2
            exit 1
        }
        printf '%s\n' "$mode"
        ;;
    validate-config)
        [ "$#" -eq 4 ] || usage
        config=$2
        encrypted_cache=$3
        no_cache=$4
        validate_boolean encrypted-cache "$encrypted_cache"
        validate_boolean no-cache "$no_cache"
        validate_single_line config "$config"

        if [ ! -f "$config" ]; then
            printf 'explicit config does not exist or is not a file: %s\n' \
                "$config" >&2
            exit 1
        fi
        encryption_true_pattern="^[[:space:]]*(cache_encryption|\"cache_encryption\"|'cache_encryption')[[:space:]]*=[[:space:]]*true([[:space:]]*(#.*)?)?$"
        if [ "$no_cache" != true ] && [ "$encrypted_cache" = true ] && \
            ! grep -Eq -- "$encryption_true_pattern" "$config"; then
            printf '%s\n' \
                'encrypted-cache requires cache_encryption = true in the explicit config' >&2
            exit 1
        fi
        if [ "$no_cache" != true ] && [ "$encrypted_cache" != true ] && \
            grep -Eq -- "$encryption_true_pattern" "$config"; then
            printf '%s\n' \
                'explicit config enables cache encryption; set encrypted-cache to true' >&2
            exit 1
        fi
        ;;
    sync)
        [ "$#" -eq 5 ] || usage
        binary=$2
        cache=$3
        config=$4
        schemas=$5

        validate_single_line cache "$cache"
        validate_single_line config "$config"
        validate_single_line schemas "$schemas"

        set -- "$binary" sync --out "$cache" --config "$config"
        if [ -n "$schemas" ]; then
            set -- "$@" --schemas "$schemas"
        fi
        "$@"
        ;;
    *)
        usage
        ;;
esac