710 lines
19 KiB
Bash
Executable File
710 lines
19 KiB
Bash
Executable File
#!/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
|