deploy: incremental static rebuilds

This commit is contained in:
E. S. 2025-05-18 00:33:25 +03:00
parent 010ef3588d
commit e926c5e8c8
10 changed files with 341 additions and 16 deletions

View File

@ -11,4 +11,4 @@ deploy:
./deploy/deploy.sh ./deploy/deploy.sh
static: static:
./deploy/static.sh -o "$$(realpath .)" ./deploy/static_incremental.sh -o "$$(realpath .)"

View File

@ -3,6 +3,7 @@ cookie_host: .example.org
admin_email: admin@example.org admin_email: admin@example.org
ic_email: idb.kniga@gmail.com ic_email: idb.kniga@gmail.com
projects: ['ic', 'foreignone', 'omnia']
default_project: 'foreignone' default_project: 'foreignone'
subdomains: subdomains:
dev: foreignone dev: foreignone

View File

@ -1,18 +1,22 @@
#!/bin/sh #!/bin/sh
die() { die() {
>&2 echo "error: $@" >&2 echo "error: $*"
exit 1 exit 1
} }
set -e set -e
if ! command -v yq >/dev/null 2>&1; then
die "yq is not installed. Please install yq to parse YAML files."
fi
DIR=$(cd "$(dirname "$(readlink -f "$0")")" && pwd) DIR=$(cd "$(dirname "$(readlink -f "$0")")" && pwd)
DEV_DIR="$(realpath "$DIR/../")" DEV_DIR="$(realpath "$DIR/../")"
STAGING_DIR="$HOME/staging" STAGING_DIR="$HOME/staging"
PROD_DIR="$HOME/www" PROD_DIR="$HOME/www"
REPO_URI=$(cat "$DEV_DIR/config.yaml" | grep ^git_repo | head -1 | awk '{print $2}') REPO_URI=$(yq -r '.git_repo' "$DEV_DIR/config.yaml")
[ "$(hostname)" = "4in1" ] || die "unexpected hostname. are you sure you're running it on the right machine?" [ "$(hostname)" = "4in1" ] || die "unexpected hostname. are you sure you're running it on the right machine?"
@ -29,6 +33,10 @@ if [ ! -d .git ]; then
fi fi
git reset --hard git reset --hard
# Store the current commit hash before pulling
PREV_COMMIT=$(git rev-parse HEAD)
git pull origin master git pull origin master
composer install --no-dev --optimize-autoloader --ignore-platform-reqs composer install --no-dev --optimize-autoloader --ignore-platform-reqs
@ -39,7 +47,7 @@ if [ ! -d node_modules ]; then
fi fi
cp "$DEV_DIR/config.yaml" . cp "$DEV_DIR/config.yaml" .
"$DIR/static.sh" -o "$STAGING_DIR" "$DIR/static_incremental.sh" -o "$STAGING_DIR" -p "$PREV_COMMIT"
cd "$DIR" cd "$DIR"

View File

