diff --git a/README.md b/README.md index 1b1bb4e..eaf834d 100644 --- a/README.md +++ b/README.md @@ -108,13 +108,10 @@ When adding a supported base version or variant, add its image index digest to ### Workspace preparation and Git -With `WODBY_WORKSPACE=1`, initialization preflights settings and upload paths before -changing the checkout. It refuses to append configuration to tracked settings or -replace tracked upload files. Generated settings must be ignored and untracked; -ignoring an already tracked file alone is insufficient. - -For tracked settings, add the bootstrap include deliberately and commit it with -application configuration. For example, in `settings.php`: +With `WODBY_WORKSPACE=1`, initialization creates `settings.php` from +`default.settings.php` when it's missing, and appends the Wodby include when the file +doesn't have one. The include is skipped when Wodby's settings file doesn't exist, +so the file keeps working elsewhere and you can commit it: ```php $wodbyConfig = (getenv('CONF_DIR') ?: '/var/www/conf') . '/wodby.settings.php'; @@ -123,9 +120,15 @@ if (is_file($wodbyConfig)) { } ``` -Multisite projects need the equivalent `wodby.sites.php` include in `sites/sites.php`. -Keep uploaded files and generated local settings out of Git. Retry preparation after -fixing the reported paths. Standard initialization retains its existing behavior. -Dependency managers and project scripts can still change source files; inspect the -Git diff before committing. Inherited workspace support requires a PHP base image -that declares workspace contract version 1. +A tracked `settings.php` shows the include as a change to commit. Settings that +already mention `wodby.settings.php` are left unchanged. Multisite projects get the +equivalent `wodby.sites.php` include in `sites/sites.php`. + +Initialization checks every path before changing the checkout. It never changes +files outside the checkout. It replaces the site's `files` directory with a link to +the files volume, so that directory must be ignored by Git and not tracked; ignoring +an already tracked path alone is insufficient. Retry preparation after fixing the +reported path. Standard initialization retains its existing behavior. Dependency +managers and project scripts can still change source files; inspect the Git diff +before committing. Inherited workspace support requires a PHP base image that +declares workspace contract version 1. diff --git a/bin/init_drupal b/bin/init_drupal index 8102385..75338f9 100755 --- a/bin/init_drupal +++ b/bin/init_drupal @@ -10,33 +10,47 @@ disclaimer="\n// Generated by Wodby." settings_php="${DRUPAL_SITE_DIR}/settings.php" default_settings_php="${DRUPAL_SITE_DIR}/default.settings.php" sites_php="${DRUPAL_ROOT}/sites/sites.php" +settings_include="include '${CONF_DIR}/wodby.settings.php';" +sites_include="include '${CONF_DIR}/wodby.sites.php';" -# A workspace checkout belongs to the developer. Preflight every generated path -# before making any changes; only ignored, untracked paths may be generated. +# A workspace checkout belongs to the developer. Settings are created or +# extended as in a build, but the include is skipped where Wodby's file doesn't +# exist, so the developer can commit it. Every path is checked before anything +# changes: nothing outside the checkout is modified, and the files directory +# becomes a link only when Git ignores it and doesn't track it. if [[ "${WODBY_WORKSPACE:-}" == 1 ]]; then root=$(git -C "${APP_ROOT}" rev-parse --show-toplevel) || exit 1 - workspace_generated_path() { - local target relative tracked - target="$(cd "$(dirname "$1")" && pwd -P)/$(basename "$1")" || return 1 + # Prints the path relative to the checkout, or fails for a path outside it. + workspace_path() { + local parent target + parent=$(dirname "$1") + # The site directory may not exist yet. + while [[ ! -d "$parent" ]]; do parent=$(dirname "$parent"); done + target="$(cd "$parent" && pwd -P)${1#"$parent"}" || return 1 if [[ -L "$1" ]]; then target=$(realpath "$1") || return 1; fi - case "$target" in "$root"/*) relative="${target#"$root"/}" ;; *) + case "$target" in "$root"/*) echo "${target#"$root"/}" ;; *) echo "Workspace setup refuses to modify a path outside the checkout: $1" >&2; return 1 ;; esac - tracked=$(git -C "$root" ls-files -- "$relative") || return 1 - if [[ -n "$tracked" ]] || ! git -C "$root" check-ignore -q -- "$relative"; then - echo "Workspace setup would change $relative. Add the required Wodby settings include explicitly, or ignore this generated path and remove it from Git tracking before retrying preparation." >&2 - return 1 - fi } - if [[ ! -f "$settings_php" ]] || ! grep -qi 'wodby.settings.php' "$settings_php"; then - workspace_generated_path "$settings_php" - fi - if [[ "${DRUPAL_SITE}" != default ]] && { [[ ! -f "$sites_php" ]] || ! grep -qi 'wodby.sites.php' "$sites_php"; }; then - workspace_generated_path "$sites_php" + # Prints PHP that includes Wodby's file $1 through the variable $2 when it exists. + workspace_include() { + printf '%s\n' "\$$2 = (getenv('CONF_DIR') ?: '/var/www/conf') . '/$1';" "if (is_file(\$$2)) {" " include \$$2;" "}" + } + workspace_path "$settings_php" >/dev/null + if [[ "${DRUPAL_SITE}" != default ]]; then + workspace_path "$sites_php" >/dev/null fi + # The link replaces the directory, so its contents must not be in Git. if [[ ! -L "${DRUPAL_SITE_DIR}/files" ]]; then - workspace_generated_path "${DRUPAL_SITE_DIR}/files" + files=$(workspace_path "${DRUPAL_SITE_DIR}/files") + tracked=$(git -C "$root" ls-files -- "$files") + if [[ -n "$tracked" ]] || ! git -C "$root" check-ignore -q -- "$files"; then + echo "Workspace setup replaces $files with a link to the files volume. Ignore it in Git and remove it from Git tracking before retrying setup." >&2 + exit 1 + fi fi + settings_include=$(workspace_include wodby.settings.php wodbyConfig) + sites_include=$(workspace_include wodby.sites.php wodbySites) fi mkdir -p "${DRUPAL_SITE_DIR}" @@ -58,7 +72,7 @@ fi if [[ $( grep -ic "wodby.settings.php" "${settings_php}" ) -eq 0 ]]; then chmod 644 "${settings_php}" echo -e "${disclaimer}" >> "${settings_php}" - echo -e "include '${CONF_DIR}/wodby.settings.php';" >> "${settings_php}" + printf '%s\n' "${settings_include}" >> "${settings_php}" fi if [[ "${DRUPAL_SITE}" != "default" ]]; then @@ -68,7 +82,7 @@ if [[ "${DRUPAL_SITE}" != "default" ]]; then if [[ $( grep -ic "wodby.sites.php" "${sites_php}" ) -eq 0 ]]; then echo -e "${disclaimer}" >> "${sites_php}" - echo -e "include '${CONF_DIR}/wodby.sites.php';" >> "${sites_php}" + printf '%s\n' "${sites_include}" >> "${sites_php}" fi fi diff --git a/tests/workspace-checkout.sh b/tests/workspace-checkout.sh index 6722107..a212458 100644 --- a/tests/workspace-checkout.sh +++ b/tests/workspace-checkout.sh @@ -3,38 +3,83 @@ set -euo pipefail script="$(cd "$(dirname "$0")/.." && pwd)/bin/init_drupal" fixture=$(mktemp -d) -trap 'rm -rf "$fixture"' EXIT +outside=$(mktemp -d) +trap 'rm -rf "$fixture" "$outside"' EXIT export APP_ROOT="$fixture" DRUPAL_ROOT="$fixture/web" DRUPAL_SITE=default export DRUPAL_SITE_DIR="$DRUPAL_ROOT/sites/default" CONF_DIR=/var/www/conf FILES_DIR="$fixture/uploads" WODBY_WORKSPACE=1 DEBUG='' +settings="$DRUPAL_SITE_DIR/settings.php" mkdir -p "$DRUPAL_SITE_DIR" "$fixture/bin" printf '#!/bin/sh\n[ -L "$1" ] || ln -s "$FILES_DIR/public" "$1"\n' > "$fixture/bin/files_link" chmod +x "$fixture/bin/files_link" export PATH="$fixture/bin:$PATH" git -C "$fixture" init -q -printf ' "$DRUPAL_SITE_DIR/settings.php" -printf 'web/sites/default/files\n' > "$fixture/.gitignore" +printf ' "$settings" +printf 'web/sites/*/files\n' > "$fixture/.gitignore" git -C "$fixture" add .gitignore web/sites/default/settings.php + +# PHP checks run where PHP is installed, such as CI and the image itself. +php_check() { + if command -v php >/dev/null; then php -l "$1" >/dev/null; fi +} + +# Tracked settings get an include that is skipped outside Wodby, as a change to commit. +bash "$script" +test -L "$DRUPAL_SITE_DIR/files" +grep -q '// customer settings' "$settings" +grep -Fq "\$wodbyConfig = (getenv('CONF_DIR') ?: '/var/www/conf') . '/wodby.settings.php';" "$settings" +grep -Fq 'if (is_file($wodbyConfig)) {' "$settings" +test "$(git -C "$fixture" diff --numstat | cut -f1-2)" = "$(printf '6\t0')" +php_check "$settings" +if command -v php >/dev/null; then + conf=$(mktemp -d "$outside/conf.XXXXXX") + printf ' "$conf/wodby.settings.php" + test "$(CONF_DIR="$conf" php -r 'include $argv[1]; echo empty($settings["wodby"]) ? "skipped" : "included";' "$settings")" = included + test "$(CONF_DIR="$outside/missing" php -r 'include $argv[1]; echo empty($settings["wodby"]) ? "skipped" : "included";' "$settings")" = skipped +fi + +# Running setup again changes nothing. before=$(git -C "$fixture" diff --binary) -if bash "$script" > "$fixture/error" 2>&1; then echo 'Modified tracked settings' >&2; exit 1; fi +bash "$script" test "$before" = "$(git -C "$fixture" diff --binary)" -test ! -L "$DRUPAL_SITE_DIR/files" -grep -q 'Add the required Wodby settings include' "$fixture/error" + # Explicitly configured tracked settings are left byte-for-byte unchanged. -printf " "$DRUPAL_SITE_DIR/settings.php" +printf " "$settings" git -C "$fixture" add web/sites/default/settings.php bash "$script" test -z "$(git -C "$fixture" diff --binary)" -# Missing settings can be generated only when ignored and untracked. + +# Missing settings are created from default.settings.php, tracked or not. git -C "$fixture" rm --cached -q web/sites/default/settings.php -rm "$DRUPAL_SITE_DIR/settings.php" -if bash "$script" > "$fixture/error" 2>&1; then echo 'Generated visible settings' >&2; exit 1; fi -printf 'web/sites/default/settings.php\n' >> "$fixture/.gitignore" +rm "$settings" +printf ' "$DRUPAL_SITE_DIR/default.settings.php" bash "$script" -grep -q wodby.settings.php "$DRUPAL_SITE_DIR/settings.php" -# Tracked upload placeholders must never be removed, even if .gitignored. +grep -q '// Drupal defaults' "$settings" +grep -Fq 'include $wodbyConfig;' "$settings" +php_check "$settings" + +# Multisite projects get the same guarded include in sites.php. +DRUPAL_SITE=example DRUPAL_SITE_DIR="$DRUPAL_ROOT/sites/example" bash "$script" +grep -Fq "\$wodbySites = (getenv('CONF_DIR') ?: '/var/www/conf') . '/wodby.sites.php';" "$DRUPAL_ROOT/sites/sites.php" +grep -Fq 'include $wodbyConfig;' "$DRUPAL_ROOT/sites/example/settings.php" +php_check "$DRUPAL_ROOT/sites/sites.php" + +# Settings linked outside the checkout are never changed. +rm "$settings" +printf ' "$outside/settings.php" +ln -s "$outside/settings.php" "$settings" +if bash "$script" > "$fixture/error" 2>&1; then echo 'Modified settings outside the checkout' >&2; exit 1; fi +grep -q 'outside the checkout' "$fixture/error" +test "$(cat "$outside/settings.php")" = ' "$settings" + +# Tracked upload placeholders must never be removed, even if .gitignored, and +# settings stay unchanged when setup stops. rm "$DRUPAL_SITE_DIR/files" mkdir "$DRUPAL_SITE_DIR/files" printf '*' > "$DRUPAL_SITE_DIR/files/.gitignore" git -C "$fixture" add -f web/sites/default/files/.gitignore if bash "$script" > "$fixture/error" 2>&1; then echo 'Replaced tracked uploads' >&2; exit 1; fi +grep -q 'link to the files volume' "$fixture/error" test -f "$DRUPAL_SITE_DIR/files/.gitignore" +test "$(cat "$settings")" = '