357 lines
15 KiB
YAML
357 lines
15 KiB
YAML
name: Deploy HickLab3D Dashboard
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
PROJECT_NAME: TechOdyssey_Designs_Dashboard
|
|
DEPLOY_ROOT: /mnt/www-live
|
|
NODE_VERSION: "22.21.1"
|
|
REQUIRED_PHP_VERSION: "8.4.0"
|
|
PHP_CMD: /opt/lhits-toolchain/bin/php
|
|
COMPOSER_CMD: /opt/lhits-toolchain/bin/composer
|
|
|
|
jobs:
|
|
deploy-production:
|
|
name: Deploy to production
|
|
runs-on: [swarm, linux, arm64]
|
|
if: github.ref == 'refs/heads/main'
|
|
|
|
steps:
|
|
- name: Install PHP, Composer, Node and deployment tools
|
|
run: |
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
apt-get update
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
|
ca-certificates curl gnupg lsb-release software-properties-common \
|
|
unzip git rsync xz-utils
|
|
|
|
. /etc/os-release
|
|
if [ "${ID:-}" = "ubuntu" ]; then
|
|
add-apt-repository -y ppa:ondrej/php
|
|
else
|
|
curl -fsSL https://packages.sury.org/php/apt.gpg | gpg --dearmor --yes -o /usr/share/keyrings/deb.sury.org-php.gpg
|
|
echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/php.list
|
|
fi
|
|
|
|
apt-get update
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
|
php8.4-cli php8.4-mbstring php8.4-mysql php8.4-xml php8.4-curl \
|
|
php8.4-zip php8.4-bcmath php8.4-sqlite3 composer
|
|
|
|
PHP_BIN="$(command -v php8.4)"
|
|
elif command -v apk >/dev/null 2>&1; then
|
|
alpine_series="v$(cut -d. -f1,2 /etc/alpine-release)"
|
|
printf '%s\n' \
|
|
"https://dl-cdn.alpinelinux.org/alpine/${alpine_series}/main" \
|
|
"https://dl-cdn.alpinelinux.org/alpine/${alpine_series}/community" \
|
|
"https://dl-cdn.alpinelinux.org/alpine/edge/main" \
|
|
"https://dl-cdn.alpinelinux.org/alpine/edge/community" \
|
|
> /etc/apk/repositories
|
|
apk update
|
|
apk add --no-cache git rsync curl ca-certificates xz libgcc
|
|
apk add --no-cache \
|
|
php84 php84-cli php84-mbstring php84-xml php84-curl php84-zip php84-phar php84-bcmath php84-ctype \
|
|
php84-mysqli php84-pdo php84-pdo_mysql php84-pdo_sqlite php84-sqlite3 php84-openssl php84-fileinfo php84-iconv php84-session
|
|
apk add --no-cache php84-dom php84-tokenizer php84-xmlwriter php84-session || true
|
|
|
|
missing=""
|
|
for extension in dom iconv pdo_sqlite session sqlite3 tokenizer xmlwriter; do
|
|
php84 -r "exit(extension_loaded('${extension}') ? 0 : 1);" || missing="${missing} ${extension}"
|
|
done
|
|
|
|
if [ -n "$missing" ]; then
|
|
apk add --no-cache php84-dom php84-iconv php84-pdo_sqlite php84-session php84-sqlite3 php84-tokenizer php84-xmlwriter || true
|
|
fi
|
|
|
|
PHP_BIN="$(command -v php84)"
|
|
curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php
|
|
"$PHP_BIN" /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer
|
|
else
|
|
echo "Unsupported package manager for installing PHP/Composer/Node" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "${PHP_BIN:-}" ] || [ ! -x "$PHP_BIN" ]; then
|
|
echo "PHP 8.4 binary was not installed or is not executable." >&2
|
|
exit 1
|
|
fi
|
|
|
|
COMPOSER_BIN="$(command -v composer)"
|
|
mkdir -p /opt/lhits-toolchain/bin
|
|
ln -sfn "$PHP_BIN" /opt/lhits-toolchain/bin/php
|
|
printf '%s\n' '#!/bin/sh' "exec \"$PHP_BIN\" \"$COMPOSER_BIN\" \"\$@\"" > /opt/lhits-toolchain/bin/composer
|
|
chmod +x /opt/lhits-toolchain/bin/composer
|
|
"$PHP_CMD" -v
|
|
|
|
arch="$(uname -m)"
|
|
case "$arch" in
|
|
x86_64) node_arch="x64" ;;
|
|
aarch64|arm64) node_arch="arm64" ;;
|
|
*) echo "Unsupported Node architecture: $arch" >&2; exit 1 ;;
|
|
esac
|
|
|
|
if [ -f /etc/alpine-release ] || ldd /bin/sh 2>&1 | grep -qi musl; then
|
|
node_dir="node-v${NODE_VERSION}-linux-${node_arch}-musl"
|
|
node_url="https://unofficial-builds.nodejs.org/download/release/v${NODE_VERSION}/${node_dir}.tar.xz"
|
|
else
|
|
node_dir="node-v${NODE_VERSION}-linux-${node_arch}"
|
|
node_url="https://nodejs.org/dist/v${NODE_VERSION}/${node_dir}.tar.xz"
|
|
fi
|
|
|
|
curl -fsSL "$node_url" -o /tmp/node.tar.xz
|
|
tar -xJf /tmp/node.tar.xz -C /tmp
|
|
rm -rf /usr/local/lib/nodejs
|
|
mkdir -p /usr/local/lib/nodejs
|
|
mv "/tmp/${node_dir}" /usr/local/lib/nodejs/node
|
|
ln -sfn /usr/local/lib/nodejs/node/bin/node /usr/local/bin/node
|
|
ln -sfn /usr/local/lib/nodejs/node/bin/npm /usr/local/bin/npm
|
|
ln -sfn /usr/local/lib/nodejs/node/bin/npx /usr/local/bin/npx
|
|
hash -r
|
|
|
|
- name: Verify runner toolchain
|
|
run: |
|
|
"$PHP_CMD" -v
|
|
"$PHP_CMD" -r '
|
|
$required = getenv("REQUIRED_PHP_VERSION");
|
|
if (version_compare(PHP_VERSION, $required, "<")) {
|
|
fwrite(STDERR, "PHP ".PHP_VERSION." is too old. Required: ".$required." or newer.\n");
|
|
exit(1);
|
|
}
|
|
|
|
$extensions = ["bcmath", "ctype", "curl", "dom", "fileinfo", "iconv", "mbstring", "openssl", "pdo", "pdo_mysql", "pdo_sqlite", "session", "sqlite3", "tokenizer", "xml", "xmlwriter", "zip"];
|
|
$missing = array_values(array_filter($extensions, fn ($extension) => ! extension_loaded($extension)));
|
|
if ($missing !== []) {
|
|
fwrite(STDERR, "Missing required PHP extensions: ".implode(", ", $missing)."\n");
|
|
exit(1);
|
|
}
|
|
'
|
|
|
|
node -e "const [major, minor] = process.versions.node.split('.').map(Number); const ok = (major === 20 && minor >= 19) || (major === 22 && minor >= 12) || major > 22; if (!ok) { console.error('Node '+process.versions.node+' is not supported. Vite requires ^20.19.0 or >=22.12.0.'); process.exit(1); }"
|
|
"$COMPOSER_CMD" --version
|
|
npm --version
|
|
|
|
- name: Clone main
|
|
env:
|
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
REPOSITORY: ${{ github.repository }}
|
|
run: |
|
|
test -n "$GITEA_TOKEN"
|
|
git config --global credential.helper '!f() { echo "username=oauth2"; echo "password=$GITEA_TOKEN"; }; f'
|
|
git clone --depth 1 --single-branch --branch main "https://git.hickmeister.uk/${REPOSITORY}.git" src
|
|
|
|
- name: Install dependencies and verify app
|
|
working-directory: src
|
|
run: |
|
|
"$COMPOSER_CMD" install --no-interaction --prefer-dist --no-progress
|
|
npm ci
|
|
cp .env.example .env
|
|
"$PHP_CMD" artisan key:generate --ansi
|
|
"$PHP_CMD" vendor/bin/phpunit
|
|
npm run build
|
|
test -f public/build/manifest.json
|
|
ls -la public/build
|
|
|
|
- name: Check existing production environment
|
|
env:
|
|
DEPLOY_PATH: ${{ env.DEPLOY_ROOT }}/${{ env.PROJECT_NAME }}
|
|
run: |
|
|
test -d "$DEPLOY_PATH"
|
|
test -f "$DEPLOY_PATH/.env"
|
|
test -f "$DEPLOY_PATH/.htaccess"
|
|
grep -q '^APP_KEY=base64:' "$DEPLOY_PATH/.env"
|
|
|
|
- name: Deploy application to live directory
|
|
env:
|
|
DEPLOY_PATH: ${{ env.DEPLOY_ROOT }}/${{ env.PROJECT_NAME }}
|
|
run: |
|
|
set +e
|
|
rsync -rlz --delete --no-perms --no-owner --no-group --omit-dir-times --no-times --inplace \
|
|
--exclude '.env' \
|
|
--exclude '.htaccess' \
|
|
--exclude '.git/' \
|
|
--exclude '.gitea/' \
|
|
--exclude 'node_modules/' \
|
|
--exclude 'vendor/' \
|
|
--exclude 'tests/' \
|
|
--exclude '.phpunit.cache/' \
|
|
--exclude '.phpunit.result.cache' \
|
|
--exclude 'storage/' \
|
|
--exclude 'bootstrap/cache/' \
|
|
--exclude 'logs/' \
|
|
--exclude 'public/storage' \
|
|
src/ "$DEPLOY_PATH/"
|
|
status=$?
|
|
set -e
|
|
if [ "$status" -ne 0 ]; then
|
|
if [ "$status" -eq 23 ] || [ "$status" -eq 24 ]; then
|
|
echo "rsync reported partial transfer (likely timestamps/attrs not permitted); continuing."
|
|
else
|
|
exit "$status"
|
|
fi
|
|
fi
|
|
|
|
- name: Prepare Laravel directories
|
|
env:
|
|
DEPLOY_PATH: ${{ env.DEPLOY_ROOT }}/${{ env.PROJECT_NAME }}
|
|
RUNTIME_PATH: /tmp/${{ env.PROJECT_NAME }}
|
|
run: |
|
|
mkdir -p \
|
|
"$DEPLOY_PATH/storage/app/public" \
|
|
"$DEPLOY_PATH/storage/framework/cache/data" \
|
|
"$DEPLOY_PATH/storage/framework/sessions" \
|
|
"$DEPLOY_PATH/storage/framework/testing" \
|
|
"$DEPLOY_PATH/storage/framework/views" \
|
|
"$DEPLOY_PATH/storage/logs" \
|
|
"$DEPLOY_PATH/bootstrap/cache" \
|
|
"$RUNTIME_PATH/views" \
|
|
"$RUNTIME_PATH/logs" \
|
|
"$RUNTIME_PATH/cache"
|
|
|
|
- name: Verify deployed Laravel files
|
|
working-directory: ${{ env.DEPLOY_ROOT }}/${{ env.PROJECT_NAME }}
|
|
run: |
|
|
for path in \
|
|
artisan \
|
|
public/index.php \
|
|
public/build/manifest.json \
|
|
routes/web.php \
|
|
resources/views/dashboard.blade.php \
|
|
app/Services/Etsy.php \
|
|
app/Services/Bambuddy.php \
|
|
app/Services/Parcel2Go.php \
|
|
app/Services/DryerClient.php
|
|
do
|
|
if [ ! -f "$path" ]; then
|
|
echo "Missing deployed Laravel file: $(pwd)/$path" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
- name: Require Laravel environment file
|
|
working-directory: ${{ env.DEPLOY_ROOT }}/${{ env.PROJECT_NAME }}
|
|
run: |
|
|
if [ ! -f ".env" ]; then
|
|
echo "Missing .env at $(pwd)/.env. Create the server-side Laravel environment file before deploying." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Ensure writable runtime paths
|
|
working-directory: ${{ env.DEPLOY_ROOT }}/${{ env.PROJECT_NAME }}
|
|
env:
|
|
RUNTIME_PATH: /tmp/${{ env.PROJECT_NAME }}
|
|
run: |
|
|
mkdir -p "$RUNTIME_PATH/views" "$RUNTIME_PATH/logs" "$RUNTIME_PATH/cache"
|
|
chmod 0777 "$RUNTIME_PATH" "$RUNTIME_PATH/views" "$RUNTIME_PATH/logs" "$RUNTIME_PATH/cache"
|
|
|
|
if grep -q '^VIEW_COMPILED_PATH=' .env; then
|
|
sed -i "s|^VIEW_COMPILED_PATH=.*|VIEW_COMPILED_PATH=/tmp/TechOdyssey_Designs_Dashboard.views|" .env
|
|
else
|
|
echo "VIEW_COMPILED_PATH=/tmp/TechOdyssey_Designs_Dashboard.views" >> .env
|
|
fi
|
|
|
|
if grep -q '^LOG_PATH=' .env; then
|
|
sed -i "s|^LOG_PATH=.*|LOG_PATH=/tmp/TechOdyssey_Designs_Dashboard.laravel.log|" .env
|
|
else
|
|
echo "LOG_PATH=/tmp/TechOdyssey_Designs_Dashboard.laravel.log" >> .env
|
|
fi
|
|
|
|
for key_path in \
|
|
"APP_SERVICES_CACHE=/tmp/TechOdyssey_Designs_Dashboard.services.php" \
|
|
"APP_PACKAGES_CACHE=/tmp/TechOdyssey_Designs_Dashboard.packages.php" \
|
|
"APP_CONFIG_CACHE=/tmp/TechOdyssey_Designs_Dashboard.config.php" \
|
|
"APP_ROUTES_CACHE=/tmp/TechOdyssey_Designs_Dashboard.routes.php" \
|
|
"APP_EVENTS_CACHE=/tmp/TechOdyssey_Designs_Dashboard.events.php"
|
|
do
|
|
key="${key_path%%=*}"
|
|
value="${key_path#*=}"
|
|
if grep -q "^${key}=" .env; then
|
|
sed -i "s|^${key}=.*|${key}=${value}|" .env
|
|
else
|
|
echo "${key}=${value}" >> .env
|
|
fi
|
|
done
|
|
|
|
- name: Install production PHP dependencies
|
|
working-directory: ${{ env.DEPLOY_ROOT }}/${{ env.PROJECT_NAME }}
|
|
run: |
|
|
"$COMPOSER_CMD" install --no-dev --no-interaction --prefer-dist --optimize-autoloader
|
|
|
|
- name: Clear compiled Laravel files
|
|
working-directory: ${{ env.DEPLOY_ROOT }}/${{ env.PROJECT_NAME }}
|
|
run: |
|
|
rm -f bootstrap/cache/config.php bootstrap/cache/routes-*.php bootstrap/cache/events.php bootstrap/cache/packages.php bootstrap/cache/services.php
|
|
"$PHP_CMD" artisan config:clear
|
|
"$PHP_CMD" artisan route:clear
|
|
"$PHP_CMD" artisan event:clear
|
|
"$PHP_CMD" artisan view:clear
|
|
|
|
- name: Ensure application key exists
|
|
working-directory: ${{ env.DEPLOY_ROOT }}/${{ env.PROJECT_NAME }}
|
|
run: |
|
|
grep -q '^APP_KEY=base64:' .env
|
|
|
|
- name: Verify database target
|
|
working-directory: ${{ env.DEPLOY_ROOT }}/${{ env.PROJECT_NAME }}
|
|
run: |
|
|
"$PHP_CMD" <<'PHP'
|
|
$env = [];
|
|
foreach (file(".env", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
|
$line = trim($line);
|
|
if ($line === "" || str_starts_with($line, "#") || ! str_contains($line, "=")) {
|
|
continue;
|
|
}
|
|
[$key, $value] = explode("=", $line, 2);
|
|
$env[$key] = trim($value, "\"'");
|
|
}
|
|
|
|
$connection = $env["DB_CONNECTION"] ?? "";
|
|
if (! in_array($connection, ["mysql", "mariadb"], true)) {
|
|
fwrite(STDERR, "DB_CONNECTION must be mysql or mariadb for this deployment. Current value: ".$connection."\n");
|
|
exit(1);
|
|
}
|
|
|
|
$host = $env["DB_HOST"] ?? "";
|
|
$port = (int) ($env["DB_PORT"] ?? 3306);
|
|
$database = $env["DB_DATABASE"] ?? "";
|
|
if ($host === "") {
|
|
fwrite(STDERR, "DB_HOST is missing from .env. Set DB_HOST to the MySQL server IP or hostname.\n");
|
|
exit(1);
|
|
}
|
|
if ($database === "") {
|
|
fwrite(STDERR, "DB_DATABASE is missing from .env.\n");
|
|
exit(1);
|
|
}
|
|
|
|
$socket = @fsockopen($host, $port, $errno, $errstr, 5);
|
|
if (! $socket) {
|
|
fwrite(STDERR, "Cannot reach MySQL at DB_HOST=".$host." DB_PORT=".$port.": ".$errstr." (".$errno.")\n");
|
|
exit(1);
|
|
}
|
|
fclose($socket);
|
|
echo "Database target reachable at DB_HOST=".$host." DB_PORT=".$port." DB_DATABASE=".$database."\n";
|
|
PHP
|
|
|
|
- name: Run Laravel migrations
|
|
working-directory: ${{ env.DEPLOY_ROOT }}/${{ env.PROJECT_NAME }}
|
|
run: |
|
|
"$PHP_CMD" artisan migrate --force
|
|
|
|
- name: Optimise Laravel
|
|
working-directory: ${{ env.DEPLOY_ROOT }}/${{ env.PROJECT_NAME }}
|
|
run: |
|
|
rm -f public/hot
|
|
rm -rf build
|
|
cp -a public/build build
|
|
rm -rf public/storage
|
|
ln -s ../storage/app/public public/storage
|
|
"$PHP_CMD" artisan config:clear
|
|
"$PHP_CMD" artisan route:clear
|
|
"$PHP_CMD" artisan view:clear
|