organize doc files and remove unecessary files
This commit is contained in:
Executable
+709
@@ -0,0 +1,709 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# build_production.sh
|
||||
#
|
||||
# Build a deployable CodeIgniter 4 production directory from a complete
|
||||
# project checkout. The script deliberately refuses incomplete projects,
|
||||
# development Composer dependencies, and non-production environment files.
|
||||
#
|
||||
# Usage:
|
||||
# ./build_production.sh
|
||||
#
|
||||
# Optional:
|
||||
# --skip-composer Use the existing vendor directory. The script rejects
|
||||
# known development packages such as PHPUnit.
|
||||
# --skip-lint Skip PHP syntax checks.
|
||||
# --no-archive Do not create alrahma.zip.
|
||||
# --no-force Refuse to replace an existing output directory.
|
||||
# --help Show help.
|
||||
#
|
||||
# The web-server document root must be <production-folder>/public.
|
||||
#
|
||||
|
||||
set -Eeuo pipefail
|
||||
IFS=$'\n\t'
|
||||
umask 027
|
||||
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
SOURCE_DIR="$(pwd)"
|
||||
OUTPUT_DIR="$(pwd)/build"
|
||||
ENV_FILE="$(pwd)/env.production"
|
||||
SKIP_COMPOSER=0
|
||||
SKIP_LINT=0
|
||||
MAKE_ARCHIVE=1
|
||||
FORCE=1
|
||||
STAGE_ROOT=""
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$SCRIPT_NAME" "$*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '[%s] WARNING: %s\n' "$SCRIPT_NAME" "$*" >&2
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[%s] ERROR: %s\n' "$SCRIPT_NAME" "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Build a CodeIgniter 4 production directory.
|
||||
|
||||
Default:
|
||||
./build_production.sh builds the current project into ./build, copies
|
||||
./env.production as .env, replaces any existing ./build directory, and
|
||||
creates ./alrahma.zip.
|
||||
|
||||
Options:
|
||||
--source DIR Complete project root. Default: current directory.
|
||||
--output DIR Destination. Default: ./build.
|
||||
--env FILE Production environment file. Default: ./env.production.
|
||||
--skip-composer Keep the existing vendor directory instead of running
|
||||
Composer. Known development dependencies are rejected.
|
||||
--skip-lint Skip PHP syntax checks.
|
||||
--archive Create ./alrahma.zip after building. Enabled by
|
||||
default. The archive contains .env and must be
|
||||
protected.
|
||||
--no-archive Do not create ./alrahma.zip.
|
||||
--force Replace an existing output directory. Enabled by
|
||||
default.
|
||||
--no-force Refuse to replace an existing output directory.
|
||||
-h, --help Show this help.
|
||||
|
||||
Example:
|
||||
./build_production.sh
|
||||
USAGE
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${STAGE_ROOT:-}" && -d "$STAGE_ROOT" ]]; then
|
||||
rm -rf -- "$STAGE_ROOT"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'die "Build failed near line $LINENO."' ERR
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--source)
|
||||
[[ $# -ge 2 ]] || die "--source requires a directory."
|
||||
SOURCE_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
[[ $# -ge 2 ]] || die "--output requires a directory."
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--env)
|
||||
[[ $# -ge 2 ]] || die "--env requires a file."
|
||||
ENV_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-composer)
|
||||
SKIP_COMPOSER=1
|
||||
shift
|
||||
;;
|
||||
--skip-lint)
|
||||
SKIP_LINT=1
|
||||
shift
|
||||
;;
|
||||
--archive)
|
||||
MAKE_ARCHIVE=1
|
||||
shift
|
||||
;;
|
||||
--no-archive)
|
||||
MAKE_ARCHIVE=0
|
||||
shift
|
||||
;;
|
||||
--force)
|
||||
FORCE=1
|
||||
shift
|
||||
;;
|
||||
--no-force)
|
||||
FORCE=0
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "Unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
for command_name in php rsync find awk sed grep sort xargs sha256sum zip; do
|
||||
command -v "$command_name" >/dev/null 2>&1 \
|
||||
|| die "Required command is missing: $command_name"
|
||||
done
|
||||
|
||||
[[ -d "$SOURCE_DIR" ]] || die "Source directory does not exist: $SOURCE_DIR"
|
||||
SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd -P)"
|
||||
|
||||
OUTPUT_PARENT="$(dirname "$OUTPUT_DIR")"
|
||||
mkdir -p -- "$OUTPUT_PARENT"
|
||||
OUTPUT_PARENT="$(cd "$OUTPUT_PARENT" && pwd -P)"
|
||||
OUTPUT_DIR="$OUTPUT_PARENT/$(basename "$OUTPUT_DIR")"
|
||||
|
||||
[[ "$OUTPUT_DIR" != "/" ]] || die "Refusing to use / as the output directory."
|
||||
[[ "$OUTPUT_DIR" != "$SOURCE_DIR" ]] \
|
||||
|| die "Output directory must not be the project root."
|
||||
|
||||
[[ -n "$ENV_FILE" ]] \
|
||||
|| die "Pass a production environment file with --env."
|
||||
[[ -f "$ENV_FILE" ]] \
|
||||
|| die "Environment file does not exist: $ENV_FILE"
|
||||
ENV_FILE="$(cd "$(dirname "$ENV_FILE")" && pwd -P)/$(basename "$ENV_FILE")"
|
||||
|
||||
required_paths=(
|
||||
"app"
|
||||
"app/Config/Paths.php"
|
||||
"public"
|
||||
"public/index.php"
|
||||
"writable"
|
||||
"spark"
|
||||
"composer.json"
|
||||
"composer.lock"
|
||||
)
|
||||
|
||||
missing_paths=()
|
||||
for required_path in "${required_paths[@]}"; do
|
||||
if [[ ! -e "$SOURCE_DIR/$required_path" ]]; then
|
||||
missing_paths+=("$required_path")
|
||||
fi
|
||||
done
|
||||
|
||||
if (( ${#missing_paths[@]} > 0 )); then
|
||||
printf '[%s] Missing required project items:\n' "$SCRIPT_NAME" >&2
|
||||
printf ' - %s\n' "${missing_paths[@]}" >&2
|
||||
die "The source is not a complete deployable CodeIgniter project."
|
||||
fi
|
||||
|
||||
[[ -f "$SOURCE_DIR/vendor/autoload.php" || "$SKIP_COMPOSER" -eq 0 ]] \
|
||||
|| die "--skip-composer requires an existing vendor/autoload.php."
|
||||
|
||||
if [[ -e "$OUTPUT_DIR" ]]; then
|
||||
(( FORCE == 1 )) \
|
||||
|| die "Output already exists. Use --force to replace it: $OUTPUT_DIR"
|
||||
log "Removing existing output: $OUTPUT_DIR"
|
||||
rm -rf -- "$OUTPUT_DIR"
|
||||
fi
|
||||
|
||||
env_value() {
|
||||
local key="$1"
|
||||
local file="$2"
|
||||
|
||||
awk -F '=' -v wanted="$key" '
|
||||
/^[[:space:]]*[#;]/ { next }
|
||||
NF < 2 { next }
|
||||
{
|
||||
key = $1
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
||||
if (key != wanted) {
|
||||
next
|
||||
}
|
||||
|
||||
value = substr($0, index($0, "=") + 1)
|
||||
sub(/[[:space:]]+[;#].*$/, "", value)
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
||||
gsub(/^["'\'']|["'\'']$/, "", value)
|
||||
print value
|
||||
exit
|
||||
}
|
||||
' "$file"
|
||||
}
|
||||
|
||||
CI_ENVIRONMENT_VALUE="$(env_value "CI_ENVIRONMENT" "$ENV_FILE" || true)"
|
||||
CI_ENVIRONMENT_VALUE="$(printf '%s' "$CI_ENVIRONMENT_VALUE" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
[[ "$CI_ENVIRONMENT_VALUE" == "production" ]] \
|
||||
|| die "The supplied environment file must set CI_ENVIRONMENT = production."
|
||||
|
||||
for recommended_key in \
|
||||
"app.baseURL" \
|
||||
"database.default.hostname" \
|
||||
"database.default.database" \
|
||||
"database.default.username"
|
||||
do
|
||||
if [[ -z "$(env_value "$recommended_key" "$ENV_FILE" || true)" ]]; then
|
||||
warn "Production environment does not define '$recommended_key'. Verify that it is injected another way."
|
||||
fi
|
||||
done
|
||||
|
||||
STAGE_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/ci4-production-build.XXXXXXXX")"
|
||||
STAGE_DIR="$STAGE_ROOT/release"
|
||||
mkdir -p -- "$STAGE_DIR"
|
||||
|
||||
log "Copying application files into a clean staging directory."
|
||||
|
||||
copy_production_path() {
|
||||
local relative_path="$1"
|
||||
local source_path="$SOURCE_DIR/$relative_path"
|
||||
local destination_path="$STAGE_DIR/$relative_path"
|
||||
|
||||
[[ -e "$source_path" ]] || return 0
|
||||
|
||||
mkdir -p -- "$(dirname "$destination_path")"
|
||||
|
||||
if [[ -d "$source_path" ]]; then
|
||||
mkdir -p -- "$destination_path"
|
||||
rsync -a \
|
||||
--exclude='.DS_Store' \
|
||||
--exclude='__MACOSX/' \
|
||||
"$source_path/" "$destination_path/"
|
||||
else
|
||||
install -m 640 "$source_path" "$destination_path"
|
||||
fi
|
||||
}
|
||||
|
||||
copy_public_files() {
|
||||
mkdir -p -- "$STAGE_DIR/public"
|
||||
rsync -a \
|
||||
--exclude='.DS_Store' \
|
||||
--exclude='__MACOSX/' \
|
||||
--exclude='/docs/***' \
|
||||
--exclude='/gitlab/***' \
|
||||
--exclude='/jenkins/***' \
|
||||
--exclude='/uploads/***' \
|
||||
"$SOURCE_DIR/public/" "$STAGE_DIR/public/"
|
||||
}
|
||||
|
||||
production_paths=(
|
||||
"app"
|
||||
"bootstrap"
|
||||
"routes"
|
||||
"composer.json"
|
||||
"composer.lock"
|
||||
"spark"
|
||||
"preload.php"
|
||||
"LICENSE"
|
||||
)
|
||||
|
||||
for production_path in "${production_paths[@]}"; do
|
||||
copy_production_path "$production_path"
|
||||
done
|
||||
|
||||
copy_public_files
|
||||
|
||||
mkdir -p -- "$STAGE_DIR/writable"
|
||||
copy_production_path "writable/.htaccess"
|
||||
copy_production_path "writable/index.html"
|
||||
copy_production_path "writable/.gitkeep"
|
||||
|
||||
if (( SKIP_COMPOSER == 1 )); then
|
||||
copy_production_path "vendor"
|
||||
fi
|
||||
|
||||
install -m 600 "$ENV_FILE" "$STAGE_DIR/.env"
|
||||
|
||||
# Remove platform debris even when it was nested in an unexpected directory.
|
||||
find "$STAGE_DIR" -name '.DS_Store' -type f -delete
|
||||
find "$STAGE_DIR" -name '__MACOSX' -type d -prune -exec rm -rf {} +
|
||||
|
||||
runtime_directories=(
|
||||
"writable/cache"
|
||||
"writable/debugbar"
|
||||
"writable/logs"
|
||||
"writable/session"
|
||||
"writable/tmp"
|
||||
"writable/uploads"
|
||||
"writable/uploads/class_material"
|
||||
"writable/uploads/early_dismissal_signatures"
|
||||
"writable/uploads/exams"
|
||||
"writable/uploads/print_requests"
|
||||
"writable/uploads/receipts"
|
||||
"writable/uploads/reimbursements"
|
||||
"writable/reports"
|
||||
"writable/report_assets"
|
||||
"public/uploads"
|
||||
"public/uploads/event_flyers"
|
||||
"public/uploads/print_requests"
|
||||
)
|
||||
|
||||
for runtime_directory in "${runtime_directories[@]}"; do
|
||||
mkdir -p -- "$STAGE_DIR/$runtime_directory"
|
||||
: > "$STAGE_DIR/$runtime_directory/.gitkeep"
|
||||
done
|
||||
|
||||
chmod 750 "$STAGE_DIR/spark"
|
||||
chmod 600 "$STAGE_DIR/.env"
|
||||
|
||||
if (( SKIP_COMPOSER == 0 )); then
|
||||
command -v composer >/dev/null 2>&1 \
|
||||
|| die "Composer is required. Install Composer or use --skip-composer with an already production-only vendor directory."
|
||||
|
||||
log "Installing production Composer dependencies."
|
||||
(
|
||||
cd "$STAGE_DIR"
|
||||
COMPOSER_ALLOW_SUPERUSER=1 composer install \
|
||||
--no-dev \
|
||||
--prefer-dist \
|
||||
--optimize-autoloader \
|
||||
--classmap-authoritative \
|
||||
--no-interaction \
|
||||
--no-progress
|
||||
|
||||
COMPOSER_ALLOW_SUPERUSER=1 composer check-platform-reqs --no-dev
|
||||
)
|
||||
else
|
||||
log "Using the existing vendor directory."
|
||||
|
||||
[[ -f "$STAGE_DIR/vendor/autoload.php" ]] \
|
||||
|| die "Existing vendor/autoload.php is missing."
|
||||
|
||||
forbidden_vendor_paths=(
|
||||
"vendor/phpunit"
|
||||
"vendor/fakerphp"
|
||||
"vendor/mockery"
|
||||
"vendor/myclabs/deep-copy"
|
||||
"vendor/phar-io"
|
||||
"vendor/sebastian"
|
||||
"vendor/theseer"
|
||||
)
|
||||
|
||||
found_dev_dependencies=()
|
||||
for vendor_path in "${forbidden_vendor_paths[@]}"; do
|
||||
if [[ -e "$STAGE_DIR/$vendor_path" ]]; then
|
||||
found_dev_dependencies+=("$vendor_path")
|
||||
fi
|
||||
done
|
||||
|
||||
if (( ${#found_dev_dependencies[@]} > 0 )); then
|
||||
printf '[%s] Development packages remain in vendor:\n' "$SCRIPT_NAME" >&2
|
||||
printf ' - %s\n' "${found_dev_dependencies[@]}" >&2
|
||||
die "Run Composer with --no-dev instead of using --skip-composer."
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -f "$STAGE_DIR/vendor/autoload.php" ]] \
|
||||
|| die "Production vendor/autoload.php was not created."
|
||||
|
||||
php -r '
|
||||
require $argv[1];
|
||||
fwrite(STDOUT, "Composer autoload check passed.\n");
|
||||
' "$STAGE_DIR/vendor/autoload.php"
|
||||
|
||||
if (( SKIP_LINT == 0 )); then
|
||||
log "Linting application PHP files."
|
||||
lint_failure_file="$STAGE_ROOT/php-lint-failures.txt"
|
||||
: > "$lint_failure_file"
|
||||
|
||||
while IFS= read -r -d '' php_file; do
|
||||
if ! php -l "$php_file" >/dev/null 2>&1; then
|
||||
printf '%s\n' "$php_file" >> "$lint_failure_file"
|
||||
fi
|
||||
done < <(
|
||||
find \
|
||||
"$STAGE_DIR/app" \
|
||||
"$STAGE_DIR/public" \
|
||||
-type f -name '*.php' -print0
|
||||
)
|
||||
|
||||
if [[ -s "$lint_failure_file" ]]; then
|
||||
cat "$lint_failure_file" >&2
|
||||
die "PHP lint failed."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Apply conservative baseline permissions. The deployment user or PHP-FPM
|
||||
# group can be supplied later using chown/chgrp on the server.
|
||||
find "$STAGE_DIR" -type d -exec chmod 750 {} +
|
||||
find "$STAGE_DIR" -type f -exec chmod 640 {} +
|
||||
chmod 750 "$STAGE_DIR/spark"
|
||||
chmod 600 "$STAGE_DIR/.env"
|
||||
|
||||
for runtime_directory in "${runtime_directories[@]}"; do
|
||||
chmod 770 "$STAGE_DIR/$runtime_directory"
|
||||
done
|
||||
|
||||
mkdir -p "$STAGE_DIR/.deployment"
|
||||
|
||||
cat > "$STAGE_DIR/.deployment/release-check.sh" <<'RELEASE_CHECK'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
errors=0
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$*" >&2
|
||||
errors=$((errors + 1))
|
||||
}
|
||||
|
||||
pass() {
|
||||
printf 'PASS: %s\n' "$*"
|
||||
}
|
||||
|
||||
[[ -f "$ROOT/.env" ]] \
|
||||
&& pass ".env exists" \
|
||||
|| fail ".env is missing"
|
||||
|
||||
if grep -Eq '^[[:space:]]*CI_ENVIRONMENT[[:space:]]*=[[:space:]]*["'\'']?production["'\'']?[[:space:]]*$' "$ROOT/.env"; then
|
||||
pass "CI_ENVIRONMENT is production"
|
||||
else
|
||||
fail "CI_ENVIRONMENT is not production"
|
||||
fi
|
||||
|
||||
for required in \
|
||||
"app/Config/Paths.php" \
|
||||
"public/index.php" \
|
||||
"vendor/autoload.php" \
|
||||
"spark"
|
||||
do
|
||||
[[ -e "$ROOT/$required" ]] \
|
||||
&& pass "$required exists" \
|
||||
|| fail "$required is missing"
|
||||
done
|
||||
|
||||
for writable_path in \
|
||||
"writable/cache" \
|
||||
"writable/logs" \
|
||||
"writable/session" \
|
||||
"writable/tmp" \
|
||||
"writable/uploads"
|
||||
do
|
||||
if [[ -d "$ROOT/$writable_path" && -w "$ROOT/$writable_path" ]]; then
|
||||
pass "$writable_path is writable"
|
||||
else
|
||||
fail "$writable_path is not writable"
|
||||
fi
|
||||
done
|
||||
|
||||
for forbidden in \
|
||||
".git" \
|
||||
"tests" \
|
||||
"node_modules" \
|
||||
"vendor/phpunit" \
|
||||
"phpunit.xml"
|
||||
do
|
||||
[[ ! -e "$ROOT/$forbidden" ]] \
|
||||
&& pass "$forbidden is absent" \
|
||||
|| fail "$forbidden must not be deployed"
|
||||
done
|
||||
|
||||
php -r 'require $argv[1];' "$ROOT/vendor/autoload.php" \
|
||||
&& pass "Composer autoload works" \
|
||||
|| fail "Composer autoload failed"
|
||||
|
||||
printf '\nRequired web document root:\n %s/public\n' "$ROOT"
|
||||
printf '\nBefore traffic is enabled, run database migration status and the application financial smoke tests.\n'
|
||||
|
||||
if (( errors > 0 )); then
|
||||
printf '\nRelease check failed with %d error(s).\n' "$errors" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '\nRelease check passed.\n'
|
||||
RELEASE_CHECK
|
||||
|
||||
chmod 750 "$STAGE_DIR/.deployment/release-check.sh"
|
||||
|
||||
cat > "$STAGE_DIR/.deployment/nginx.conf.example" <<'NGINX'
|
||||
server {
|
||||
listen 80;
|
||||
server_name __DOMAIN__;
|
||||
|
||||
# Point the web root only at public. Exposing the project root is unsafe.
|
||||
root __APP_ROOT__/public;
|
||||
index index.php;
|
||||
|
||||
client_max_body_size 25m;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param HTTP_PROXY "";
|
||||
fastcgi_pass __PHP_FPM_SOCKET__;
|
||||
}
|
||||
|
||||
location ~ /\.(?!well-known).* {
|
||||
deny all;
|
||||
}
|
||||
|
||||
location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|webp|woff|woff2|ttf)$ {
|
||||
expires 7d;
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
}
|
||||
NGINX
|
||||
|
||||
cat > "$STAGE_DIR/.deployment/apache-vhost.conf.example" <<'APACHE'
|
||||
<VirtualHost *:80>
|
||||
ServerName __DOMAIN__
|
||||
DocumentRoot "__APP_ROOT__/public"
|
||||
|
||||
<Directory "__APP_ROOT__/public">
|
||||
AllowOverride All
|
||||
Options FollowSymLinks
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/school-app-error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/school-app-access.log combined
|
||||
</VirtualHost>
|
||||
APACHE
|
||||
|
||||
cat > "$STAGE_DIR/.deployment/DEPLOYMENT.md" <<'DEPLOYMENT'
|
||||
# Production Deployment Notes
|
||||
|
||||
## Required document root
|
||||
|
||||
Configure the web server document root as:
|
||||
|
||||
```text
|
||||
<release-directory>/public
|
||||
```
|
||||
|
||||
Never expose the project root, `app`, `vendor`, `writable`, or `.env`.
|
||||
|
||||
## Server preparation
|
||||
|
||||
1. Install the PHP version and extensions accepted by:
|
||||
|
||||
```bash
|
||||
composer check-platform-reqs --no-dev
|
||||
```
|
||||
|
||||
2. Set the release owner and PHP-FPM group:
|
||||
|
||||
```bash
|
||||
sudo chown -R DEPLOY_USER:PHP_FPM_GROUP /path/to/release
|
||||
```
|
||||
|
||||
3. Keep application code read-only for PHP-FPM. Grant write access only to runtime locations:
|
||||
|
||||
```bash
|
||||
sudo find /path/to/release -type d -exec chmod 750 {} \;
|
||||
sudo find /path/to/release -type f -exec chmod 640 {} \;
|
||||
sudo chmod 600 /path/to/release/.env
|
||||
sudo chmod 750 /path/to/release/spark
|
||||
sudo chmod -R 770 /path/to/release/writable
|
||||
sudo chmod -R 770 /path/to/release/public/uploads
|
||||
```
|
||||
|
||||
4. Run the packaged release check:
|
||||
|
||||
```bash
|
||||
/path/to/release/.deployment/release-check.sh
|
||||
```
|
||||
|
||||
## Database deployment
|
||||
|
||||
Back up the production database first.
|
||||
|
||||
Review migration status before executing migrations:
|
||||
|
||||
```bash
|
||||
cd /path/to/release
|
||||
php spark migrate:status
|
||||
php spark migrate --all
|
||||
```
|
||||
|
||||
Financial hardening migrations must fail if required constraints cannot be installed. Do not accept warning-only deployment of uniqueness or check constraints.
|
||||
|
||||
## Persistent data
|
||||
|
||||
Do not overwrite persistent production data during release replacement:
|
||||
|
||||
- `.env`
|
||||
- `writable/uploads`
|
||||
- `writable/reports`
|
||||
- `writable/report_assets`
|
||||
- `public/uploads`
|
||||
|
||||
Mount or copy these from controlled persistent storage.
|
||||
|
||||
## Traffic activation
|
||||
|
||||
Before enabling traffic:
|
||||
|
||||
1. Run migrations.
|
||||
2. Run release checks.
|
||||
3. Run financial smoke tests for invoice, payment, refund, discount, expense, reimbursement, and purchase-order workflows.
|
||||
4. Confirm scheduled jobs and workers point at the new release.
|
||||
5. Confirm writable ownership.
|
||||
6. Confirm the web server points to `public`.
|
||||
7. Confirm `CI_ENVIRONMENT = production`.
|
||||
8. Confirm debug output is disabled.
|
||||
9. Confirm database backups and rollback instructions exist.
|
||||
|
||||
Use an atomic `current` symlink switch where possible rather than copying files over a live release.
|
||||
DEPLOYMENT
|
||||
|
||||
SOURCE_COMMIT="not-a-git-checkout"
|
||||
if command -v git >/dev/null 2>&1 && git -C "$SOURCE_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
SOURCE_COMMIT="$(git -C "$SOURCE_DIR" rev-parse HEAD)"
|
||||
fi
|
||||
|
||||
cat > "$STAGE_DIR/.deployment/BUILD_INFO" <<BUILD_INFO
|
||||
built_at_utc=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
source_directory=$SOURCE_DIR
|
||||
source_commit=$SOURCE_COMMIT
|
||||
php_version=$(php -r 'echo PHP_VERSION;')
|
||||
composer_mode=$([[ "$SKIP_COMPOSER" -eq 1 ]] && printf 'existing-vendor' || printf 'composer-no-dev')
|
||||
BUILD_INFO
|
||||
|
||||
(
|
||||
cd "$STAGE_DIR"
|
||||
find . -type f \
|
||||
! -path './.env' \
|
||||
! -path './.deployment/MANIFEST.sha256' \
|
||||
-print0 \
|
||||
| sort -z \
|
||||
| xargs -0 sha256sum \
|
||||
> .deployment/MANIFEST.sha256
|
||||
)
|
||||
|
||||
log "Running packaged release verification."
|
||||
"$STAGE_DIR/.deployment/release-check.sh"
|
||||
|
||||
log "Publishing production directory: $OUTPUT_DIR"
|
||||
mkdir -p -- "$(dirname "$OUTPUT_DIR")"
|
||||
mv -- "$STAGE_DIR" "$OUTPUT_DIR"
|
||||
|
||||
if (( MAKE_ARCHIVE == 1 )); then
|
||||
ARCHIVE_PATH="$(dirname "$OUTPUT_DIR")/alrahma.zip"
|
||||
rm -f -- "$ARCHIVE_PATH"
|
||||
|
||||
warn "The archive contains the production .env. Store and transfer it as a secret."
|
||||
(
|
||||
cd "$(dirname "$OUTPUT_DIR")"
|
||||
zip -qr "$ARCHIVE_PATH" "$(basename "$OUTPUT_DIR")"
|
||||
)
|
||||
|
||||
chmod 600 "$ARCHIVE_PATH"
|
||||
log "Created protected archive: $ARCHIVE_PATH"
|
||||
fi
|
||||
|
||||
cat <<SUMMARY
|
||||
|
||||
Production build completed.
|
||||
|
||||
Directory:
|
||||
$OUTPUT_DIR
|
||||
|
||||
Web document root:
|
||||
$OUTPUT_DIR/public
|
||||
|
||||
Verification:
|
||||
$OUTPUT_DIR/.deployment/release-check.sh
|
||||
|
||||
Deployment notes:
|
||||
$OUTPUT_DIR/.deployment/DEPLOYMENT.md
|
||||
|
||||
The build excludes runtime logs, sessions, caches, existing uploads, tests,
|
||||
development metadata, and local environment files. The supplied production
|
||||
environment file was copied as .env with mode 600.
|
||||
SUMMARY
|
||||
@@ -0,0 +1,230 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ===============================
|
||||
# Al Rahma Sunday School Deployment Script
|
||||
# ===============================
|
||||
|
||||
# ----- Domain and App Info -----
|
||||
DM_NAME="home.alrahmaisgl.org"
|
||||
domain_app="alrahma"
|
||||
|
||||
# ----- Database credentials -----
|
||||
DB_HOST="localhost"
|
||||
DB_NAME="u280815660_school"
|
||||
DB_USER="u280815660_melabidi"
|
||||
DB_PASS=">tNxlRzP/W8"
|
||||
|
||||
# ----- Directories -----
|
||||
ZIP_FILE="$1"
|
||||
BASE_DIR="/home/u280815660/domains"
|
||||
DEPLOY_DIR="$BASE_DIR/$domain_app"
|
||||
APP_DIR="$BASE_DIR/$DM_NAME/$domain_app"
|
||||
PUBLIC_DIR="$BASE_DIR/$DM_NAME/public_html"
|
||||
BACKUP_DIR="$BASE_DIR/archive"
|
||||
SECRETS_DIR="$BASE_DIR/deploy_secrets"
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
|
||||
# ----- Runtime binaries -----
|
||||
# Hostinger/CloudLinux exposes PHP 8.5 here. The default CLI `php` may still be
|
||||
# PHP 8.2, which cannot install this project's PHP 8.5 lock file.
|
||||
PHP_BIN="${PHP_BIN:-/opt/alt/php85/usr/bin/php}"
|
||||
COMPOSER_BIN="${COMPOSER_BIN:-$(command -v composer2 || command -v composer || true)}"
|
||||
|
||||
|
||||
# ===============================
|
||||
# 1. VALIDATE INPUT
|
||||
# ===============================
|
||||
if [ -z "$ZIP_FILE" ] || [ ! -f "$ZIP_FILE" ]; then
|
||||
echo "? Please provide the ZIP file: ./deploy_home.sh alrahma_deploy.zip"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "?? Starting deployment of $ZIP_FILE..."
|
||||
|
||||
if [ ! -x "$PHP_BIN" ]; then
|
||||
echo "? PHP 8.5 binary was not found or is not executable: $PHP_BIN"
|
||||
echo " Set PHP_BIN=/path/to/php85 and rerun the deployment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$COMPOSER_BIN" ] || [ ! -e "$COMPOSER_BIN" ]; then
|
||||
echo "? composer2/composer was not found in PATH."
|
||||
echo " Set COMPOSER_BIN=/path/to/composer2 and rerun the deployment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "?? Using PHP: $("$PHP_BIN" -v | head -n 1)"
|
||||
echo "?? Using Composer: $COMPOSER_BIN"
|
||||
|
||||
# ===============================
|
||||
# 2. BACKUP EXISTING SITE
|
||||
# ===============================
|
||||
echo "??? Backing up current site..."
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
cp -r "$PUBLIC_DIR" "$BACKUP_DIR/public_html_$TIMESTAMP"
|
||||
cp -r "$APP_DIR" "$BACKUP_DIR/$domain_app_$TIMESTAMP"
|
||||
|
||||
# ===============================
|
||||
# 3. BACKUP PERSISTENT FILES
|
||||
# ===============================
|
||||
echo "?? Backing up persistent files..."
|
||||
mkdir -p "$SECRETS_DIR"
|
||||
PERSIST_BACKUP="$BASE_DIR/persist_backup_$TIMESTAMP"
|
||||
mkdir -p "$PERSIST_BACKUP"
|
||||
|
||||
# writable/
|
||||
if [ -d "$APP_DIR/writable" ]; then
|
||||
echo " ? Backing up writable/"
|
||||
cp -r "$APP_DIR/writable" "$PERSIST_BACKUP/writable"
|
||||
fi
|
||||
|
||||
# .env
|
||||
if [ -f "$APP_DIR/.env" ]; then
|
||||
echo " ? Backing up .env"
|
||||
cp "$APP_DIR/.env" "$PERSIST_BACKUP/.env"
|
||||
fi
|
||||
|
||||
# public/.htaccess backup is kept for rollback only. The new release file is
|
||||
# deployed below so stale CSP/rewrite rules do not survive deployments.
|
||||
if [ -f "$PUBLIC_DIR/.htaccess" ]; then
|
||||
echo " ? Backing up public .htaccess"
|
||||
cp "$PUBLIC_DIR/.htaccess" "$PERSIST_BACKUP/.htaccess"
|
||||
fi
|
||||
|
||||
# public/index.php backup is kept for rollback only. The new release file is
|
||||
# deployed below and then patched to point at the sibling app directory.
|
||||
if [ -f "$PUBLIC_DIR/index.php" ]; then
|
||||
echo " ? Backing up public index.php"
|
||||
cp "$PUBLIC_DIR/index.php" "$PERSIST_BACKUP/index.php"
|
||||
fi
|
||||
|
||||
# ===============================
|
||||
# 4. CLEAN & EXTRACT NEW DEPLOY
|
||||
# ===============================
|
||||
echo "?? Cleaning old deployment..."
|
||||
rm -rf "$DEPLOY_DIR"
|
||||
unzip "$ZIP_FILE" -d "$BASE_DIR"
|
||||
|
||||
# ===============================
|
||||
# 5. DEPLOY APP FILES
|
||||
# ===============================
|
||||
echo "?? Deploying new app files..."
|
||||
rm -rf "$APP_DIR"
|
||||
mkdir -p "$APP_DIR"
|
||||
cp -r "$DEPLOY_DIR"/. "$APP_DIR"
|
||||
|
||||
# ===============================
|
||||
# 6. RESTORE PERSISTENT FILES
|
||||
# ===============================
|
||||
echo "?? Restoring persistent files..."
|
||||
|
||||
# writable/
|
||||
if [ -d "$PERSIST_BACKUP/writable" ]; then
|
||||
echo " ? Restoring writable/"
|
||||
rm -rf "$APP_DIR/writable"
|
||||
cp -r "$PERSIST_BACKUP/writable" "$APP_DIR/writable"
|
||||
fi
|
||||
|
||||
# .env
|
||||
if [ -f "$PERSIST_BACKUP/.env" ]; then
|
||||
echo " ? Restoring .env"
|
||||
cp "$PERSIST_BACKUP/.env" "$APP_DIR/.env"
|
||||
fi
|
||||
|
||||
# ===============================
|
||||
# 7. DEPLOY PUBLIC FILES
|
||||
# ===============================
|
||||
echo "?? Deploying public files..."
|
||||
mkdir -p "$PUBLIC_DIR"
|
||||
find "$PUBLIC_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
||||
cp -a "$APP_DIR/public/." "$PUBLIC_DIR/"
|
||||
|
||||
# ===============================
|
||||
# 8. FIX index.php PATH
|
||||
# ===============================
|
||||
echo "?? Fixing index.php path..."
|
||||
sed -i "s|require FCPATH . '../app/Config/Paths.php';|require FCPATH . '../alrahma/app/Config/Paths.php';|" "$PUBLIC_DIR/index.php"
|
||||
|
||||
# ===============================
|
||||
# 9. UPDATE BASE URL & DB CONFIG
|
||||
# ===============================
|
||||
APP_CONFIG="$APP_DIR/app/Config/App.php"
|
||||
DB_CONFIG="$APP_DIR/app/Config/Database.php"
|
||||
echo "?? Updating configuration files..."
|
||||
|
||||
# Base URL
|
||||
sed -i "s|public string \$baseURL = .*|public string \$baseURL = 'https://$DM_NAME/';|" "$APP_CONFIG"
|
||||
|
||||
# Database.php
|
||||
sed -i "s|'hostname' => .*|'hostname' => '$DB_HOST',|" "$DB_CONFIG"
|
||||
sed -i "s|'username' => .*|'username' => '$DB_USER',|" "$DB_CONFIG"
|
||||
sed -i "s|'password' => .*|'password' => '$DB_PASS',|" "$DB_CONFIG"
|
||||
sed -i "s|'database' => .*|'database' => '$DB_NAME',|" "$DB_CONFIG"
|
||||
|
||||
# ===============================
|
||||
# 10. UPDATE db_connection.php
|
||||
# ===============================
|
||||
DB_CONN_FILE="$APP_DIR/app/db_connection.php"
|
||||
if [ -f "$DB_CONN_FILE" ]; then
|
||||
echo "??? Updating db_connection.php..."
|
||||
sed -i "s|\$host = '.*';|\$host = '$DB_HOST';|" "$DB_CONN_FILE"
|
||||
sed -i "s|\$dbname = '.*';|\$dbname = '$DB_NAME';|" "$DB_CONN_FILE"
|
||||
sed -i "s|\$username = '.*';|\$username = '$DB_USER';|" "$DB_CONN_FILE"
|
||||
sed -i "s|\$password = '.*';|\$password = '$DB_PASS';|" "$DB_CONN_FILE"
|
||||
fi
|
||||
|
||||
# ===============================
|
||||
# 11. FIX FPDF PATHS
|
||||
# ===============================
|
||||
echo "?? Fixing FPDF paths..."
|
||||
grep -rl "ThirdParty\\\\fpdf\\\\fpdf.php" "$APP_DIR/app" | while read -r file; do
|
||||
sed -i "s|ThirdParty\\\\fpdf\\\\fpdf.php|ThirdParty/fpdf/fpdf.php|g" "$file"
|
||||
echo "? Fixed path in: $file"
|
||||
done
|
||||
|
||||
# ===============================
|
||||
# 12. COMPOSER INSTALL
|
||||
# ===============================
|
||||
echo "?? Installing production Composer dependencies with PHP 8.5..."
|
||||
cd "$APP_DIR" || exit
|
||||
|
||||
export PATH="$(dirname "$PHP_BIN"):$PATH"
|
||||
export COMPOSER_ALLOW_SUPERUSER=1
|
||||
|
||||
if "$PHP_BIN" "$COMPOSER_BIN" --version >/dev/null 2>&1; then
|
||||
"$PHP_BIN" "$COMPOSER_BIN" install \
|
||||
--no-dev \
|
||||
--prefer-dist \
|
||||
--optimize-autoloader \
|
||||
--classmap-authoritative \
|
||||
--no-interaction \
|
||||
--no-progress
|
||||
else
|
||||
"$COMPOSER_BIN" install \
|
||||
--no-dev \
|
||||
--prefer-dist \
|
||||
--optimize-autoloader \
|
||||
--classmap-authoritative \
|
||||
--no-interaction \
|
||||
--no-progress
|
||||
fi
|
||||
|
||||
"$PHP_BIN" spark --version
|
||||
|
||||
# ===============================
|
||||
# 13. RESTORE PERMISSIONS
|
||||
# ===============================
|
||||
echo "?? Setting permissions..."
|
||||
chmod 644 "$PUBLIC_DIR/.htaccess"
|
||||
chmod 644 "$PUBLIC_DIR/index.php"
|
||||
chmod -R 755 "$APP_DIR"
|
||||
chmod 644 "$APP_DIR/app/db_connection.php"
|
||||
chmod -R 777 "$APP_DIR/writable"
|
||||
|
||||
# ===============================
|
||||
# ? DONE
|
||||
# ===============================
|
||||
echo "? Deployment completed successfully at $TIMESTAMP"
|
||||
echo "?? Backup stored in: $BACKUP_DIR"
|
||||
echo "?? Persistent files restored from: $PERSIST_BACKUP"
|
||||
echo "?? Writable and .env restored; public .htaccess and index.php deployed from the new release."
|
||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
container_name: mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK
|
||||
MYSQL_DATABASE: school_prod
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin/phpmyadmin:latest
|
||||
platform: linux/amd64
|
||||
container_name: phpmyadmin
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PMA_HOST: mysql
|
||||
PMA_PORT: 3306
|
||||
ports:
|
||||
- "8081:80"
|
||||
depends_on:
|
||||
- mysql
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# save as find_env_issue.sh and run: bash find_env_issue.sh
|
||||
|
||||
PHP_BIN="/opt/lampp/bin/php" # change if needed
|
||||
ROOT="$(pwd)"
|
||||
INPUT="$ROOT/.env.full"
|
||||
OUT="$ROOT/.env"
|
||||
|
||||
> "$OUT"
|
||||
n=0
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
n=$((n+1))
|
||||
printf "%s\n" "$line" >> "$OUT"
|
||||
"$PHP_BIN" -r "require 'vendor/autoload.php'; try { (new \CodeIgniter\Config\DotEnv(getcwd().'/.env'))->load(); echo 'OK\n'; } catch (Throwable \$e) { echo 'FAIL\n'.get_class(\$e).': '.\$e->getMessage().PHP_EOL; exit(1); }" >/tmp/env_check.log 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo \"Problem detected when adding line $n:\"
|
||||
sed -n \"${n}p\" \"$INPUT\"
|
||||
echo
|
||||
echo \"See /tmp/env_check.log for full output.\"
|
||||
exit 0
|
||||
fi
|
||||
done < "$INPUT"
|
||||
echo "No problems found by incremental test (unexpected)."
|
||||
@@ -0,0 +1,25 @@
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
# === Configuration ===
|
||||
php_path = r"C:\wamp64\bin\php\php8.3.14\php.exe"
|
||||
project_path = r"C:\wamp64\www\alrahma_school_sunday"
|
||||
command = f'"{php_path}" "{project_path}\\spark" users:delete-inactive-users'
|
||||
|
||||
# === Loop Settings ===
|
||||
interval_seconds = 60 * 15 # Run every 5 minutes (change as needed)
|
||||
|
||||
while True:
|
||||
try:
|
||||
print("\n🕐 Running delete-inactive-users command...")
|
||||
subprocess.run(command, check=True, shell=True)
|
||||
print("✅ Command executed successfully.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("❌ Failed to run the command.")
|
||||
print(e)
|
||||
|
||||
print(f"⏳ Waiting {interval_seconds} seconds before next run...\n")
|
||||
time.sleep(interval_seconds)
|
||||
|
||||
|
||||
#/var/lib/vz/dump/vzdump-lxc-104-2025_05_27-02_51_09.tar.zst'
|
||||
Reference in New Issue
Block a user