From a96f6e35303e1b9e6aad8d5f5dbada2c54d0efc1 Mon Sep 17 00:00:00 2001 From: Chingis S Date: Wed, 23 Sep 2026 19:54:13 +0400 Subject: [PATCH 1/2] Protect tracked files during workspace preparation Preflight generated settings and uploads before initialization. Require explicit bootstrap configuration for tracked settings and test ignored, untracked generation and refusal without partial changes. --- Makefile | 2 ++ README.md | 24 ++++++++++++++++++++++ bin/init_drupal | 28 ++++++++++++++++++++++++++ tests/workspace-checkout.sh | 40 +++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 tests/workspace-checkout.sh diff --git a/Makefile b/Makefile index 9c0eea09..8958ff16 100644 --- a/Makefile +++ b/Makefile @@ -68,6 +68,8 @@ buildx-imagetools-create: test: bash tests/runtime-configuration.sh + bash tests/workspace-checkout.sh + docker run --rm --network none --entrypoint /docker-entrypoint.sh -e DRUPAL_VERSION=11 $(REPO):$(TAG) --configure-runtime docker run --rm --network none --user root \ -v "$(CURDIR)/tests/asset-permissions.sh:/tmp/asset-permissions.sh:ro" \ $(REPO):$(TAG) bash /tmp/asset-permissions.sh diff --git a/README.md b/README.md index 0004818c..1b1bb4e9 100644 --- a/README.md +++ b/README.md @@ -105,3 +105,27 @@ image. A version without a pin fails before the build starts. When adding a supported base version or variant, add its image index digest to `base-images.mk`. For a custom build, override `BASE_IMAGE` with a complete `repository:tag@sha256:...` reference. + +### 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`: + +```php +$wodbyConfig = (getenv('CONF_DIR') ?: '/var/www/conf') . '/wodby.settings.php'; +if (is_file($wodbyConfig)) { + include $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. diff --git a/bin/init_drupal b/bin/init_drupal index c90abf29..81023850 100755 --- a/bin/init_drupal +++ b/bin/init_drupal @@ -11,6 +11,34 @@ settings_php="${DRUPAL_SITE_DIR}/settings.php" default_settings_php="${DRUPAL_SITE_DIR}/default.settings.php" sites_php="${DRUPAL_ROOT}/sites/sites.php" +# A workspace checkout belongs to the developer. Preflight every generated path +# before making any changes; only ignored, untracked paths may be generated. +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 + if [[ -L "$1" ]]; then target=$(realpath "$1") || return 1; fi + case "$target" in "$root"/*) relative="${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" + fi + if [[ ! -L "${DRUPAL_SITE_DIR}/files" ]]; then + workspace_generated_path "${DRUPAL_SITE_DIR}/files" + fi +fi + mkdir -p "${DRUPAL_SITE_DIR}" chmod 755 "${DRUPAL_SITE_DIR}" || true diff --git a/tests/workspace-checkout.sh b/tests/workspace-checkout.sh new file mode 100644 index 00000000..67221078 --- /dev/null +++ b/tests/workspace-checkout.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Run without Drupal installed; real Git tracking rules guard all mutations. +set -euo pipefail +script="$(cd "$(dirname "$0")/.." && pwd)/bin/init_drupal" +fixture=$(mktemp -d) +trap 'rm -rf "$fixture"' 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='' +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" +git -C "$fixture" add .gitignore web/sites/default/settings.php +before=$(git -C "$fixture" diff --binary) +if bash "$script" > "$fixture/error" 2>&1; then echo 'Modified tracked settings' >&2; exit 1; fi +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" +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. +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" +bash "$script" +grep -q wodby.settings.php "$DRUPAL_SITE_DIR/settings.php" +# Tracked upload placeholders must never be removed, even if .gitignored. +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 +test -f "$DRUPAL_SITE_DIR/files/.gitignore" From 2e2f84945619f0b5c40d3d4009369aff3daf977e Mon Sep 17 00:00:00 2001 From: Chingis S Date: Wed, 23 Sep 2026 21:11:59 +0400 Subject: [PATCH 2/2] Use PHP r4 workspace-compatible base images Pin released PHP r4 image indexes for ordinary and development variants. Verify inherited workspace labels, repeatable runtime configuration, OPcache settings and preservation of developer SSH/Git configuration in image tests. --- .github/workflows/workflow.yml | 2 +- Makefile | 4 +++- base-images.mk | 13 +++++++++++++ tests/workspace-contract.sh | 9 +++++++++ tests/workspace-image-runtime.sh | 27 +++++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 tests/workspace-contract.sh create mode 100644 tests/workspace-image-runtime.sh diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index d6bd4131..44327aee 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -12,7 +12,7 @@ on: pull_request: env: - BASE_IMAGE_REVISION: r3 + BASE_IMAGE_REVISION: r4 jobs: build: diff --git a/Makefile b/Makefile index 8958ff16..e9d3e12a 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -include env_make # Accept legacy build arguments during the image revision transition. -BASE_IMAGE_REVISION ?= $(BASE_IMAGE_STABILITY_TAG) +BASE_IMAGE_REVISION ?= $(if $(BASE_IMAGE_STABILITY_TAG),$(BASE_IMAGE_STABILITY_TAG),r4) PHP_VER ?= 8.5 @@ -67,6 +67,8 @@ buildx-imagetools-create: .PHONY: buildx-imagetools-create test: + IMAGE=$(REPO):$(TAG) bash tests/workspace-contract.sh + docker run --rm --network none --entrypoint /bin/bash -e DRUPAL_VERSION=11 -v "$(CURDIR)/tests/workspace-image-runtime.sh:/tmp/workspace-image-runtime.sh:ro" $(REPO):$(TAG) /tmp/workspace-image-runtime.sh bash tests/runtime-configuration.sh bash tests/workspace-checkout.sh docker run --rm --network none --entrypoint /docker-entrypoint.sh -e DRUPAL_VERSION=11 $(REPO):$(TAG) --configure-runtime diff --git a/base-images.mk b/base-images.mk index ebce6a89..e7dc5af2 100644 --- a/base-images.mk +++ b/base-images.mk @@ -43,3 +43,16 @@ BASE_IMAGE_DIGEST_8.5-dev-macos-r3 := sha256:37efa836179df64abda61e401750fdd3d35 # Fail before building when a version or variant has no reviewed pin. BASE_IMAGE = $(BASE_IMAGE_REPOSITORY):$(BASE_IMAGE_TAG)@$(or $(BASE_IMAGE_DIGEST_$(BASE_IMAGE_TAG)),$(error No base image digest for $(BASE_IMAGE_REPOSITORY):$(BASE_IMAGE_TAG); update base-images.mk)) + +BASE_IMAGE_DIGEST_8.2-r4 := sha256:c374f68216ff78ce766e93e02fd3faa90cb5c9ae5db5b12a747533ddfae5b4e0 +BASE_IMAGE_DIGEST_8.2-dev-r4 := sha256:4c22a51a81c328e29f42a4fc67210f7762a411a9a0638624e80d342b05940a35 +BASE_IMAGE_DIGEST_8.2-dev-macos-r4 := sha256:bfdad65b9efc8a5baa3fd7f373c3edcccf1953d34f5ad088e66ccb171278d161 +BASE_IMAGE_DIGEST_8.3-r4 := sha256:20f41bfaecae7d7e29b76bb90d0eb4bbf582b1a21e35af2d4dcc81c15182c56a +BASE_IMAGE_DIGEST_8.3-dev-r4 := sha256:24dc62dbdaca5b7c31ae2ce244b50b537cbf7b10b68c3397edbe2b35bed0cfb8 +BASE_IMAGE_DIGEST_8.3-dev-macos-r4 := sha256:4bf12cf98108e2a8afc9049756041f45e6f1dabf2f34965c63272f2c75b802c9 +BASE_IMAGE_DIGEST_8.4-r4 := sha256:f3d719311664ba6e9d061a8667de3bddd9ccba1cd858a8f9896adb234d3be321 +BASE_IMAGE_DIGEST_8.4-dev-r4 := sha256:55041fd0bc5fbcfb857d6fd908c2a9c109b0a1f1a1bea5345f8c0c52978845e4 +BASE_IMAGE_DIGEST_8.4-dev-macos-r4 := sha256:6fcdb77452be58c1b834b6278feea00cc72a021783862bf9013d3ad7eaeee872 +BASE_IMAGE_DIGEST_8.5-r4 := sha256:9fa803b8e5def413753a9869c310f1e793d0d6da5fc91cb54ffb2be7b76edd94 +BASE_IMAGE_DIGEST_8.5-dev-r4 := sha256:9227547ca9d7ef6d7eb31f7bd06b2aba31c9605a0a984b0726d3c2f3e9bc92b3 +BASE_IMAGE_DIGEST_8.5-dev-macos-r4 := sha256:8fbe0b190d93e3f9fd7084ba1e713cbd6a8f9bec2675ac2dd78daf9a7a710534 diff --git a/tests/workspace-contract.sh b/tests/workspace-contract.sh new file mode 100644 index 00000000..2d01f422 --- /dev/null +++ b/tests/workspace-contract.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Ensure ordinary images do not advertise the development-only contract. +set -euo pipefail +label=$(docker image inspect --format '{{index .Config.Labels "com.wodby.workspace.contract"}}' "$IMAGE") +if docker image inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$IMAGE" | grep -Eq '^PHP_DEV=.+$'; then + test "$label" = 1 +else + test -z "$label" +fi diff --git a/tests/workspace-image-runtime.sh b/tests/workspace-image-runtime.sh new file mode 100644 index 00000000..bc43648b --- /dev/null +++ b/tests/workspace-image-runtime.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Run inside a fresh PHP container, with its normal entrypoint bypassed. +set -euo pipefail +mkdir -p "$HOME/.ssh" +printf '# custom SSH config\n' > "$HOME/.ssh/config" +printf '[user]\n\tname = Developer\n' > "$HOME/.gitconfig" +ssh_before=$(sha256sum "$HOME/.ssh/config") +git_before=$(sha256sum "$HOME/.gitconfig") +export PHP_CLI_MEMORY_LIMIT=321M +# Configuration must not execute a checkout action, even when one is configured. +export WODBY2_SERVICE_INIT_ACTION=nonexistent-workspace-test-action +/docker-entrypoint.sh --configure-runtime +test "$(php -r 'echo ini_get("memory_limit");')" = 321M +test "$(sha256sum "$HOME/.ssh/config")" = "$ssh_before" +test "$(sha256sum "$HOME/.gitconfig")" = "$git_before" +/docker-entrypoint.sh --configure-runtime +test "$(sha256sum "$HOME/.ssh/config")" = "$ssh_before" + +# Live code must be revalidated even when production disabled timestamp checks. +export WODBY_WORKSPACE=1 PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 PHP_OPCACHE_REVALIDATE_FREQ=60 +/docker-entrypoint.sh --configure-runtime +test "$(php -r 'echo ini_get("opcache.validate_timestamps");')" = 1 +test "$(php -r 'echo ini_get("opcache.revalidate_freq");')" = 0 +unset WODBY_WORKSPACE +/docker-entrypoint.sh --configure-runtime +test "$(php -r 'echo ini_get("opcache.validate_timestamps");')" = 0 +test "$(php -r 'echo ini_get("opcache.revalidate_freq");')" = 60