diff --git a/README.md b/README.md old mode 100644 new mode 100755 index bd979d7..6ac4b74 --- a/README.md +++ b/README.md @@ -1,3 +1,57 @@ -# radius-cli - -A lightweight, pull-based, Git-driven Continuous Deployment (CD) agent written in pure Bash. \ No newline at end of file +# radius-cli 🚀 + +A lightweight, pull-based, Git-driven Continuous Deployment (CD) agent written in pure Bash. + +**radius-cli** is designed for developers hosting web applications on VPS or shared hosting environments where traditional heavy CI/CD runners (like GitHub Actions, GitLab Runners, or Jenkins) cannot run due to memory limitations or network environment constraints (e.g., SSHFS network mounts, Cygwin, or firewalled networks). + +## ✨ Key Features + +- **Zero Memory Footprint:** Runs instantly via standard Cron intervals. Consumes 0MB of background RAM. +- **Fail-Safe Extraction Guard:** Checks code health before modifying the production web directory to protect your site from network corruption. +- **Smart Directory Mirroring:** Uses optimized `rsync --delete` loops with complete custom exclusions. +- **Granular Daily Operations Logging:** Automatically maintains organized logs in `DDMMYYYY.log` formatting. +- **Integrated Environment Management:** Automatically handles server file permissions (`chmod`/`chown`) and post-deployment optimization steps (`composer install -o`). + +## 🛠️ Installation & Setup + +### 1. Clone the repository structure into your project `bin/` directory: +```bash +mkdir -p bin && cd bin +wget https://git.codelxior.com/jaspreet/radius-cli/archive/main.zip +chmod +x radius-cli +``` + +### 2. Configure Your Environment Variables: +Copy the example config file and fill in your Gitea token credentials: +```bash +cd radius-cli +nano config/main +``` + +### 3. Create your Deployment Excludes File (`exclude.rsync.conf`): +List all runtime variable configurations and user file upload directories that SyncForge must never modify or overwrite: +```text +.env +uploads/ +assets/images/dealers/ +logs/ +syncforge.conf +``` + +## 🚀 Usage Guide + +SyncForge runs as an expressive, command-driven CLI tool layout: + +- **List All Remote Branches:** `./bin/radius list-branch` +- **List System Milestone Releases:** `./bin/radius list-release` +- **Trigger Production Deployment Manually:** `./bin/radius update` +- **Background Cron-Quiet Pipeline Execution:** `./bin/radius update --quiet` + +### Automating with Crontab +To run your web sync operations automatically every night at 2:30 AM, append this single statement to your server's crontab config (`crontab -e`): +```text +30 2 * * * /home/username/public_html/bin/radius update --quiet +``` + +## 📄 License +This utility is open-source software licensed under the [MIT License](LICENSE). diff --git a/bin/radius b/bin/radius new file mode 100755 index 0000000..8ba62ce --- /dev/null +++ b/bin/radius @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# ============================================================================== +# Radius CLI Engine - Modular Git-Driven Deployment Manager +# Developed by: Codelxior (https://github.com) +# License: MIT +# ============================================================================== + +set -e + +# Terminal Style and Color Configurations +BOLD='\e[1m' # Bold text style +DIM='\e[2m' # Dim/Faint text style +UNDERLINE='\e[4m' # Underlined text style +BLINK='\e[5m' # Blinking text style + +# Dark / Standard Colors (Low-Intensity) +DARK_RED='\e[31m' # Dark Red +DARK_GREEN='\e[32m' # Dark Green +DARK_YELLOW='\e[33m' # Dark Yellow +DARK_BLUE='\e[34m' # Dark Blue +DARK_MAGENTA='\e[35m' # Dark Magenta +DARK_CYAN='\e[36m' # Dark Cyan +DARK_WHITE='\e[37m' # Light Grey + +# Light / Bright Colors (High-Intensity) +LIGHT_RED='\e[91m' # Bright Red (Errors) +LIGHT_GREEN='\e[92m' # Bright Green (Success tags) +LIGHT_YELLOW='\e[93m' # Bright Yellow (Warnings) +LIGHT_BLUE='\e[94m' # Bright Blue (System info) +LIGHT_MAGENTA='\e[95m' # Bright Magenta +LIGHT_CYAN='\e[96m' # Bright Cyan (Data streams) +LIGHT_WHITE='\e[97m' # Absolute White + +# END Colors +NC='\e[0m' + + +# Set absolute script path here. +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +# Absolute path to home directory. +DIR_HOME="$(readlink -f "${SCRIPT_DIR}/..")" +# Absolute path to .config directory +DIR_CONFIG="${DIR_HOME}/config" +# Absolute path to library directory +DIR_LIB="${DIR_HOME}/include" +# Absolute path to logs directory +DIR_LOG="${DIR_HOME}/logs" + +# File log. +FILE_LOG="${DIR_LOG}/$(date '+%d%m%Y').log" + + +[ -f "${DIR_CONFIG}/main" ] && source "${DIR_CONFIG}/main" || { + echo -e "Fatal Error: ${LIGHT_RED}Failed to locate main configuration file. ${DIR_CONFIG}/main${NC}"; exit 1; +} + +[ -f "${DIR_LIB}/helper.inc" ] && source "${DIR_LIB}/helper.inc" || { + echo -e "Fatal Error: ${LIGHT_RED}Failed to locate library file. ${DIR_LIB}/helper.inc${NC}"; exit 1; +} + +[ -f "${DIR_LIB}/function.inc" ] && source "${DIR_LIB}/function.inc" || { + echo -e "Fatal Error: ${LIGHT_RED}Failed to locate library file. ${DIR_LIB}/function.inc\e[0m"; exit 1; } + + +[ ! -d "${DIR_LOG}" ] && { mkdir "${DIR_LOG}"; chmod 775 "${DIR_LOG}"; } + +[ ! -f "${FILE_LOG}" ] && { touch "${FILE_LOG}"; chmod 664 "${FILE_LOG}"; function_log "info: ${DARK_GREEN}New log file created.${NC}"; } + + + +MODE_QUIET=false +ARGS_CLEAN=() + +for arg in "$@"; do + + ARG_LOWER=$(function_strtolower "$arg") + + if [ "$ARG_LOWER" == "--quiet" ] || [ "$ARG_LOWER" == "-q" ]; then + + MODE_QUIET=true + + else + + ARGS_CLEAN+=("$arg") + fi + +done + +set -- "${ARGS_CLEAN[@]}" + + + +if [ -z "$1" ]; then + + function_printhelp + + exit 0 + +fi + +PHP_BIN="" +COMPOSER_BIN="" +NODE_BIN="" +NPM_BIN="" + +function_app_init + +case "$1" in + + update) + + function_app_update + ;; + + list-branch) + + function_app_list_branches + ;; + + list-release) + + function_app_list_release + ;; + + --) + + shift; + ;; + + *) + + echo -e "${LIGHT_RED}Unknown operations command: $1${NC}" + function_printhelp + exit 1 + ;; + +esac \ No newline at end of file diff --git a/config/exclude.rsync.conf b/config/exclude.rsync.conf new file mode 100755 index 0000000..7059df4 --- /dev/null +++ b/config/exclude.rsync.conf @@ -0,0 +1,3 @@ +# NOT TO SYNC THESE +# +.gitignore \ No newline at end of file diff --git a/config/include.rsync.conf b/config/include.rsync.conf new file mode 100755 index 0000000..e69de29 diff --git a/config/main b/config/main new file mode 100755 index 0000000..8ea56ad --- /dev/null +++ b/config/main @@ -0,0 +1,26 @@ +# ============================================================================== +# Radius Cli Configuration Environment Variables +# ============================================================================== + +# Enable +enabled=true + +# Server Configuration +REMOTE_HOSTNAME='' +REMOTE_PORT='' +USE_SSL='' + +# Authentication +AUTH_TOKEN='' + +# Project +PROJECT_NAME='' +PROJECT_USER='' +PROJECT_BRANCH='' + +# Check Entry +PROJECT_ENTRY_FILE='' + +# Files and Directories Path +DIR_ROOT='' +DIR_TMP='' diff --git a/include/function.inc b/include/function.inc new file mode 100755 index 0000000..ec994da --- /dev/null +++ b/include/function.inc @@ -0,0 +1,496 @@ +#!/usr/bin/env bash + + function_printhelp () { + + echo -e "-" + echo -e " Usage: radius [command]" + echo -e "-" + + ECO=" :\033[31m\e[1mCOMMAND:DESCRIPTION${NC}\033[0m\n" + ECO+="-\n" + ECO+=" :[ update ]:Updates from remote git server.\n" + ECO+=" :[ list-release ]:List all releases in project.\n" + ECO+=" :[ list-branches ]:List all branches in project.\n" + ECO+="-\n" + + printf "$ECO" | column -t -s ':' + + } + + function_app_init () { + + PHP_BIN="$(type -p "php")" + + if [ $? -ne 0 ] || [ ! -n ${PHP_BIN} ]; then + + function_log "error: ${LIGHT_RED}PHP is not installed or not in PATH.${NC}" + + exit 1 + + fi + + COMPOSER_BIN="$(type -p "composer")" + + if [ $? -ne 0 ] || [ ! -n ${COMPOSER_BIN} ]; then + + function_log "error: ${LIGHT_RED}composer is not installed or not in PATH.${NC}" + + exit 1 + + fi + + NODE_BIN="$(type -p "node")" + + if [ $? -ne 0 ] || [ ! -n ${NODE_BIN} ]; then + + function_log "error: ${LIGHT_RED}node.js is not installed or not in PATH.${NC}" + + exit 1 + + fi + + NPM_BIN="$(type -p "npm")" + + if [ $? -ne 0 ] || [ ! -n ${NPM_BIN} ]; then + + function_log "error: ${LIGHT_RED}npm is not installed or not in PATH.${NC}" + + exit 1 + + fi + + if [ ! -d "${DIR_TMP}" ] || [ ! -w "${DIR_TMP}" ]; then + + function_log "error: ${LIGHT_RED}temporary directory does not exists or not writable.${NC}" true + + exit 1 + + fi + + } + + function_app_update () { + + echo -e "-" + + function_log "info: ${DARK_GREEN}Gathering information...${NC}" true + + echo -e "-\n" + + ECO=" ${LIGHT_RED}SETTING|VALUE${NC}\n" + ECO+="-\n" + ECO+=" ${DARK_GREEN}Remote Host${NC}|$REMOTE_HOSTNAME\n"; + ECO+=" ${DARK_GREEN}Remote Port${NC}|$REMOTE_PORT\n"; + ECO+=" ${DARK_GREEN}Enable SSL${NC}|$(function_strtoupper $USE_SSL)\n"; + ECO+=" ${DARK_GREEN}Repo Name${NC}|$PROJECT_NAME\n"; + ECO+=" ${DARK_GREEN}Repo User${NC}|$PROJECT_USER\n"; + ECO+=" ${DARK_GREEN}Repo Branch${NC}|$PROJECT_BRANCH\n"; + ECO+=" ${DARK_GREEN}Temp Directory${NC}|$DIR_TMP\n"; + ECO+=" ${DARK_GREEN}PHP Engine${NC}|$($PHP_BIN -v | head -n 1)\n"; + ECO+=" ${DARK_GREEN}Composer${NC}|v$($COMPOSER_BIN about | sed -n '1 p' | grep -i 'Composer - Depen' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')\n"; + ECO+=" ${DARK_GREEN}Node.js${NC}|$($NODE_BIN -v)\n"; + ECO+=" ${DARK_GREEN}NPM${NC}|v$($NPM_BIN -v)\n"; + + printf "$ECO" | column -t -s '|' + + echo -e "-" + + echo -e "Remeber this will download source code from remote git server branch or a release than it will synchronize" + echo -e "source and root directory using rsync command. This rync command is configured with exclude and include list." + echo -e "You must configure this exclude and include rsync config files before, only than proceed. If you don't this" + echo -e "can delete some usefull files and directories too." + + echo -e "-" + + ABORT=true + + if [ "${MODE_QUIET}" = false ]; then + + read -p $'\e[92m\e[1mDo you want to procced with above mentioned configuartion ?\e[0m\e[0m Y/N default N: ' ASK + + echo -e "-" + + if [ "$(function_strtolower "${ASK}")" != "y" ] || [ "$(function_strtolower "${ASK}")" != "yes" ]; then + + ABORT=false + + fi + + if [ "${ABORT}" == true ]; then + + function_log "info: ${LIGHT_RED}aborted by user.${NC}" + + exit 0 + + else + + function_log "info: ${LIGHT_GREEN}moving on...${NC}" + + fi + + fi + + if [ "${REMOTE_PORT}" == "80" ]; then + + REMOTE_PORT="" + + elif [ "${REMOTE_PORT}" == "443" ]; then + + USE_SSL="YES" + REMOTE_PORT="" + + fi + + if [ "${USE_SSL}" == "" ] || [ "$(function_strtolower $USE_SSL)" == "yes" ]; then + + REMOTE_URL="https://"; + + else + + REMOTE_URL="https://" + + fi + + REMOTE_URL+="${REMOTE_HOSTNAME}" + + if [ "${REMOTE_PORT}" != "" ]; then + + REMOTE_URL+=":${REMOTE_PORT}" + + fi + + REMOTE_URL+="/api/v1/repos/${PROJECT_USER}/${PROJECT_NAME}/archive/${PROJECT_BRANCH}.zip" + + LOCAL_TAG="${PROJECT_NAME}-${PROJECT_BRANCH}" + LOCAL_DSTI="${DIR_TMP}/${LOCAL_TAG}.zip" + LOCAL_SORC="${DIR_TMP}/${LOCAL_TAG}" + + if [ -f "${LOCAL_DSTI}" ]; then + + echo -e "-" + + function_log "warning: ${LIGHT_CYAN}found previously downloaded file in destination directory.${NC}" true + + function_log "info: ${LIGHT_GREEN}removing....${NC}" + + rm -f "${LOCAL_DSTI}" & + + BACK_PID=$! + + wait $BACK_PID + + function_log "info: ${LIGHT_GREEN}removed successfully.${NC}" true + + echo -e "-" + + fi + + if [ -d "${LOCAL_SORC}" ]; then + + echo -e "-" + + function_log "warning: ${LIGHT_CYAN}found previously extracted source directory.${NC}" true + + function_log "info: ${LIGHT_GREEN}removing....${NC}" + + rm -fR "${LOCAL_SORC}" & + + BACK_PID=$! + + wait $BACK_PID + + function_log "info: ${LIGHT_GREEN}removed successfully.${NC}" true + + echo -e "-" + + fi + + function_log "info: ${LIGHT_GREEN}Downloading - ${REMOTE_URL}.${NC}" true + function_log "info: ${LIGHT_GREEN}Destination - ${LOCAL_DSTI}.${NC}" true + + function_log "info: ${DARK_GREEN}fetching project archive from remote server....${NC}" + + if [ "${MODE_QUIET}" == false ]; then + + wget --header="Accept: application/json" \ + --header="Authorization: Bearer ${AUTH_TOKEN}" \ + --header="Cache-Control: no-cache" \ + --no-check-certificate \ + -O "${LOCAL_DSTI}" \ + "${REMOTE_URL}" & + + else + + wget --header="Accept: application/json" \ + --header="Authorization: Bearer ${AUTH_TOKEN}" \ + --header="Cache-Control: no-cache" \ + --no-check-certificate \ + --quiet \ + -O "${LOCAL_DSTI}" \ + "${REMOTE_URL}" & + + fi + + BACK_PID=$! + + wait $BACK_PID + + if [ -f "${LOCAL_DSTI}" ]; then + + function_log "info: ${LIGHT_GREEN}downloaded successfully.${NC}" true + + else + + function_log "error: ${LIGHT_RED}failed to download project repository.${NC}" true + + exit 1 + + fi + + function_log "info: ${LIGHT_GREEN}extracting....${NC}" + + unzip "${LOCAL_DSTI}" -d "$LOCAL_SORC" > /dev/null + + if [ -d "${LOCAL_SORC}" ]; then + + function_log "info: ${LIGHT_GREEN}checking extracted files......${NC}" true + + [ ! -f "${LOCAL_SORC}/$(function_strtolower "${PROJECT_NAME}")/${PROJECT_ENTRY_FILE}" ] && { function_log "error: ${LIGHT_RED}looks like extracted directory is corrupted. Aborting.${NC}" true; exit 1; } + + function_log "info: ${LIGHT_GREEN}extracted successfully.${NC}" true + + else + + function_log "error: ${LIGHT_RED}failed to extract downloaded file.${NC}" true + + exit 1 + + fi + + + function_log "info: ${DARK_GREEN}Starting updating....${NC}" true + + if [ ! -d "${DIR_ROOT}" ]; then + + function_log "info: ${DARK_GREEN}creating root directory....${NC}" true + + mkdir -p "${DIR_ROOT}" + + fi + + function_log "info: ${LIGHT_GREEN}Root Directory - ${DIR_ROOT}.${NC}" true + + if [ ! -w "${DIR_ROOT}" ]; then + + function_log "error: ${LIGHT_RED}configured root directory is not writable.${NC}" true + + exit 1 + + fi + + function_log "info: ${LIGHT_GREEN}Source Directory - ${LOCAL_SORC}/$(function_strtolower "${PROJECT_NAME}")/.${NC}" true + + function_log "info: ${DARK_GREEN}Updating...${NC}" true + + rsync -at --human-readable --stats --delete \ + --include-from "${DIR_CONFIG}/include.rsync.conf" \ + --exclude-from "${DIR_CONFIG}/exclude.rsync.conf" \ + "${LOCAL_SORC}/$(function_strtolower "${PROJECT_NAME}")/" "${DIR_ROOT}" & + + BACK_PID=$! + + wait $BACK_PID + + function_log "info: ${DARK_GREEN}updation done.${NC}" true + + function_log "info: ${DARK_GREEN}erasing temporary data...${NC}" true + + [ -f "$LOCAL_DSTI" ] && { rm -f $LOCAL_DSTI; } + + [ -d "$LOCAL_SORC" ] && { rm -fR $LOCAL_SORC; } + + function_log "info: ${DARK_GREEN}removed temporary data.${NC}" true + + function_log "info: ${DARK_GREEN}erasing junk data.....${NC}" true + + cd "${DIR_ROOT}" + + find . \( -path ./node_modules -o -path ./vendor \) -prune -false -o \( -name ".gitkeep" -o -name ".gitignore" \) -exec rm -f {} \; + + # Automatically remove log files older than 30 days + find "${DIR_LOG}" -name "*.log" -type f -mtime +30 -delete + + function_log "info: ${DARK_GREEN}removed junk data.${NC}" true + + function_log "info: ${LIGHT_GREEN}all done.${NC}" true + + exit 0 + + } + + function_app_list_release () { + + if [ "${REMOTE_PORT}" == "80" ]; then + + REMOTE_PORT="" + + elif [ "${REMOTE_PORT}" == "443" ]; then + + USE_SSL="YES" + REMOTE_PORT="" + + fi + + if [ "${USE_SSL}" == "" ] || [ "$(function_strtolower $USE_SSL)" == "yes" ]; then + + REMOTE_URL="https://"; + + else + + REMOTE_URL="https://" + + fi + + REMOTE_URL+="${REMOTE_HOSTNAME}" + + if [ "${REMOTE_PORT}" != "" ]; then + + REMOTE_URL+=":${REMOTE_PORT}" + + fi + + RELEASE_URL="${REMOTE_URL}/api/v1/repos/${PROJECT_USER}/${PROJECT_NAME}/releases" + + function_log "info: ${DARK_GREEN}fetching list of all releases.${NC}" true + + RAW_JSON=$(curl -s -k -H "Authorization: Bearer ${AUTH_TOKEN}" "$RELEASE_URL") + + if [ -z "$RAW_JSON" ] || [ "$RAW_JSON" == "[]" ]; then + + function_log "info: ${DARK_GREEN}no releases found for project repository.${NC}" true + + exit 0 + + fi + + echo -e "-" + echo -e "${BOLD}Available Repository Releases:${NC}" + echo -e "--------------------------------------------------" + + echo "$RAW_JSON" | grep -o '{"id":[^{]*' | while read -r row; do + + TAG=$(echo "$row" | grep -o '"tag_name":"[^"]*' | cut -d'"' -f4 || true) + NAME=$(echo "$row" | grep -o '"name":"[^"]*' | cut -d'"' -f4 || true) + IS_PRE=$(echo "$row" | grep -o '"prerelease":[a-z]*' | cut -d':' -f2 || true) + + if [ -z "$TAG" ]; then + + continue + + fi + + if [ -z "$NAME" ]; then + + NAME="No Title" + + fi + + if [ -z "$FIRST_RELEASE" ]; then + + FIRST_RELEASE="done" + echo -e " ${LIGHT_GREEN}➔ ${TAG} - ${NAME} (${BOLD}LATEST / ACTIVE${NC}${LIGHT_GREEN})${NC}" + + else + + if [ "$IS_PRE" == "true" ]; then + + echo -e " ${LIGHT_CYAN}${TAG}${NC} - ${NAME} (Pre-release)" + + else + + echo -e " ${TAG} - ${NAME}" + + fi + + fi + + done + + echo -e "--------------------------------------------------" + echo -e "\n" + + exit 0 + + } + + function_app_list_branches () { + + if [ "${REMOTE_PORT}" == "80" ]; then + + REMOTE_PORT="" + + elif [ "${REMOTE_PORT}" == "443" ]; then + + USE_SSL="YES" + REMOTE_PORT="" + + fi + + if [ "${USE_SSL}" == "" ] || [ "$(function_strtolower $USE_SSL)" == "yes" ]; then + + REMOTE_URL="https://"; + + else + + REMOTE_URL="https://" + + fi + + REMOTE_URL+="${REMOTE_HOSTNAME}" + + if [ "${REMOTE_PORT}" != "" ]; then + + REMOTE_URL+=":${REMOTE_PORT}" + + fi + + RELEASE_URL="${REMOTE_URL}/api/v1/repos/${PROJECT_USER}/${PROJECT_NAME}/branches" + + function_log "info: ${DARK_GREEN}fetching list of all branches.${NC}" true + + RAW_JSON=$(curl -s -k -H "Authorization: Bearer ${AUTH_TOKEN}" "$RELEASE_URL") + + if [ -z "$RAW_JSON" ] || [ "$RAW_JSON" == "[]" ]; then + + function_log "info: ${DARK_GREEN}no branch found for project repository.${NC}" true + + exit 0 + + fi + + echo -e "-" + echo -e "${BOLD}Available Repository Branches:${NC}" + echo -e "--------------------------------------------------" + + echo "$RAW_JSON" | grep -o '{"name":"[^"]*","commit"' | while read -r row; do + + NAME=$(echo "$row" | grep -o '"name":"[^"]*' | cut -d'"' -f4) + + if [ "$NAME" == "main" ]; then + + echo -e " ${LIGHT_GREEN}➔ ${NAME} (${BOLD}DEFAULT / ACTIVE${NC}${LIGHT_GREEN})${NC}" + + else + + echo -e " ${NAME}" + + fi + + done + + echo -e "--------------------------------------------------" + echo -e "\n" + + exit 0 + + } \ No newline at end of file diff --git a/include/helper.inc b/include/helper.inc new file mode 100755 index 0000000..529f9b7 --- /dev/null +++ b/include/helper.inc @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + + function_log () { + + if [ "$1" ]; then + + if [ "${MODE_QUIET}" == false ]; then + + echo -e $1 + + elif [ "${MODE_QUIET}" == true ] && [ "$#" -ge 2 ] && [ "$2" == true ]; then + + echo -e $1 + + fi + + echo -e "`date "+%I:%M:%S"` $(hostname) $SID: $1" | sed -r 's/\x1B\[([0-9]{1,3}(;[0-9]{1,2};?)?)?[mGK]//g' | sed -e 's/^[ \t]*//' >>$FILE_LOG + + fi + + } + + function_strtolower () { + + [[ "$1" == "" ]] && { function_log "error: ${LIGHT_RED}failed to locate any argument in request. File: include/helper.inc, Line: 17${NC}"; exit 1; } + + echo "$(echo "$1" | tr '[:upper:]' '[:lower:]')" + + } + + function_strtoupper () { + + [[ "$1" == "" ]] && { function_log "error: ${LIGHT_RED}failed to locate any argument in request. File: include/helper.inc, Line: 25${NC}"; exit 1; } + + echo "$(echo "$1" | tr '[:lower:]' '[:upper:]')" + + } \ No newline at end of file