@ -1,12 +1,16 @@
#!/bin/sh #!/bin/sh
die() { die() {
>&2 echo "error: $@" >&2 echo "error: $*"
exit 1 exit 1
} }
set -e set -e
if ! command -v yq >/dev/null 2>&1; then
die "yq is not installed. Please install yq to parse YAML files."
fi
PHP="$(which php)" PHP="$(which php)"
SCRIPT_DIR=$(cd "$(dirname "$(readlink -f "$0")")" && pwd) SCRIPT_DIR=$(cd "$(dirname "$(readlink -f "$0")")" && pwd)
APP_DIR="$(realpath "$SCRIPT_DIR/../")" APP_DIR="$(realpath "$SCRIPT_DIR/../")"
@ -22,11 +26,12 @@ while [ $# -gt 0 ]; do
done done
[ -z "$OUTPUT_ROOT_DIR" ] && die "you must specify output directory" [ -z "$OUTPUT_ROOT_DIR" ] && die "you must specify output directory"
for project in ic foreignone omnia; do PROJECTS=$("$SCRIPT_DIR"/util/get_projects.sh "$APP_DIR/config.yaml")
"$SCRIPT_DIR"/util/build_css.sh -i "$APP_DIR/public/$project/scss" -o "$OUTPUT_ROOT_DIR/public/$project/dist-css" || die "build_css failed" for project in $PROJECTS; do
"$SCRIPT_DIR"/util/build_js.sh -i "$APP_DIR/public/common/js" -o "$OUTPUT_ROOT_DIR/public/$project/dist-js" || die "build_js failed" "$SCRIPT_DIR"/util/build_css.sh -i "$APP_DIR/public/$project/scss" -o "$OUTPUT_ROOT_DIR/public/$project/dist-css" || die "build_css failed"
"$SCRIPT_DIR"/util/build_js.sh -i "$APP_DIR/public/common/js" -o "$OUTPUT_ROOT_DIR/public/$project/dist-js" || die "build_js failed"
$PHP "$SCRIPT_DIR"/util/gen_runtime_config.php \ $PHP "$SCRIPT_DIR"/util/gen_runtime_config.php \
--app-root "$OUTPUT_ROOT_DIR" \ --app-root "$OUTPUT_ROOT_DIR" \
--commit-hash "$(git rev-parse --short=8 HEAD)" \ --commit-hash "$(git rev-parse HEAD)" \
> "$OUTPUT_ROOT_DIR/config-runtime.php" || die "gen_runtime_config failed" > "$OUTPUT_ROOT_DIR/config-runtime.php" || die "gen_runtime_config failed"
done done

84
deploy/static_incremental.sh Executable file
View File

@ -0,0 +1,84 @@
#!/bin/sh
die() {
>&2 echo "error: $*"
exit 1
}
set -e
PHP="$(which php)"
SCRIPT_DIR=$(cd "$(dirname "$(readlink -f "$0")")" && pwd)
APP_DIR="$(realpath "$SCRIPT_DIR/../")"
OUTPUT_ROOT_DIR=
PREV_COMMIT=
usage() {
cat <<EOF
usage: $(basename "$0") [OPTIONS]
Options:
-o output directory
-p previous commit hash
-h show this help
EOF
exit 1
}
while [ $# -gt 0 ]; do
case $1 in
-o) OUTPUT_ROOT_DIR="$2"; shift ;;
-p) PREV_COMMIT="$2"; shift ;;
-h) usage ;;
*) die "unexpected argument: $1" ;;
esac
shift
done
[ -z "$OUTPUT_ROOT_DIR" ] && die "you must specify output directory"
# Get current commit hash
CURR_COMMIT=$(git rev-parse HEAD)
# Detect which projects have changed
CHANGED_PROJECTS=$("$SCRIPT_DIR"/util/detect_changes.sh -a "$APP_DIR" -p "$PREV_COMMIT" -c "$CURR_COMMIT")
# If no projects have changed, just update the commit hash in config-runtime.php
if [ -z "$CHANGED_PROJECTS" ]; then
echo "No static files have changed, only updating commit hash in config-runtime.php"
$PHP "$SCRIPT_DIR"/util/gen_runtime_config_incremental.php \
--app-root "$OUTPUT_ROOT_DIR" \
--commit-hash "$CURR_COMMIT" \
--config-file "$OUTPUT_ROOT_DIR/config-runtime.php" \
> "$OUTPUT_ROOT_DIR/config-runtime.php.new" || die "gen_runtime_config_incremental failed"
mv "$OUTPUT_ROOT_DIR/config-runtime.php.new" "$OUTPUT_ROOT_DIR/config-runtime.php"
exit 0
fi
echo "Rebuilding static for projects: $CHANGED_PROJECTS"
# Create output directories if they don't exist
for project in $CHANGED_PROJECTS; do
mkdir -p "$OUTPUT_ROOT_DIR/public/$project/dist-css"
mkdir -p "$OUTPUT_ROOT_DIR/public/$project/dist-js"
done
# Build static for changed projects
for project in $CHANGED_PROJECTS; do
echo "Building CSS for $project..."
"$SCRIPT_DIR"/util/build_css.sh -i "$APP_DIR/public/$project/scss" -o "$OUTPUT_ROOT_DIR/public/$project/dist-css" || die "build_css failed for $project"
echo "Building JS for $project..."
"$SCRIPT_DIR"/util/build_js.sh -i "$APP_DIR/public/common/js" -o "$OUTPUT_ROOT_DIR/public/$project/dist-js" || die "build_js failed for $project"
done
# Generate runtime config with integrity hashes
echo "Generating runtime config..."
$PHP "$SCRIPT_DIR"/util/gen_runtime_config_incremental.php \
--app-root "$OUTPUT_ROOT_DIR" \
--commit-hash "$CURR_COMMIT" \
--changed-projects "$CHANGED_PROJECTS" \
--config-file "$OUTPUT_ROOT_DIR/config-runtime.php" \
> "$OUTPUT_ROOT_DIR/config-runtime.php.new" || die "gen_runtime_config_incremental failed"
mv "$OUTPUT_ROOT_DIR/config-runtime.php.new" "$OUTPUT_ROOT_DIR/config-runtime.php"
echo "Static build completed successfully"

View File

