Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 16 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
52 changes: 33 additions & 19 deletions bin/init_drupal
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -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
Expand All @@ -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

Expand Down
71 changes: 58 additions & 13 deletions tests/workspace-checkout.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<?php\n// customer settings\n' > "$DRUPAL_SITE_DIR/settings.php"
printf 'web/sites/default/files\n' > "$fixture/.gitignore"
printf '<?php\n// customer settings\n' > "$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 '<?php\n$settings["wodby"] = TRUE;\n' > "$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 "<?php\ninclude '/var/www/conf/wodby.settings.php';\n" > "$DRUPAL_SITE_DIR/settings.php"
printf "<?php\ninclude '/var/www/conf/wodby.settings.php';\n" > "$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 '<?php\n// Drupal defaults\n' > "$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 '<?php\n' > "$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")" = '<?php'
rm "$settings"
printf '<?php\n' > "$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")" = '<?php'