@ -5,17 +5,18 @@ set -e
INDIR= INDIR=
OUTDIR= OUTDIR=
PRESERVE_OUTPUT=0
error() { error() {
>&2 echo "error: $@" >&2 echo "error: $*"
} }
warning() { warning() {
>&2 echo "warning: $@" >&2 echo "warning: $*"
} }
die() { die() {
error "$@" error "$*"
exit 1 exit 1
} }
@ -28,6 +29,7 @@ usage: $PROGNAME [OPTIONS]
Options: Options:
-o output directory -o output directory
-i input directory -i input directory
-p preserve output directory (don't clear it)
-h show this help -h show this help
EOF EOF
exit "$code" exit "$code"
@ -39,6 +41,7 @@ input_args() {
case $1 in case $1 in
-o) OUTDIR="$2"; shift ;; -o) OUTDIR="$2"; shift ;;
-i) INDIR="$2"; shift ;; -i) INDIR="$2"; shift ;;
-p) PRESERVE_OUTPUT=1 ;;
-h) usage ;; -h) usage ;;
*) die "unexpected argument: $1" ;; *) die "unexpected argument: $1" ;;
esac esac
@ -57,7 +60,7 @@ check_args() {
} }
if [ ! -d "$OUTDIR" ]; then if [ ! -d "$OUTDIR" ]; then
mkdir "$OUTDIR" mkdir "$OUTDIR"
else elif [ "$PRESERVE_OUTPUT" -eq 0 ]; then
find "$OUTDIR" -mindepth 1 -delete find "$OUTDIR" -mindepth 1 -delete
fi fi
} }

75
deploy/util/detect_changes.sh Executable file
View File

@ -0,0 +1,75 @@
#!/bin/sh
# This script detects changes in static files after git pull
# It outputs a list of subprojects that need to be rebuilt
set -e
die() {
>&2 echo "error: $*"
exit 1
}
if ! command -v yq >/dev/null 2>&1; then
die "yq is not installed. Please install yq to parse YAML files."
fi
APP_DIR=
PREV_COMMIT=
CURR_COMMIT=
usage() {
cat <<EOF
usage: $(basename "$0") [OPTIONS]
Options:
-a app directory
-p previous commit hash
-c current commit hash
-h show this help
EOF
exit 1
}
while [ $# -gt 0 ]; do
case $1 in
-a) APP_DIR="$2"; shift ;;
-p) PREV_COMMIT="$2"; shift ;;
-c) CURR_COMMIT="$2"; shift ;;
-h) usage ;;
*) die "unexpected argument: $1" ;;
esac
shift
done
[ -z "$APP_DIR" ] && die "app directory not specified"
[ -z "$CURR_COMMIT" ] && die "current commit hash not specified"
SCRIPT_DIR=$(cd "$(dirname "$(readlink -f "$0")")" && pwd)
PROJECTS=$("$SCRIPT_DIR"/get_projects.sh "$APP_DIR/config.yaml")
# If no previous commit is specified, assume all projects need to be rebuilt
if [ -z "$PREV_COMMIT" ]; then
echo "$PROJECTS"
exit 0
fi
# Check if common files have changed
common_changed=$(git diff --name-only "$PREV_COMMIT" "$CURR_COMMIT" -- "$APP_DIR/public/common")
if [ -n "$common_changed" ]; then
# If common files have changed, all projects need to be rebuilt
echo "$PROJECTS"
exit 0
fi
# Check which project-specific files have changed
projects_changed=""
for project in $PROJECTS; do
project_changed=$(git diff --name-only "$PREV_COMMIT" "$CURR_COMMIT" -- "$APP_DIR/public/$project")
if [ -n "$project_changed" ]; then
projects_changed="$projects_changed $project"
fi
done
# Output the list of projects that need to be rebuilt
echo "$projects_changed"

11
deploy/util/gen_runtime_config.php Normal file → Executable file
View File

@ -2,6 +2,13 @@
<?php <?php
require __DIR__.'/../../src/init.php'; require __DIR__.'/../../src/init.php';
global $config;
// Check if yaml extension is available
if (!function_exists('yaml_parse_file')) {
fwrite(STDERR, "error: yaml extension is not installed. Please install php-yaml extension.\n");
exit(1);
}
$commit_hash = null; $commit_hash = null;
$app_root = null; $app_root = null;
@ -28,7 +35,7 @@ $hashes = [
'assets' => [] 'assets' => []
]; ];
foreach (['ic', 'foreignone', 'omnia'] as $project) { foreach ($config['projects'] as $project) {
foreach (['js', 'css'] as $type) { foreach (['js', 'css'] as $type) {
$dist_dir = $app_root.'/public/'.$project.'/dist-'.$type; $dist_dir = $app_root.'/public/'.$project.'/dist-'.$type;
$entries = glob_recursive($dist_dir.'/*.'.$type); $entries = glob_recursive($dist_dir.'/*.'.$type);
@ -81,4 +88,4 @@ function glob_recursive(string $pattern, int $flags = 0): array {
$files = array_merge($files, glob_recursive($dir.'/'.basename($pattern), $flags)); $files = array_merge($files, glob_recursive($dir.'/'.basename($pattern), $flags));
} }
return $files; return $files;
} }

View File

@ -0,0 +1,117 @@
#!/usr/bin/env php
<?php
require __DIR__.'/../../src/init.php';
global $config;
// Check if yaml extension is available
if (!function_exists('yaml_parse_file')) {
fwrite(STDERR, "error: yaml extension is not installed. Please install php-yaml extension.\n");
exit(1);
}
$commit_hash = null;
$app_root = null;
$changed_projects = [];
$config_file = null;
for ($i = 1; $i < $argc; $i++) {
switch ($argv[$i]) {
case '--commit-hash':
$commit_hash = $argv[++$i] ?? usage('missing value for --commit-hash');
break;
case '--app-root':
$app_root = $argv[++$i] ?? usage('missing value for --app-root');
break;
case '--changed-projects':
$changed_projects_str = $argv[++$i] ?? usage('missing value for --changed-projects');
$changed_projects = explode(' ', trim($changed_projects_str));
break;
case '--config-file':
$config_file = $argv[++$i] ?? usage('missing value for --config-file');
break;
default:
usage("unknown option {$argv[$i]}");
}
}
if (is_null($commit_hash) || is_null($app_root))
usage();
$all_projects = $config['projects'];
// Load existing config if available
$hashes = [
'commit_hash' => $commit_hash,
'assets' => []
];
if (!empty($config_file) && file_exists($config_file)) {
$existing_hashes = include $config_file;
if (is_array($existing_hashes) && isset($existing_hashes['assets'])) {
$hashes['assets'] = $existing_hashes['assets'];
}
}
// Process only changed projects or all if none specified
$projects = !empty($changed_projects) ? $changed_projects : $all_projects;
foreach ($projects as $project) {
foreach (['js', 'css'] as $type) {
$dist_dir = $app_root.'/public/'.$project.'/dist-'.$type;
$entries = glob_recursive($dist_dir.'/*.'.$type);
if (empty($entries)) {
fwrite(STDERR, "warning: no files found in $dist_dir\n");
continue;
}
foreach ($entries as $file) {
$asset_key = $type.'/'.basename($file);
$hashes['assets'][$project][$asset_key] = [
'integrity' => []
];
foreach (\engine\skin\FeaturedSkin::RESOURCE_INTEGRITY_HASHES as $hash_type) {
$hashes['assets'][$project][$asset_key]['integrity'][$hash_type] = base64_encode(hash_file($hash_type, $file, true));
}
}
}
}
echo "<?php\n\n";
echo "return ".var_export($hashes, true).";\n";
function usage(string $msg = ''): never {
if ($msg !== '')
fwrite(STDERR, "error: {$msg}\n");
$script = $GLOBALS['argv'][0];
fwrite(STDERR, "usage: {$script} --commit-hash HASH --app-root APP_ROOT [--changed-projects PROJECTS] [--config-file FILE]\n");
exit(1);
}
function glob_escape(string $pattern): string {
if (str_contains($pattern, '[') || str_contains($pattern, ']')) {
$placeholder = uniqid();
$replaces = [$placeholder.'[', $placeholder.']', ];
$pattern = str_replace( ['[', ']'], $replaces, $pattern);
$pattern = str_replace( $replaces, ['[[]', '[]]'], $pattern);
}
return $pattern;
}
/**
* Does not support flag GLOB_BRACE
*
* @param string $pattern
* @param int $flags
* @return array
*/
function glob_recursive(string $pattern, int $flags = 0): array {
$files = glob(glob_escape($pattern), $flags);
foreach (glob(glob_escape(dirname($pattern)).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
$files = array_merge($files, glob_recursive($dir.'/'.basename($pattern), $flags));
}
return $files;
}

25
deploy/util/get_projects.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/sh
# This script parses the projects list from config.yaml using yq
# It outputs a space-separated list of projects
die() {
>&2 echo "error: $*"
exit 1
}
if ! command -v yq >/dev/null 2>&1; then
die "yq is not installed. Please install yq to parse YAML files."
fi
CONFIG_FILE="$1"
[ -z "$CONFIG_FILE" ] && die "config file not specified"
[ -f "$CONFIG_FILE" ] || die "config file not found: $CONFIG_FILE"
# Parse projects list from config.yaml
PROJECTS=$(yq -r '.projects | join(" ")' "$CONFIG_FILE")
# Check if projects list is empty
[ -z "$PROJECTS" ] && die "projects list is empty in $CONFIG_FILE"
echo "$PROJECTS"