diff --git a/.github/workflows/phpunit-feature.yml b/.github/workflows/phpunit-feature.yml index 468385667..7ceeb6627 100644 --- a/.github/workflows/phpunit-feature.yml +++ b/.github/workflows/phpunit-feature.yml @@ -355,7 +355,7 @@ jobs: -w /app \ --entrypoint sh \ durable-workflow-server-corpus-validator \ - -lc 'set -eu; tar --no-same-owner -xf - -C /app; cp -a /app/workflow-package /workflow; php scripts/ci/prepare-release-workflow-composer-metadata.php; composer update durable-workflow/workflow --with-dependencies --minimal-changes --no-install --no-scripts --no-autoloader --no-interaction --no-progress --prefer-dist; composer install --no-interaction --no-progress --prefer-dist; git --version; vendor/bin/phpunit --version; git rev-parse --verify "$CORPUS_BASE_REF^{commit}"; php scripts/ci/check-worker-openapi-evolution.php "$CORPUS_BASE_REF"; vendor/bin/phpunit tests/Feature tests/Unit/NexusContractTest.php tests/Unit/CodecRegressionCorpusTest.php tests/Unit/PhpunitFeatureWorkflowContractTest.php tests/Unit/OpenApiDocumentEvolutionTest.php tests/Unit/NamespaceConformanceRunnerContractTest.php tests/Unit/NamespaceRuntimeContractTest.php tests/Unit/SchedulesConformanceRunnerContractTest.php tests/Unit/SchedulesRuntimeContractTest.php tests/Unit/ServerParitySdkDirectionResponseSummaryTest.php tests/Unit/ServerParitySdkQueryTransportTest.php tests/Unit/ServerParitySdkUpdateTransportTest.php tests/Unit/ServerParityLockPressureRetryTest.php tests/Unit/ServerParityManyUpdatesRetryTest.php --no-progress --colors=never; vendor/bin/phpunit tests/Unit/ReleaseImageContinuationTest.php tests/Unit/ReleaseImageImmutabilityGuardTest.php --no-progress --colors=never; vendor/bin/phpunit tests/Unit/ReleaseImagePublishWorkflowContractTest.php --filter test_php_security_audits_fail_before_release_publication --no-progress --colors=never; python3 scripts/ci/validate-regression-corpus.py --base-ref "$CORPUS_BASE_REF" --verify-counterfactual' + -lc 'set -eu; tar --no-same-owner -xf - -C /app; cp -a /app/workflow-package /workflow; php scripts/ci/prepare-release-workflow-composer-metadata.php; composer update durable-workflow/workflow --with-dependencies --minimal-changes --no-install --no-scripts --no-autoloader --no-interaction --no-progress --prefer-dist; composer install --no-interaction --no-progress --prefer-dist; git --version; vendor/bin/phpunit --version; git rev-parse --verify "$CORPUS_BASE_REF^{commit}"; php scripts/ci/check-worker-openapi-evolution.php "$CORPUS_BASE_REF"; vendor/bin/phpunit tests/Feature tests/Unit/NexusContractTest.php tests/Unit/NativePublishedComposeContractTest.php tests/Unit/PublishedServerAutoSelectionTest.php tests/Unit/CodecRegressionCorpusTest.php tests/Unit/PhpunitFeatureWorkflowContractTest.php tests/Unit/OpenApiDocumentEvolutionTest.php tests/Unit/NamespaceConformanceRunnerContractTest.php tests/Unit/NamespaceRuntimeContractTest.php tests/Unit/SchedulesConformanceRunnerContractTest.php tests/Unit/SchedulesRuntimeContractTest.php tests/Unit/ServerParitySdkDirectionResponseSummaryTest.php tests/Unit/ServerParitySdkQueryTransportTest.php tests/Unit/ServerParitySdkUpdateTransportTest.php tests/Unit/ServerParityLockPressureRetryTest.php tests/Unit/ServerParityManyUpdatesRetryTest.php --no-progress --colors=never; vendor/bin/phpunit tests/Unit/ReleaseImageContinuationTest.php tests/Unit/ReleaseImageImmutabilityGuardTest.php --no-progress --colors=never; vendor/bin/phpunit tests/Unit/ReleaseImagePublishWorkflowContractTest.php --filter test_php_security_audits_fail_before_release_publication --no-progress --colors=never; python3 scripts/ci/validate-regression-corpus.py --base-ref "$CORPUS_BASE_REF" --verify-counterfactual' - name: Select production image qualification for changed build inputs id: image_inputs diff --git a/.github/workflows/published-compose.yml b/.github/workflows/published-compose.yml index 16abbeed2..4fc4c2b4c 100644 --- a/.github/workflows/published-compose.yml +++ b/.github/workflows/published-compose.yml @@ -1,14 +1,15 @@ name: Published Compose Smoke on: + pull_request: + paths: + - docker-compose.published.yml + - scripts/smoke-published-compose.sh + - .github/workflows/published-compose.yml workflow_dispatch: inputs: - server_tag: - description: "Published durableworkflow/server tag to test when DW_SERVER_IMAGE is not set" - required: false - default: "0.2" server_image: - description: "Full image reference or digest override, for example ghcr.io/durable-workflow/server:" + description: "Native image reference or digest to test instead of docker-compose.published.yml's default" required: false default: "" @@ -52,10 +53,8 @@ jobs: - name: Set Compose image selector env: - INPUT_SERVER_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.server_tag || '0.2' }} INPUT_SERVER_IMAGE: ${{ github.event_name == 'workflow_dispatch' && inputs.server_image || '' }} run: | - echo "DW_SERVER_TAG=${INPUT_SERVER_TAG:-0.2}" >> "$GITHUB_ENV" if [ -n "$INPUT_SERVER_IMAGE" ]; then echo "DW_SERVER_IMAGE=$INPUT_SERVER_IMAGE" >> "$GITHUB_ENV" fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8fc6dc8d2..305ba71b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -313,6 +313,7 @@ jobs: DW_SERVER_IMAGE: ${{ env.DOCKERHUB_IMAGE }}@${{ steps.exact.outputs.image_digest }} DW_SERVER_PLATFORM: linux/amd64 DW_PUBLISHED_COMPOSE_PROFILE: local + DW_PUBLISHED_COMPOSE_FILE: docker-compose.php.yml DW_PUBLISHED_COMPOSE_PROJECT: release-compose-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} SERVER_PORT: "0" DW_AUTH_TOKEN: published-compose-token @@ -364,7 +365,7 @@ jobs: DW_PUBLISHED_COMPOSE_PROJECT: release-compose-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} run: >- docker compose -p "$DW_PUBLISHED_COMPOSE_PROJECT" - -f release-source/docker-compose.published.yml down -v --remove-orphans + -f release-source/docker-compose.php.yml down -v --remove-orphans >/dev/null 2>&1 || true - name: Verify published protocol catalog convergence diff --git a/.github/workflows/replay-query-concurrent-http.yml b/.github/workflows/replay-query-concurrent-http.yml index fa80703e3..421e89f3a 100644 --- a/.github/workflows/replay-query-concurrent-http.yml +++ b/.github/workflows/replay-query-concurrent-http.yml @@ -14,7 +14,7 @@ on: - 'composer.lock' - 'config/dw-bounded-growth.php' - 'docker/**' - - 'docker-compose.published.yml' + - 'docker-compose.php.yml' - 'Dockerfile' - 'scripts/regression/apache-module-preflight.sh' - 'scripts/regression/replay-query-concurrent-http.sh' @@ -30,7 +30,7 @@ on: - 'composer.lock' - 'config/dw-bounded-growth.php' - 'docker/**' - - 'docker-compose.published.yml' + - 'docker-compose.php.yml' - 'Dockerfile' - 'scripts/regression/apache-module-preflight.sh' - 'scripts/regression/replay-query-concurrent-http.sh' @@ -93,5 +93,5 @@ jobs: run: | docker rm -f "${DW_REPLAY_QUERY_COMPOSE_PROJECT}-nonroot-http" >/dev/null 2>&1 || true docker compose -p "$DW_REPLAY_QUERY_COMPOSE_PROJECT" \ - -f docker-compose.published.yml down -v --remove-orphans \ + -f docker-compose.php.yml down -v --remove-orphans \ >/dev/null 2>&1 || true diff --git a/Dockerfile.dockerignore b/Dockerfile.dockerignore index e9e7fd290..1ef6aa9a8 100644 --- a/Dockerfile.dockerignore +++ b/Dockerfile.dockerignore @@ -14,6 +14,7 @@ !composer.lock !docker-compose.yml !docker-compose.published.yml +!docker-compose.php.yml !docker-compose.small-cluster.yml !docker-compose.dedicated-matching.yml !docker-compose.failover-rehearsal.yml diff --git a/README.md b/README.md index e2823d8fb..c950b6d4b 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,13 @@ For the 21 October 2026 critical PHP update, follow the [PHP runtime security notice](docs/security/php-runtime-2026-10-21.md) and verify the patched runtime in every deployed role. -The published Compose stack starts Server with MySQL, Redis, a queue worker, -and the scheduler. It bootstraps the database and the default namespace before -accepting traffic. +The published Compose stack starts the native Server with MySQL. Native is a +single process: it creates its schema and the default namespace at startup and +runs its own maintenance, so there is no Redis, queue worker or scheduler. The +PHP 2.x stack remains available as +[`docker-compose.php.yml`](docker-compose.php.yml) for installations that pin +it; see the [native takeover runbook](docs/rust-server-upgrade-takeover.md) to +move an existing PHP database to native. ```bash curl -fsSLO https://raw.githubusercontent.com/durable-workflow/server/main/docker-compose.published.yml @@ -41,9 +45,13 @@ curl -H "Authorization: Bearer $DW_AUTH_TOKEN" \ http://localhost:8080/api/cluster/info ``` -This development configuration uses a single compatibility token. Production -deployments should use role-scoped worker, operator, and administrator -credentials and pin the Server image by version or digest. See the +The file requires `DW_AUTH_TOKEN` and publishes the API on `127.0.0.1` only; +set `SERVER_BIND_ADDRESS` to a protected private interface for remote access and +terminate HTTPS at a reverse proxy. This development configuration uses the +shared `dev-token`. Production +deployments must set a strong `DW_AUTH_TOKEN` (native accepts it for every +role), should add role-scoped worker, operator, and administrator tokens, and +should pin the Server image by version or digest. See the [self-hosting reference](docs/server-reference.md) for SQLite, production Compose, authentication, backup, upgrade, API, and configuration guidance. For a tested recovery sequence, see the diff --git a/docker-compose.dedicated-matching.yml b/docker-compose.dedicated-matching.yml index 6fb11e426..363903611 100644 --- a/docker-compose.dedicated-matching.yml +++ b/docker-compose.dedicated-matching.yml @@ -1,14 +1,14 @@ name: durable-workflow-server # Compose override that enables the dedicated matching-role daemon shape on -# top of `docker-compose.published.yml`. The default shape runs the in-worker +# top of `docker-compose.php.yml`. The default shape runs the in-worker # matching-role wake on every Looping event and an unscoped scheduler repair # pass for remote SDK queues. Larger deployments can opt execution-only workers # out of the wake and let a single dedicated daemon own the broad sweep. # # Usage: # docker compose \ -# -f docker-compose.published.yml \ +# -f docker-compose.php.yml \ # -f docker-compose.dedicated-matching.yml \ # up # diff --git a/docker-compose.php.yml b/docker-compose.php.yml new file mode 100644 index 000000000..8a7601a1d --- /dev/null +++ b/docker-compose.php.yml @@ -0,0 +1,161 @@ +name: durable-workflow-server + +# The PHP 2.x Durable Workflow Server: HTTP server, queue worker, scheduler, +# bootstrap job, MySQL and Redis. From 3.0.0 the published Compose file, +# docker-compose.published.yml, runs the native server; this file keeps the +# PHP 2.x stack for installations that pin it, and for the published-artifact +# conformance runners that qualify PHP releases. +# +# Generated by scripts/ci/sync-source-release.mjs. Do not edit the fallback. +x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.11.8}} + +x-server-environment: &server-environment + APP_NAME: "Durable Workflow Server" + APP_ENV: ${APP_ENV:-local} + DW_SERVER_KEY: ${DW_SERVER_KEY:-} + APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.11.8}} + APP_DEBUG: ${APP_DEBUG:-false} + LOG_CHANNEL: ${LOG_CHANNEL:-stderr} + LOG_LEVEL: ${LOG_LEVEL:-info} + DB_CONNECTION: mysql + DB_HOST: mysql + DB_PORT: 3306 + DB_DATABASE: ${DB_DATABASE:-durable_workflow} + DB_USERNAME: ${DB_USERNAME:-workflow} + DB_PASSWORD: ${DB_PASSWORD:-workflow} + REDIS_HOST: redis + QUEUE_CONNECTION: redis + CACHE_STORE: redis + DW_TASK_DISPATCH_MODE: ${DW_TASK_DISPATCH_MODE:-poll} + DW_AUTH_DRIVER: ${DW_AUTH_DRIVER:-token} + DW_AUTH_TOKEN: ${DW_AUTH_TOKEN:-dev-token} + DW_PRINCIPAL_TOKENS: ${DW_PRINCIPAL_TOKENS:-${WORKFLOW_SERVER_PRINCIPAL_TOKENS:-}} + DW_RUNTIME_CREDENTIALS_ENABLED: ${DW_RUNTIME_CREDENTIALS_ENABLED:-${WORKFLOW_SERVER_RUNTIME_CREDENTIALS_ENABLED:-false}} + DW_WORKER_TOKEN: ${DW_WORKER_TOKEN:-} + DW_OPERATOR_TOKEN: ${DW_OPERATOR_TOKEN:-} + DW_ADMIN_TOKEN: ${DW_ADMIN_TOKEN:-} + DW_AUTH_BACKWARD_COMPATIBLE: ${DW_AUTH_BACKWARD_COMPATIBLE:-true} + DW_EXTERNAL_PAYLOAD_S3_ACCESS_KEY_ID: ${DW_EXTERNAL_PAYLOAD_S3_ACCESS_KEY_ID:-} + DW_EXTERNAL_PAYLOAD_S3_SECRET_ACCESS_KEY: ${DW_EXTERNAL_PAYLOAD_S3_SECRET_ACCESS_KEY:-} + DW_EXTERNAL_PAYLOAD_S3_SESSION_TOKEN: ${DW_EXTERNAL_PAYLOAD_S3_SESSION_TOKEN:-} + DW_EXTERNAL_PAYLOAD_S3_REGION: ${DW_EXTERNAL_PAYLOAD_S3_REGION:-us-east-1} + DW_EXTERNAL_PAYLOAD_S3_BUCKET: ${DW_EXTERNAL_PAYLOAD_S3_BUCKET:-} + DW_EXTERNAL_PAYLOAD_S3_ENDPOINT: ${DW_EXTERNAL_PAYLOAD_S3_ENDPOINT:-} + DW_EXTERNAL_PAYLOAD_S3_USE_PATH_STYLE_ENDPOINT: ${DW_EXTERNAL_PAYLOAD_S3_USE_PATH_STYLE_ENDPOINT:-false} + +services: + bootstrap: + image: *server-image + platform: ${DW_SERVER_PLATFORM:-} + command: ["server-bootstrap"] + healthcheck: + disable: true + environment: + <<: *server-environment + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + + server: + image: *server-image + platform: ${DW_SERVER_PLATFORM:-} + ports: + - "${SERVER_PORT:-8080}:8080" + environment: + <<: *server-environment + DW_SERVER_TOPOLOGY_SHAPE: standalone_server + DW_SERVER_PROCESS_CLASS: server_http_node + depends_on: + bootstrap: + condition: service_completed_successfully + mysql: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/ready"] + interval: 10s + timeout: 5s + retries: 3 + + worker: + image: *server-image + platform: ${DW_SERVER_PLATFORM:-} + command: php artisan queue:work --sleep=1 --tries=3 --max-time=3600 + restart: unless-stopped + healthcheck: + test: ["CMD", "server-process-healthcheck", "worker"] + interval: 5s + timeout: 3s + retries: 3 + start_period: 5s + environment: + <<: *server-environment + DW_SERVER_TOPOLOGY_SHAPE: standalone_server + DW_SERVER_PROCESS_CLASS: worker_node + depends_on: + bootstrap: + condition: service_completed_successfully + server: + condition: service_healthy + mysql: + condition: service_healthy + redis: + condition: service_healthy + + scheduler: + image: *server-image + platform: ${DW_SERVER_PLATFORM:-} + init: true + # Remote SDK queues can differ from the Laravel queue worker's Looping scope. + # The dedicated matching override disables this unscoped scheduler pass. + command: >- + sh -c 'while true; do php artisan schedule:evaluate --limit=100 --json; if [ "$$DW_V2_MATCHING_ROLE_QUEUE_WAKE" != "false" ] && [ "$$DW_V2_MATCHING_ROLE_QUEUE_WAKE" != "0" ]; then php artisan workflow:v2:repair-pass --json; fi; php artisan activity:timeout-enforce --limit=100; php artisan history:prune --limit=100; sleep 10; done' + healthcheck: + test: ["CMD", "server-process-healthcheck", "scheduler"] + interval: 5s + timeout: 3s + retries: 3 + start_period: 5s + environment: + <<: *server-environment + DW_SERVER_TOPOLOGY_SHAPE: standalone_server + DW_SERVER_PROCESS_CLASS: scheduler_node + depends_on: + bootstrap: + condition: service_completed_successfully + mysql: + condition: service_healthy + redis: + condition: service_healthy + + mysql: + image: mysql:${MYSQL_VERSION:-8.0} + environment: + MYSQL_DATABASE: ${DB_DATABASE:-durable_workflow} + MYSQL_USER: ${DB_USERNAME:-workflow} + MYSQL_PASSWORD: ${DB_PASSWORD:-workflow} + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root} + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 5s + timeout: 3s + retries: 20 + + redis: + image: redis:${REDIS_VERSION:-7-alpine} + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + +volumes: + mysql_data: + redis_data: diff --git a/docker-compose.published.yml b/docker-compose.published.yml index a0f9f6e2a..ff97200d3 100644 --- a/docker-compose.published.yml +++ b/docker-compose.published.yml @@ -1,129 +1,73 @@ name: durable-workflow-server -# Generated by scripts/ci/sync-source-release.mjs. Do not edit the fallback. -x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.11.8}} - -x-server-environment: &server-environment - APP_NAME: "Durable Workflow Server" - APP_ENV: ${APP_ENV:-local} - DW_SERVER_KEY: ${DW_SERVER_KEY:-} - APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.11.8}} - APP_DEBUG: ${APP_DEBUG:-false} - LOG_CHANNEL: ${LOG_CHANNEL:-stderr} - LOG_LEVEL: ${LOG_LEVEL:-info} - DB_CONNECTION: mysql - DB_HOST: mysql - DB_PORT: 3306 - DB_DATABASE: ${DB_DATABASE:-durable_workflow} - DB_USERNAME: ${DB_USERNAME:-workflow} - DB_PASSWORD: ${DB_PASSWORD:-workflow} - REDIS_HOST: redis - QUEUE_CONNECTION: redis - CACHE_STORE: redis - DW_TASK_DISPATCH_MODE: ${DW_TASK_DISPATCH_MODE:-poll} - DW_AUTH_DRIVER: ${DW_AUTH_DRIVER:-token} - DW_AUTH_TOKEN: ${DW_AUTH_TOKEN:-dev-token} - DW_PRINCIPAL_TOKENS: ${DW_PRINCIPAL_TOKENS:-${WORKFLOW_SERVER_PRINCIPAL_TOKENS:-}} - DW_RUNTIME_CREDENTIALS_ENABLED: ${DW_RUNTIME_CREDENTIALS_ENABLED:-${WORKFLOW_SERVER_RUNTIME_CREDENTIALS_ENABLED:-false}} - DW_WORKER_TOKEN: ${DW_WORKER_TOKEN:-} - DW_OPERATOR_TOKEN: ${DW_OPERATOR_TOKEN:-} - DW_ADMIN_TOKEN: ${DW_ADMIN_TOKEN:-} - DW_AUTH_BACKWARD_COMPATIBLE: ${DW_AUTH_BACKWARD_COMPATIBLE:-true} - DW_EXTERNAL_PAYLOAD_S3_ACCESS_KEY_ID: ${DW_EXTERNAL_PAYLOAD_S3_ACCESS_KEY_ID:-} - DW_EXTERNAL_PAYLOAD_S3_SECRET_ACCESS_KEY: ${DW_EXTERNAL_PAYLOAD_S3_SECRET_ACCESS_KEY:-} - DW_EXTERNAL_PAYLOAD_S3_SESSION_TOKEN: ${DW_EXTERNAL_PAYLOAD_S3_SESSION_TOKEN:-} - DW_EXTERNAL_PAYLOAD_S3_REGION: ${DW_EXTERNAL_PAYLOAD_S3_REGION:-us-east-1} - DW_EXTERNAL_PAYLOAD_S3_BUCKET: ${DW_EXTERNAL_PAYLOAD_S3_BUCKET:-} - DW_EXTERNAL_PAYLOAD_S3_ENDPOINT: ${DW_EXTERNAL_PAYLOAD_S3_ENDPOINT:-} - DW_EXTERNAL_PAYLOAD_S3_USE_PATH_STYLE_ENDPOINT: ${DW_EXTERNAL_PAYLOAD_S3_USE_PATH_STYLE_ENDPOINT:-false} +# The published Durable Workflow Server: the native (Rust) server and MySQL. +# +# curl -fsSLO https://raw.githubusercontent.com/durable-workflow/server/main/docker-compose.published.yml +# docker compose -f docker-compose.published.yml up -d --wait +# +# Native is one process. It creates and checks its own schema at startup and +# runs its maintenance (timers, repair, retention) in the same process, so +# there is no bootstrap job, queue worker, scheduler or Redis. The PHP 2.x +# server's Compose file is docker-compose.php.yml; to keep running PHP, use +# that file (or pin a 2.x release of this one). +# +# Until the first final native release (3.0.0) is published, the default image +# is the current native release candidate by digest. The 3.0.0 release +# replaces it with durableworkflow/server:3.0.0. Set DW_SERVER_IMAGE to run +# any other native image, by tag or digest. +x-server-image: &server-image ${DW_SERVER_IMAGE:-ghcr.io/durable-workflow/server-native@sha256:f54e89d775dbf7c6074f37836c6bb0b25f94869d23711b0706e9547c3edecb29} services: - bootstrap: - image: *server-image - platform: ${DW_SERVER_PLATFORM:-} - command: ["server-bootstrap"] - healthcheck: - disable: true - environment: - <<: *server-environment - depends_on: - mysql: - condition: service_healthy - redis: - condition: service_healthy - server: image: *server-image platform: ${DW_SERVER_PLATFORM:-} + restart: unless-stopped + # Loopback only by default. For remote access set SERVER_BIND_ADDRESS to a + # protected private interface and terminate HTTPS at a reverse proxy. ports: - - "${SERVER_PORT:-8080}:8080" + - "${SERVER_BIND_ADDRESS:-127.0.0.1}:${SERVER_PORT:-8080}:8080" environment: - <<: *server-environment - DW_SERVER_TOPOLOGY_SHAPE: standalone_server - DW_SERVER_PROCESS_CLASS: server_http_node + # Release candidates refuse to start without this opt-in. The final + # 3.0.0 image does not need it. + DW_RUST_EXPERIMENTAL: "1" + DW_SERVER_KEY: ${DW_SERVER_KEY:-} + DB_CONNECTION: mysql + DB_HOST: mysql + DB_PORT: 3306 + DB_DATABASE: ${DB_DATABASE:-durable_workflow} + DB_USERNAME: ${DB_USERNAME:-workflow} + DB_PASSWORD: ${DB_PASSWORD:-workflow} + # Token auth. There is no default token: Compose refuses to start until + # DW_AUTH_TOKEN is set. Native requires this shared token and accepts it + # for every role (it refuses DW_AUTH_BACKWARD_COMPATIBLE=false), so use a + # strong random value outside local testing. The role tokens add + # narrower worker, operator and admin tokens. + DW_AUTH_DRIVER: ${DW_AUTH_DRIVER:-token} + DW_AUTH_TOKEN: ${DW_AUTH_TOKEN:?Set DW_AUTH_TOKEN, for example export DW_AUTH_TOKEN=dev-token for local testing} + DW_RUNTIME_CREDENTIALS_ENABLED: ${DW_RUNTIME_CREDENTIALS_ENABLED:-false} + DW_WORKER_TOKEN: ${DW_WORKER_TOKEN:-} + DW_OPERATOR_TOKEN: ${DW_OPERATOR_TOKEN:-} + DW_ADMIN_TOKEN: ${DW_ADMIN_TOKEN:-} + volumes: + - server_storage:/app/storage depends_on: - bootstrap: - condition: service_completed_successfully mysql: condition: service_healthy - redis: - condition: service_healthy + # The image has no curl; bash's /dev/tcp reads /api/ready. healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/api/ready"] + test: + - CMD + - bash + - -c + - >- + exec 3<>/dev/tcp/127.0.0.1/8080 && + printf 'GET /api/ready HTTP/1.0\r\nHost: localhost\r\n\r\n' >&3 && + head -n 1 <&3 | grep -q ' 200 ' interval: 10s timeout: 5s retries: 3 - - worker: - image: *server-image - platform: ${DW_SERVER_PLATFORM:-} - command: php artisan queue:work --sleep=1 --tries=3 --max-time=3600 - restart: unless-stopped - healthcheck: - test: ["CMD", "server-process-healthcheck", "worker"] - interval: 5s - timeout: 3s - retries: 3 - start_period: 5s - environment: - <<: *server-environment - DW_SERVER_TOPOLOGY_SHAPE: standalone_server - DW_SERVER_PROCESS_CLASS: worker_node - depends_on: - bootstrap: - condition: service_completed_successfully - server: - condition: service_healthy - mysql: - condition: service_healthy - redis: - condition: service_healthy - - scheduler: - image: *server-image - platform: ${DW_SERVER_PLATFORM:-} - init: true - # Remote SDK queues can differ from the Laravel queue worker's Looping scope. - # The dedicated matching override disables this unscoped scheduler pass. - command: >- - sh -c 'while true; do php artisan schedule:evaluate --limit=100 --json; if [ "$$DW_V2_MATCHING_ROLE_QUEUE_WAKE" != "false" ] && [ "$$DW_V2_MATCHING_ROLE_QUEUE_WAKE" != "0" ]; then php artisan workflow:v2:repair-pass --json; fi; php artisan activity:timeout-enforce --limit=100; php artisan history:prune --limit=100; sleep 10; done' - healthcheck: - test: ["CMD", "server-process-healthcheck", "scheduler"] - interval: 5s - timeout: 3s - retries: 3 - start_period: 5s - environment: - <<: *server-environment - DW_SERVER_TOPOLOGY_SHAPE: standalone_server - DW_SERVER_PROCESS_CLASS: scheduler_node - depends_on: - bootstrap: - condition: service_completed_successfully - mysql: - condition: service_healthy - redis: - condition: service_healthy + start_period: 20s + stop_grace_period: 30s mysql: image: mysql:${MYSQL_VERSION:-8.0} @@ -140,16 +84,6 @@ services: timeout: 3s retries: 20 - redis: - image: redis:${REDIS_VERSION:-7-alpine} - volumes: - - redis_data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 3s - retries: 5 - volumes: mysql_data: - redis_data: + server_storage: diff --git a/docs/rust-server-port.md b/docs/rust-server-port.md index cf046ccae..8ed234686 100644 --- a/docs/rust-server-port.md +++ b/docs/rust-server-port.md @@ -888,7 +888,7 @@ does not establish full parity, current-head qualification or release readiness. | Local activities, cancellation scopes, worker sessions, streams, service catalog/Nexus, bridge adapters, standalone activities, debugging and repair | `routes/api.php`, `docs/contracts/`, worker/control-plane specifications, corresponding Feature tests and Nexus runner | Scoped native repair and diagnostics exist. Local-activity execution, cancellation scopes, sessions, streams, Nexus/catalog, bridge and standalone-activity compatibility remain required work. No unpublished package telemetry or stored-job contract is adopted by these slices. | | Actual PHP/Python/Rust directions and existing CLI/Waterline/Sample App | Organization [conformance runbook](https://github.com/durable-workflow/.github/blob/main/conformance/README.md) and its SDK coverage inventory; Sample App activity, child, timer, saga, update, namespace and search experiments | Published cohorts cover workflow/activity (#401), parent/child (#408), timer authors (#411), signals (#415), queries (#422), successful/failed updates (#424/#427), named namespaces (#429), selected-run signals (#434) and subsequent corrected marker/range and authority slices. Each retains its exact source/tuple and failures. CLI reads have bounded coverage. Remaining SDK families, broader CLI, native Waterline/Sample App and browser acceptance remain open; published PHP consumers do not qualify native. | | SQL schema and stored representations | `database/migrations/`, Workflow package `src/migrations/`, model casts, `Workflow\\Serializers\\Serializer`, queue job payloads, exported history and credential digests | [Representation checkpoints](rust-server-storage-audit.md) recorded; complete mapping and executable migration tests pending | -| Images, architecture, bootstrap, configuration, readiness, metrics and graceful shutdown | `Dockerfile`, `docker/`, `config/`, `docker-compose*.yml`, `k8s/helm/`, `docs/server-reference.md`, small-cluster/multi-region validation docs | Published PHP remains the default. Native bootstrap, health/readiness, configuration and shutdown foundations exist. Qualified native publication, both architectures, full configuration/metrics compatibility, Compose/Helm consumers and operational cutover remain open. | +| Images, architecture, bootstrap, configuration, readiness, metrics and graceful shutdown | `Dockerfile`, `docker/`, `config/`, `docker-compose*.yml`, `k8s/helm/`, `docs/server-reference.md`, small-cluster/multi-region validation docs | From November 1 (operator decision) the published Compose file, `docker-compose.published.yml`, runs the native server with MySQL only: native creates its schema, runs its maintenance loop and needs no bootstrap job, queue worker, scheduler or Redis. Its default image is the current native release candidate by digest until 3.0.0 replaces it. The PHP 2.x stack moved to `docker-compose.php.yml`, which PHP releases still synchronize and the PHP published-artifact runners use. Native refuses `DW_AUTH_BACKWARD_COMPATIBLE=false` and an empty `DW_AUTH_TOKEN`, so role-only authentication is not available: the shared token is required and accepted for every role. The Helm chart switch, full configuration and metrics compatibility, and operational cutover remain open. | | Backup and in-place upgrade | `docs/self-hosted-backup-and-restore.md`, [PHP ownership/refusal controls](php-database-ownership.md), external payload backup holds; last PHP image writes, all PHP roles stop, Rust takes over same DB sequentially | Ownership/refusal controls and isolated migration preparation exist. Backup-first native takeover with pending work, interrupted migration, external payload preservation and PHP refusal still need complete qualification on SQLite, MariaDB/MySQL and PostgreSQL. No mixed fleet or PHP rollback is permitted. | | Throughput, memory, CPU, latency, backlog/drain, idle mixed long polls | `benchmarks/capacity/v1/` and `scripts/benchmark/`; repeated runs and drift checks using frozen artifacts | The [diagnostic PHP reference](../benchmarks/server-port/v1/php-observations-2026-10-09.md) and scoped comparison observations are recorded. Complete matched throughput/memory/idle-poll proof, repeated measurements, drift checks and CPU-load deadline health remain release gates. Source checks and functional comparisons make no performance claim. | diff --git a/docs/self-hosted-backup-and-restore.md b/docs/self-hosted-backup-and-restore.md index b0c981cd6..3b6172a00 100644 --- a/docs/self-hosted-backup-and-restore.md +++ b/docs/self-hosted-backup-and-restore.md @@ -1,5 +1,8 @@ # Back up and restore a self-hosted Server +This procedure applies to the PHP 2.x Compose stack, `docker-compose.php.yml`. +The published Compose file runs the native server from 3.0.0. + This procedure applies to the published single-node Compose stack with MySQL, Redis, and a shared S3-compatible external-payload store. A backup is a recovery point, not a live migration. It contains operations committed before the MySQL @@ -36,7 +39,7 @@ external payloads. mkdir "$BACKUP_DIR" printf '%s\n' "$BACKUP_ID" > "$BACKUP_DIR/hold-owner" docker compose --env-file durable-workflow.prod.env \ - -f docker-compose.published.yml exec -T server \ + -f docker-compose.php.yml exec -T server \ php artisan external-payloads:backup-hold acquire \ --owner="$BACKUP_ID" --ttl=1800 ``` @@ -45,7 +48,7 @@ external payloads. ```bash docker compose --env-file durable-workflow.prod.env \ - -f docker-compose.published.yml exec -T mysql \ + -f docker-compose.php.yml exec -T mysql \ sh -lc 'mysqldump --single-transaction --quick --no-tablespaces -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' \ > "$BACKUP_DIR/database.sql" 2> "$BACKUP_DIR/database.stderr" ``` @@ -68,10 +71,10 @@ external payloads. ```bash docker compose --env-file durable-workflow.prod.env \ - -f docker-compose.published.yml exec -T server \ + -f docker-compose.php.yml exec -T server \ php artisan external-payloads:backup-hold status --owner="$BACKUP_ID" docker compose --env-file durable-workflow.prod.env \ - -f docker-compose.published.yml exec -T server \ + -f docker-compose.php.yml exec -T server \ php artisan external-payloads:backup-hold release --owner="$BACKUP_ID" ``` @@ -93,9 +96,9 @@ restoring it into an isolated stack. ```bash docker compose --env-file restored.env \ - -f docker-compose.published.yml up -d mysql redis + -f docker-compose.php.yml up -d mysql redis docker compose --env-file restored.env \ - -f docker-compose.published.yml exec -T mysql \ + -f docker-compose.php.yml exec -T mysql \ sh -lc 'mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' \ < "$BACKUP_DIR/database.sql" ``` @@ -111,9 +114,9 @@ restoring it into an isolated stack. ```bash docker compose --env-file restored.env \ - -f docker-compose.published.yml up -d --wait + -f docker-compose.php.yml up -d --wait docker compose --env-file restored.env \ - -f docker-compose.published.yml exec -T server \ + -f docker-compose.php.yml exec -T server \ php artisan external-payloads:backup-hold release --owner="$BACKUP_ID" ``` @@ -132,7 +135,7 @@ restoring it into an isolated stack. ```bash docker compose --env-file restored.env \ - -f docker-compose.published.yml exec -T server \ + -f docker-compose.php.yml exec -T server \ php artisan workflow:v2:repair-pass --json ``` diff --git a/docs/server-reference.md b/docs/server-reference.md index 2fd08cb71..424fa6191 100644 --- a/docs/server-reference.md +++ b/docs/server-reference.md @@ -89,16 +89,16 @@ which also requires writable Laravel and Apache runtime paths. ### Official Image + Compose -Use this path when you want a source-free multi-container stack backed by MySQL -and Redis. The same Compose file supports local development and single-node +Use this path when you want a source-free stack: the native server backed by +MySQL. The same Compose file supports local development and single-node production; the difference is the environment you provide and the operational care around persistence, backups, and upgrades. Image selection: -- The checked-in Compose file defaults to the exact Server image from its - stable release metadata. -- `DW_SERVER_TAG=` selects an exact Docker Hub image. +- The checked-in Compose file defaults to the current native image: the + release candidate by digest until 3.0.0 is published, then the exact + release. - `DW_SERVER_IMAGE=ghcr.io/durable-workflow/server:` selects the same release line from GitHub Container Registry. - `DW_SERVER_IMAGE=durableworkflow/server@sha256:...` pins an exact image @@ -107,8 +107,12 @@ Image selection: #### Local Development Compose This recipe is for one developer machine or internal non-production testing. It -uses the default MySQL/Redis volumes, exposes only the API port, and allows the -single `DW_AUTH_TOKEN` compatibility token for quick verification. +runs the native server with MySQL in named volumes, publishes only the API port +and only on `127.0.0.1`, and uses the shared `DW_AUTH_TOKEN` for quick +verification. Compose refuses to start until `DW_AUTH_TOKEN` is set. The PHP 2.x stack +(server, queue worker, scheduler, bootstrap job and Redis) is +`docker-compose.php.yml`; the PHP-specific procedures below that run `artisan` +commands apply to it. ```bash curl -fsSLO https://raw.githubusercontent.com/durable-workflow/server/main/docker-compose.published.yml @@ -137,19 +141,17 @@ curl -X POST http://localhost:8080/api/worker/register \ #### Single-Node Production Compose This recipe is for a small self-hosted deployment on one Docker host. It keeps -MySQL and Redis in named volumes, exposes only the API port, and expects -role-scoped credentials plus an exact image tag or digest. The Compose file -does not persist server-local payload files. For external payloads, configure -[shared S3-compatible storage](contracts/external-payload-storage.md#self-hosted-backing-storage) -reachable by every Server process. +MySQL in a named volume, persists the native server's local payload storage in +`server_storage`, exposes only the API port, and expects a strong shared token, +role-scoped credentials and an exact image tag or digest. Create a production env file outside source control: ```env DW_SERVER_IMAGE=durableworkflow/server: SERVER_PORT=8080 -APP_ENV=production -APP_DEBUG=false +# Loopback by default. Use a protected private interface for the reverse proxy. +SERVER_BIND_ADDRESS=127.0.0.1 DB_DATABASE=durable_workflow DB_USERNAME=workflow @@ -157,7 +159,8 @@ DB_PASSWORD=replace-with-random-password DB_ROOT_PASSWORD=replace-with-random-root-password DW_AUTH_DRIVER=token -DW_AUTH_BACKWARD_COMPATIBLE=false +# Native requires a shared token and accepts it for every role; keep it secret. +DW_AUTH_TOKEN=replace-with-random-shared-token DW_WORKER_TOKEN=replace-with-worker-token DW_OPERATOR_TOKEN=replace-with-operator-token DW_ADMIN_TOKEN=replace-with-admin-token @@ -180,18 +183,18 @@ Register SDK workers with `DW_WORKER_TOKEN` and send operator traffic with `/api/worker/register` for diagnostic worker registration, while heartbeats, task polling, task completion, and orderly self-deregistration remain worker-token endpoints. Put TLS, request logging, and public routing in a -reverse proxy in front of the API container; do not expose the MySQL or Redis -services. +reverse proxy in front of the API container; do not expose the MySQL +service. Persistence and backups: - `mysql_data` is the durable workflow state. Back it up before every image upgrade and on a regular schedule. -- `redis_data` contains queue/cache state. Preserve it for graceful restarts; - MySQL remains the source of truth for workflow history. +- `server_storage` holds the native server's local external payload objects. + Back it up with `mysql_data`; a recovery point needs both. - Keep a copy of the exact env file and image reference with each backup so a restore uses the same auth, database, and image contract. -- External payload bytes are outside the MySQL and Redis volumes. Follow the +- External payload bytes stored outside the server (S3) are outside these volumes. Follow the [online backup coordination procedure](contracts/external-payload-storage.md#coordinating-online-backups) and verify a copy of the referenced objects before accepting a recovery point. @@ -225,13 +228,14 @@ for the backup hold, object manifest, recovery-point boundary and verification. Upgrade order: 1. Back up MySQL and record the current image reference. -2. Change only `DW_SERVER_IMAGE` or `DW_SERVER_TAG` in the env file. +2. Change only `DW_SERVER_IMAGE` in the env file. 3. Run `docker compose --env-file durable-workflow.prod.env -f docker-compose.published.yml pull`. 4. Run `docker compose --env-file durable-workflow.prod.env -f docker-compose.published.yml up -d --wait`. 5. Confirm `/api/ready`, `/api/cluster/info`, and worker registration before shifting external traffic. -The release that introduces `SignalRejected` requires a stop-first upgrade. +For the PHP 2.x stack (`docker-compose.php.yml`): the release that introduces +`SignalRejected` requires a stop-first upgrade. Older Core readers cannot decode this persisted history event. After pulling the new image, pause admission, drain external workers and stop the existing `server`, `worker` and `scheduler` services before starting their replacements. @@ -1552,11 +1556,11 @@ docker run --rm --name durable-workflow-matching \ For Compose deployments, layer the [`docker-compose.dedicated-matching.yml`](../docker-compose.dedicated-matching.yml) -override on top of `docker-compose.published.yml` to enable the same shape: +override on top of the PHP 2.x `docker-compose.php.yml` to enable the same shape: ```bash docker compose \ - -f docker-compose.published.yml \ + -f docker-compose.php.yml \ -f docker-compose.dedicated-matching.yml \ up ``` diff --git a/docs/terminal-summary-recovery.md b/docs/terminal-summary-recovery.md index 2648dd755..9cc605286 100644 --- a/docs/terminal-summary-recovery.md +++ b/docs/terminal-summary-recovery.md @@ -1,5 +1,8 @@ # Repair a terminal history-size summary +This procedure applies to the PHP 2.x Compose stack, `docker-compose.php.yml`. +The published Compose file runs the native server from 3.0.0. + [Server #479](https://github.com/durable-workflow/server/issues/479) tracks terminal summaries that undercount history bytes after Server adds the authenticated worker principal to a completed or failed workflow event. The @@ -20,10 +23,10 @@ published Compose configuration: ```sh RUN_ID='replace-with-the-selected-run-id' -docker compose -f docker-compose.published.yml exec server \ +docker compose -f docker-compose.php.yml exec server \ php artisan workflow:v2:rebuild-projections --run-id="$RUN_ID" --dry-run --json -docker compose -f docker-compose.published.yml exec server \ +docker compose -f docker-compose.php.yml exec server \ php artisan workflow:v2:rebuild-projections --run-id="$RUN_ID" --json ``` diff --git a/scripts/ci/sync-source-release.mjs b/scripts/ci/sync-source-release.mjs index 13a9089ad..cd684a084 100644 --- a/scripts/ci/sync-source-release.mjs +++ b/scripts/ci/sync-source-release.mjs @@ -180,7 +180,7 @@ const consumers = [ }, })), ...[ - ['docker-compose.published.yml', 2], + ['docker-compose.php.yml', 2], ['docker-compose.dedicated-matching.yml', 2], ].map(([path, count]) => ({ path, diff --git a/scripts/conformance/heartbeats-published-artifacts.mjs b/scripts/conformance/heartbeats-published-artifacts.mjs index 49505f849..adee65e16 100755 --- a/scripts/conformance/heartbeats-published-artifacts.mjs +++ b/scripts/conformance/heartbeats-published-artifacts.mjs @@ -101,7 +101,7 @@ const PROJECT_DIR = path.join( IS_PYTHON_CELL ? 'sdk-python' : (IS_RUST_CELL ? 'sdk-rust' : 'sdk-php'), ); const COMPOSE_OVERRIDE = path.join(RUN_ROOT, 'docker-compose.heartbeat.yml'); -const COMPOSE_FILE = path.join(REPO_ROOT, 'docker-compose.published.yml'); +const COMPOSE_FILE = path.join(REPO_ROOT, 'docker-compose.php.yml'); const DISTRIBUTION_IDENTITY_HELPER = path.join(REPO_ROOT, 'scripts/conformance/distribution_identities.py'); const DISTRIBUTION_IDENTITY_FILE = path.join(RESULT_DIR, 'executed-distribution-identities.json'); const SDK_ARTIFACT = IS_PYTHON_CELL ? 'sdk-python' : (IS_RUST_CELL ? 'sdk-rust' : 'sdk-php'); diff --git a/scripts/conformance/heartbeats-shared-server.mjs b/scripts/conformance/heartbeats-shared-server.mjs index e06c4d7cc..b0e227f12 100644 --- a/scripts/conformance/heartbeats-shared-server.mjs +++ b/scripts/conformance/heartbeats-shared-server.mjs @@ -841,7 +841,7 @@ function validateState(value) { if (!value?.compose?.project || !value?.compose?.base_file || !value?.compose?.override_file) { throw new Error(`shared heartbeat server state at ${statePath} has no compose ownership`); } - if (value.compose.base_file !== 'docker-compose.published.yml' + if (value.compose.base_file !== 'docker-compose.php.yml' || path.basename(value.compose.override_file) !== value.compose.override_file || value.compose.network !== `${value.compose.project}_default`) { throw new Error(`shared heartbeat server state at ${statePath} has invalid compose paths`); @@ -897,7 +897,7 @@ async function start() { throw new Error(`refusing to overwrite existing shared heartbeat server state: ${statePath}`); } - const baseFile = path.join(repoRoot, 'docker-compose.published.yml'); + const baseFile = path.join(repoRoot, 'docker-compose.php.yml'); if (!fs.existsSync(baseFile)) throw new Error(`published compose file not found: ${baseFile}`); fs.mkdirSync(path.dirname(statePath), { recursive: true }); const suffix = digest(`${statePath}:${process.pid}:${Date.now()}`).slice(0, 12); diff --git a/scripts/conformance/replay-published-artifacts.sh b/scripts/conformance/replay-published-artifacts.sh index a672c3804..c1e5d6271 100755 --- a/scripts/conformance/replay-published-artifacts.sh +++ b/scripts/conformance/replay-published-artifacts.sh @@ -141,7 +141,7 @@ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" canonical_server_repo_root() { local candidate="$1" - if [[ -n "$candidate" && -f "$candidate/docker-compose.published.yml" ]]; then + if [[ -n "$candidate" && -f "$candidate/docker-compose.php.yml" ]]; then cd "$candidate" && pwd -P return 0 fi @@ -175,7 +175,7 @@ resolve_server_repo_root() { candidate="$(dirname "$candidate")" done - printf '%s\n' 'could not locate server checkout containing docker-compose.published.yml' >&2 + printf '%s\n' 'could not locate server checkout containing docker-compose.php.yml' >&2 return 1 } @@ -184,12 +184,12 @@ resolve_published_compose_file() { local directory directory="$(cd "$root" && pwd -P)" - if [[ ! -f "$directory/docker-compose.published.yml" ]]; then - printf 'server checkout is missing docker-compose.published.yml: %s\n' "$directory" >&2 + if [[ ! -f "$directory/docker-compose.php.yml" ]]; then + printf 'server checkout is missing docker-compose.php.yml: %s\n' "$directory" >&2 return 1 fi - printf '%s/docker-compose.published.yml\n' "$directory" + printf '%s/docker-compose.php.yml\n' "$directory" } repo_root="$(resolve_server_repo_root)" diff --git a/scripts/conformance/schedules-published-artifacts.mjs b/scripts/conformance/schedules-published-artifacts.mjs index 36a5935ee..68213bc90 100755 --- a/scripts/conformance/schedules-published-artifacts.mjs +++ b/scripts/conformance/schedules-published-artifacts.mjs @@ -1936,7 +1936,7 @@ async function runPythonLifecycleShard({ startedAt, artifactVersions, artifactSo const overlayPath = path.join(resultDir, 'schedules-python-lifecycle-compose.override.yml'); const composeFiles = [ '-f', - path.join(repoRoot, 'docker-compose.published.yml'), + path.join(repoRoot, 'docker-compose.php.yml'), '-f', overlayPath, ]; @@ -2461,7 +2461,7 @@ async function runCadenceShard({ startedAt, artifactVersions, artifactSources, s const cadenceEvidencePath = path.join(resultDir, 'schedules-cadence-evidence.json'); const composeFiles = [ '-f', - path.join(repoRoot, 'docker-compose.published.yml'), + path.join(repoRoot, 'docker-compose.php.yml'), '-f', overlayPath, ]; @@ -3113,7 +3113,7 @@ async function runOperatorControlsShard({ startedAt, artifactVersions, artifactS const overlayPath = path.join(resultDir, 'schedules-operator-controls-compose.override.yml'); const composeFiles = [ '-f', - path.join(repoRoot, 'docker-compose.published.yml'), + path.join(repoRoot, 'docker-compose.php.yml'), '-f', overlayPath, ]; @@ -4109,7 +4109,7 @@ async function runMissedRestartShard({ startedAt, artifactVersions, artifactSour const overlayPath = path.join(resultDir, 'schedules-missed-restart-compose.override.yml'); const composeFiles = [ '-f', - path.join(repoRoot, 'docker-compose.published.yml'), + path.join(repoRoot, 'docker-compose.php.yml'), '-f', overlayPath, ]; @@ -4854,7 +4854,7 @@ async function runAdversarialShard({ startedAt, artifactVersions, artifactSource const overlayPath = path.join(resultDir, 'schedules-adversarial-compose.override.yml'); const composeFiles = [ '-f', - path.join(repoRoot, 'docker-compose.published.yml'), + path.join(repoRoot, 'docker-compose.php.yml'), '-f', overlayPath, ]; @@ -5817,7 +5817,7 @@ async function runCliSurfaceShard({ startedAt, artifactVersions, artifactSources const serverPort = positiveInt(process.env.DW_SCHEDULES_SERVER_PORT, 0) || await freePort(); let serverUrl = existingServerUrl || `http://127.0.0.1:${serverPort}`; const composeProject = sanitizeDockerName(runId); - const composeFiles = ['-f', path.join(repoRoot, 'docker-compose.published.yml')]; + const composeFiles = ['-f', path.join(repoRoot, 'docker-compose.php.yml')]; let composeStarted = false; let cliPath = ''; @@ -6457,7 +6457,7 @@ async function runCrossLanguageShard({ startedAt, artifactVersions, artifactSour const overlayPath = path.join(resultDir, 'schedules-cross-language-compose.override.yml'); const composeFiles = [ '-f', - path.join(repoRoot, 'docker-compose.published.yml'), + path.join(repoRoot, 'docker-compose.php.yml'), '-f', overlayPath, ]; @@ -7742,7 +7742,7 @@ async function runPhpSurfaceShard({ startedAt, artifactVersions, artifactSources const overlayPath = path.join(resultDir, 'schedules-php-surface-compose.override.yml'); const composeFiles = [ '-f', - path.join(repoRoot, 'docker-compose.published.yml'), + path.join(repoRoot, 'docker-compose.php.yml'), '-f', overlayPath, ]; diff --git a/scripts/conformance/schedules-published-artifacts.sh b/scripts/conformance/schedules-published-artifacts.sh index 489f5f1bc..44a5dd0ef 100755 --- a/scripts/conformance/schedules-published-artifacts.sh +++ b/scripts/conformance/schedules-published-artifacts.sh @@ -106,7 +106,7 @@ Environment overrides: DW_SCHEDULES_CLI_EXECUTABLE Existing official dw executable to use instead of installing via the release install script. DW_SCHEDULES_SERVER_URL Existing published server URL to probe instead - of starting docker-compose.published.yml. + of starting docker-compose.php.yml. DW_SERVER_IMAGE Exact published server image/tag/digest to test. DW_SCHEDULES_SERVER_PORT Host port for the published server. Defaults to a free port. diff --git a/scripts/conformance/signals-queries-published-artifacts.sh b/scripts/conformance/signals-queries-published-artifacts.sh index ce8118834..783f184c1 100755 --- a/scripts/conformance/signals-queries-published-artifacts.sh +++ b/scripts/conformance/signals-queries-published-artifacts.sh @@ -1533,7 +1533,7 @@ def start_published_server(run_root: Path, log_file: Path) -> tuple[str, list[li if not command_available("docker"): raise RuntimeError("docker is required to start the published server") - compose = Path(os.environ.get("REPO_ROOT", os.getcwd())) / "docker-compose.published.yml" + compose = Path(os.environ.get("REPO_ROOT", os.getcwd())) / "docker-compose.php.yml" if not compose.is_file(): raise RuntimeError(f"published compose file not found: {compose}") diff --git a/scripts/conformance/skew-published-artifacts.sh b/scripts/conformance/skew-published-artifacts.sh index c293dde53..3818883ed 100755 --- a/scripts/conformance/skew-published-artifacts.sh +++ b/scripts/conformance/skew-published-artifacts.sh @@ -275,7 +275,7 @@ cleanup() { fi if [[ "$server_started" == "1" || "$compose_cleanup_needed" == "1" ]]; then - docker compose -p "$compose_project" -f "$repo_root/docker-compose.published.yml" down -v >/dev/null 2>&1 || true + docker compose -p "$compose_project" -f "$repo_root/docker-compose.php.yml" down -v >/dev/null 2>&1 || true fi if [[ "$keep_run_root" != "1" && "$code" -eq 0 && "$result_dir" != "$run_root" ]]; then @@ -398,7 +398,7 @@ if [[ -z "$server_url" ]]; then DW_SERVER_IMAGE="$server_image" \ DW_SERVER_TAG="${DW_SERVER_VERSION:-}" \ DW_AUTH_TOKEN="${DW_SKEW_AUTH_TOKEN:-dev-token}" \ - docker compose -p "$compose_project" -f "$repo_root/docker-compose.published.yml" up -d server worker >"$result_dir/docker-compose-up.log" 2>&1; then + docker compose -p "$compose_project" -f "$repo_root/docker-compose.php.yml" up -d server worker >"$result_dir/docker-compose-up.log" 2>&1; then write_blocked_result "published server failed to start from ${server_image}; see docker-compose-up.log" exit 0 fi @@ -411,7 +411,7 @@ if [[ -z "$server_url" ]]; then server_queue_worker_id="" for _ in {1..30}; do - server_queue_worker_id="$(docker compose -p "$compose_project" -f "$repo_root/docker-compose.published.yml" ps -q worker 2>/dev/null || true)" + server_queue_worker_id="$(docker compose -p "$compose_project" -f "$repo_root/docker-compose.php.yml" ps -q worker 2>/dev/null || true)" if [[ -n "$server_queue_worker_id" ]] \ && [[ "$(docker inspect -f '{{.State.Running}}' "$server_queue_worker_id" 2>/dev/null || true)" == "true" ]]; then break @@ -422,7 +422,7 @@ if [[ -z "$server_url" ]]; then done if [[ -z "$server_queue_worker_id" ]]; then - docker compose -p "$compose_project" -f "$repo_root/docker-compose.published.yml" logs worker >"$result_dir/server-queue-worker.log" 2>&1 || true + docker compose -p "$compose_project" -f "$repo_root/docker-compose.php.yml" logs worker >"$result_dir/server-queue-worker.log" 2>&1 || true write_blocked_result "published server queue worker failed to start; sdk-php compatible skew evidence requires queue-backed workflow task fixture polling; see server-queue-worker.log" exit 0 fi diff --git a/scripts/conformance/worker-versioning-published-artifacts.sh b/scripts/conformance/worker-versioning-published-artifacts.sh index cbcfef8c4..eb32ccac7 100755 --- a/scripts/conformance/worker-versioning-published-artifacts.sh +++ b/scripts/conformance/worker-versioning-published-artifacts.sh @@ -668,7 +668,7 @@ waterline_container="" docker_compose() { local compose_args=( -p "$compose_project" - -f "$repo_root/docker-compose.published.yml" + -f "$repo_root/docker-compose.php.yml" ) if [[ -f "$run_root/waterline-compose.yml" ]]; then diff --git a/scripts/conformance/workflow-updates-published-artifacts.sh b/scripts/conformance/workflow-updates-published-artifacts.sh index 56449c77a..eecb051d2 100755 --- a/scripts/conformance/workflow-updates-published-artifacts.sh +++ b/scripts/conformance/workflow-updates-published-artifacts.sh @@ -127,7 +127,7 @@ cleanup_background_processes() { done local project for project in "${cleanup_compose_projects[@]}"; do - docker compose -p "$project" -f "$repo_root/docker-compose.published.yml" down -v >/dev/null 2>&1 || true + docker compose -p "$project" -f "$repo_root/docker-compose.php.yml" down -v >/dev/null 2>&1 || true done } trap cleanup_background_processes EXIT @@ -2320,7 +2320,7 @@ capture_php_server_startup_diagnostics() { DW_SERVER_IMAGE="${DW_SERVER_IMAGE}" \ DW_SERVER_TAG="${DW_SERVER_VERSION}" \ DW_AUTH_TOKEN="$auth_token" \ - docker compose -p "$compose_project" -f "$repo_root/docker-compose.published.yml" ps --all 2>&1 || true + docker compose -p "$compose_project" -f "$repo_root/docker-compose.php.yml" ps --all 2>&1 || true } | head -c 32768 > "$result_dir/sdk-php-compose-ps.log" server_container_id="$( @@ -2329,7 +2329,7 @@ capture_php_server_startup_diagnostics() { DW_SERVER_IMAGE="${DW_SERVER_IMAGE}" \ DW_SERVER_TAG="${DW_SERVER_VERSION}" \ DW_AUTH_TOKEN="$auth_token" \ - docker compose -p "$compose_project" -f "$repo_root/docker-compose.published.yml" ps -q server 2>/dev/null || true + docker compose -p "$compose_project" -f "$repo_root/docker-compose.php.yml" ps -q server 2>/dev/null || true } | head -n 1 )" if [[ -n "$server_container_id" ]]; then @@ -2345,7 +2345,7 @@ capture_php_server_startup_diagnostics() { DW_SERVER_IMAGE="${DW_SERVER_IMAGE}" \ DW_SERVER_TAG="${DW_SERVER_VERSION}" \ DW_AUTH_TOKEN="$auth_token" \ - docker compose -p "$compose_project" -f "$repo_root/docker-compose.published.yml" \ + docker compose -p "$compose_project" -f "$repo_root/docker-compose.php.yml" \ logs --no-color --tail 200 server 2>&1 || true } | tail -c 65536 > "$result_dir/sdk-php-server-container.log" } @@ -2612,21 +2612,21 @@ run_php_package_shard() { server_url="http://${server_connect_host}:${server_port}" cleanup_compose_projects+=("$compose_project") if ! SERVER_PORT="$compose_server_port" DW_SERVER_IMAGE="${DW_SERVER_IMAGE}" DW_SERVER_TAG="${DW_SERVER_VERSION}" DW_AUTH_TOKEN="$auth_token" \ - docker compose -p "$compose_project" -f "$repo_root/docker-compose.published.yml" up -d mysql redis \ + docker compose -p "$compose_project" -f "$repo_root/docker-compose.php.yml" up -d mysql redis \ > "$result_dir/sdk-php-compose-dependencies.log" 2>&1; then capture_php_server_startup_diagnostics "$compose_project" "$compose_server_port" "$auth_token" write_php_package_shard_status runner_blocked "The published server dependencies could not start for the PHP SDK update shard." server_dependencies true return 0 fi if ! SERVER_PORT="$compose_server_port" DW_SERVER_IMAGE="${DW_SERVER_IMAGE}" DW_SERVER_TAG="${DW_SERVER_VERSION}" DW_AUTH_TOKEN="$auth_token" \ - docker compose -p "$compose_project" -f "$repo_root/docker-compose.published.yml" run --rm bootstrap \ + docker compose -p "$compose_project" -f "$repo_root/docker-compose.php.yml" run --rm bootstrap \ > "$result_dir/sdk-php-server-bootstrap.log" 2>&1; then capture_php_server_startup_diagnostics "$compose_project" "$compose_server_port" "$auth_token" write_php_package_shard_status fail "The exact public server image could not bootstrap for the PHP SDK update shard." server_bootstrap false return 0 fi if ! SERVER_PORT="$compose_server_port" DW_SERVER_IMAGE="${DW_SERVER_IMAGE}" DW_SERVER_TAG="${DW_SERVER_VERSION}" DW_AUTH_TOKEN="$auth_token" \ - docker compose -p "$compose_project" -f "$repo_root/docker-compose.published.yml" up -d --no-deps server scheduler \ + docker compose -p "$compose_project" -f "$repo_root/docker-compose.php.yml" up -d --no-deps server scheduler \ > "$result_dir/sdk-php-server.log" 2>&1; then capture_php_server_startup_diagnostics "$compose_project" "$compose_server_port" "$auth_token" write_php_package_shard_status runner_blocked "The exact public server image could not start for the PHP SDK update shard." server_start true diff --git a/scripts/perf/run-server-soak.sh b/scripts/perf/run-server-soak.sh index f188037dd..9a99b4080 100755 --- a/scripts/perf/run-server-soak.sh +++ b/scripts/perf/run-server-soak.sh @@ -113,7 +113,7 @@ export DW_AUTH_BACKWARD_COMPATIBLE="${DW_AUTH_BACKWARD_COMPATIBLE:-true}" export DW_PERF_STANDARD_WORKFLOWS="${DW_PERF_STANDARD_WORKFLOWS:-true}" OVERRIDE_FILE="$ARTIFACT_DIR/docker-compose.perf.yml" -PUBLISHED_OVERRIDE_FILE="$ARTIFACT_DIR/docker-compose.published.yml" +PUBLISHED_OVERRIDE_FILE="$ARTIFACT_DIR/docker-compose.php.yml" published_image_id="" server_php_version="" cat > "$OVERRIDE_FILE" </dev/null 2>&1; then + echo "Compose rendered without DW_AUTH_TOKEN; it must refuse an unset token" >&2 + exit 1 +fi + compose pull compose up -d --wait if [ "$ENDPOINT_MODE" = "host" ]; then - SERVER_PORT="$(compose port server 8080 | awk -F: 'END {print $NF}')" + published="$(compose port server 8080)" + # The default host binding is loopback only. + if [ "$NATIVE_PUBLISHED" = true ] && [ -z "${SERVER_BIND_ADDRESS:-}" ] && [ "${published%%:*}" != "127.0.0.1" ]; then + echo "server port is published on ${published}, not loopback" >&2 + exit 1 + fi + SERVER_PORT="$(printf '%s\n' "$published" | awk -F: 'END {print $NF}')" if [ -z "$SERVER_PORT" ]; then echo "Unable to discover the published server port for $PROJECT" >&2 exit 1 @@ -144,4 +164,12 @@ assert worker.get("worker_id") == os.environ["DW_SMOKE_WORKER_ID"], worker assert worker.get("registered") is True, worker PY +if [ "$PROFILE" = "production" ]; then + status="$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer dev-token" "${BASE_URL}/api/cluster/info" || true)" + if [ "$status" != "401" ]; then + echo "production profile accepted dev-token (HTTP $status)" >&2 + exit 1 + fi +fi + echo "Published Compose smoke passed" diff --git a/tests/Unit/DedicatedMatchingComposeContractTest.php b/tests/Unit/DedicatedMatchingComposeContractTest.php index b4291c081..032418ba8 100644 --- a/tests/Unit/DedicatedMatchingComposeContractTest.php +++ b/tests/Unit/DedicatedMatchingComposeContractTest.php @@ -58,7 +58,7 @@ public function test_override_disables_in_worker_matching_wake_on_every_long_run public function test_override_uses_the_same_image_alias_as_published_compose(): void { $override = $this->read('docker-compose.dedicated-matching.yml'); - $published = $this->read('docker-compose.published.yml'); + $published = $this->read('docker-compose.php.yml'); $source = json_decode( $this->read('resources/release/source-release.json'), true, @@ -85,7 +85,7 @@ public function test_override_uses_the_same_image_alias_as_published_compose(): public function test_published_compose_pins_topology_identity_for_supported_processes(): void { - $published = $this->read('docker-compose.published.yml'); + $published = $this->read('docker-compose.php.yml'); foreach ([ 'DW_SERVER_TOPOLOGY_SHAPE: standalone_server', diff --git a/tests/Unit/HeartbeatSharedCleanupRegression.mjs b/tests/Unit/HeartbeatSharedCleanupRegression.mjs index 75d145b4e..7049e2406 100644 --- a/tests/Unit/HeartbeatSharedCleanupRegression.mjs +++ b/tests/Unit/HeartbeatSharedCleanupRegression.mjs @@ -121,7 +121,7 @@ function sharedState(project, overrideFile) { }, compose: { project, - base_file: 'docker-compose.published.yml', + base_file: 'docker-compose.php.yml', override_file: path.basename(overrideFile), network: `${project}_default`, }, diff --git a/tests/Unit/NativePublishedComposeContractTest.php b/tests/Unit/NativePublishedComposeContractTest.php new file mode 100644 index 000000000..d98963c71 --- /dev/null +++ b/tests/Unit/NativePublishedComposeContractTest.php @@ -0,0 +1,114 @@ +repoRoot = dirname(__DIR__, 2); + } + + public function test_published_compose_runs_only_the_native_server_and_mysql(): void + { + $compose = Yaml::parse($this->read('docker-compose.published.yml')); + + $this->assertSame(['server', 'mysql'], array_keys($compose['services'])); + $this->assertSame(['mysql_data', 'server_storage'], array_keys($compose['volumes'])); + } + + public function test_default_image_is_a_native_image_pinned_by_digest_or_exact_release(): void + { + $source = $this->read('docker-compose.published.yml'); + + $this->assertMatchesRegularExpression( + '/^x-server-image: &server-image \$\{DW_SERVER_IMAGE:-(ghcr\.io\/durable-workflow\/server-native@sha256:[0-9a-f]{64}|durableworkflow\/server:3\.\d+\.\d+)\}$/m', + $source, + ); + } + + public function test_server_environment_matches_what_native_accepts(): void + { + $server = Yaml::parse($this->read('docker-compose.published.yml'))['services']['server']; + $environment = $server['environment']; + + // Native refuses DW_AUTH_BACKWARD_COMPATIBLE=false and principal tokens. + $this->assertArrayNotHasKey('DW_AUTH_BACKWARD_COMPATIBLE', $environment); + $this->assertArrayNotHasKey('DW_PRINCIPAL_TOKENS', $environment); + $this->assertSame('mysql', $environment['DB_CONNECTION']); + $this->assertSame('mysql', $environment['DB_HOST']); + foreach (['DW_AUTH_TOKEN', 'DW_WORKER_TOKEN', 'DW_OPERATOR_TOKEN', 'DW_ADMIN_TOKEN'] as $token) { + $this->assertArrayHasKey($token, $environment); + } + foreach (array_keys($environment) as $name) { + $this->assertDoesNotMatchRegularExpression('/^(REDIS_|QUEUE_|CACHE_|APP_)/', $name); + } + } + + public function test_published_compose_fails_closed_without_a_token_and_binds_loopback(): void + { + $source = $this->read('docker-compose.published.yml'); + $compose = Yaml::parse($source); + + // No implicit shared token: Compose refuses to render until one is set. + $this->assertMatchesRegularExpression('/^ DW_AUTH_TOKEN: \$\{DW_AUTH_TOKEN:\?[^}]+\}$/m', $source); + $this->assertStringNotContainsString('DW_AUTH_TOKEN:-', $source); + + // The API is published on loopback unless an interface is chosen. + $this->assertSame( + ['${SERVER_BIND_ADDRESS:-127.0.0.1}:${SERVER_PORT:-8080}:8080'], + $compose['services']['server']['ports'], + ); + $this->assertArrayNotHasKey('ports', $compose['services']['mysql']); + } + + public function test_healthcheck_reads_readiness_without_curl(): void + { + $server = Yaml::parse($this->read('docker-compose.published.yml'))['services']['server']; + $test = $server['healthcheck']['test']; + + $this->assertSame(['CMD', 'bash', '-c'], array_slice($test, 0, 3)); + $this->assertStringContainsString('/dev/tcp/127.0.0.1/8080', $test[3]); + $this->assertStringContainsString('GET /api/ready', $test[3]); + $this->assertSame('service_healthy', $server['depends_on']['mysql']['condition']); + } + + public function test_php_stack_stays_available_and_release_smoke_uses_it(): void + { + $php = Yaml::parse($this->read('docker-compose.php.yml')); + foreach (['bootstrap', 'server', 'worker', 'scheduler', 'mysql', 'redis'] as $service) { + $this->assertArrayHasKey($service, $php['services']); + } + + $release = $this->read('.github/workflows/release.yml'); + $this->assertStringContainsString('DW_PUBLISHED_COMPOSE_FILE: docker-compose.php.yml', $release); + } + + public function test_published_compose_smoke_runs_on_pull_requests_that_change_it(): void + { + $workflow = Yaml::parse($this->read('.github/workflows/published-compose.yml')); + // YAML 1.1 reads the "on" key as true. + $on = $workflow['on'] ?? $workflow[true]; + + $this->assertContains('docker-compose.published.yml', $on['pull_request']['paths']); + } + + private function read(string $path): string + { + $contents = file_get_contents($this->repoRoot.'/'.$path); + $this->assertIsString($contents); + + return $contents; + } +} diff --git a/tests/Unit/PublishedImageFirstRunContractTest.php b/tests/Unit/PublishedImageFirstRunContractTest.php index cb5ad6101..8bd280291 100644 --- a/tests/Unit/PublishedImageFirstRunContractTest.php +++ b/tests/Unit/PublishedImageFirstRunContractTest.php @@ -62,7 +62,7 @@ public function test_published_image_smoke_exercises_unhealthy_then_bootstrapped public function test_published_compose_keeps_automatic_bootstrap_dependency(): void { - $compose = Yaml::parse($this->read('docker-compose.published.yml')); + $compose = Yaml::parse($this->read('docker-compose.php.yml')); $this->assertSame( ['server-bootstrap'], @@ -94,7 +94,7 @@ public function test_compose_wait_can_observe_every_long_running_cli_process(): 'worker' => 'worker', 'scheduler' => 'scheduler', ], - 'docker-compose.published.yml' => [ + 'docker-compose.php.yml' => [ 'worker' => 'worker', 'scheduler' => 'scheduler', ], @@ -130,7 +130,7 @@ public function test_compose_wait_can_observe_every_long_running_cli_process(): public function test_published_compose_restarts_the_queue_worker_after_backend_loss(): void { - $compose = Yaml::parse($this->read('docker-compose.published.yml')); + $compose = Yaml::parse($this->read('docker-compose.php.yml')); $this->assertSame('unless-stopped', $compose['services']['worker']['restart'] ?? null); } diff --git a/tests/Unit/ReplayConformanceRunnerContractTest.php b/tests/Unit/ReplayConformanceRunnerContractTest.php index 1ff884589..b49caf0c2 100644 --- a/tests/Unit/ReplayConformanceRunnerContractTest.php +++ b/tests/Unit/ReplayConformanceRunnerContractTest.php @@ -389,11 +389,11 @@ public function test_runner_uses_server_checkout_compose_path_before_any_compose ); } - $this->assertStringNotContainsString('-f docker-compose.published.yml', $source); - $this->assertStringNotContainsString('published_compose_file="$repo_root/docker-compose.published.yml"', $source); - $this->assertStringNotContainsString('-f "$repo_root/docker-compose.published.yml"', $source); - $this->assertStringNotContainsString('-f "$run_root/docker-compose.published.yml"', $source); - $this->assertStringNotContainsString('-f "$result_dir/docker-compose.published.yml"', $source); + $this->assertStringNotContainsString('-f docker-compose.php.yml', $source); + $this->assertStringNotContainsString('published_compose_file="$repo_root/docker-compose.php.yml"', $source); + $this->assertStringNotContainsString('-f "$repo_root/docker-compose.php.yml"', $source); + $this->assertStringNotContainsString('-f "$run_root/docker-compose.php.yml"', $source); + $this->assertStringNotContainsString('-f "$result_dir/docker-compose.php.yml"', $source); } public function test_staged_runner_resolves_compose_file_from_server_checkout_not_caller_directory(): void @@ -422,7 +422,7 @@ public function test_staged_runner_resolves_compose_file_from_server_checkout_no copy($repoRoot.'/scripts/conformance/replay-published-artifacts.sh', $stagedScript); chmod($stagedScript, 0755); - copy($repoRoot.'/docker-compose.published.yml', $serverCheckout.'/docker-compose.published.yml'); + copy($repoRoot.'/docker-compose.php.yml', $serverCheckout.'/docker-compose.php.yml'); $realPython = trim((string) shell_exec('command -v python3 2>/dev/null')); if ($realPython === '') { @@ -579,12 +579,12 @@ public function test_staged_runner_resolves_compose_file_from_server_checkout_no $this->assertFileExists($dockerLog); $log = (string) file_get_contents($dockerLog); - $expectedComposeFile = $serverCheckout.'/docker-compose.published.yml'; + $expectedComposeFile = $serverCheckout.'/docker-compose.php.yml'; $this->assertStringContainsString('compose -p ', $log); $this->assertStringContainsString('-f '.$expectedComposeFile, $log); - $this->assertStringNotContainsString($workspace.'/.tmp/docker-compose.published.yml', $log); - $this->assertStringNotContainsString($cwd.'/docker-compose.published.yml', $log); - $this->assertStringNotContainsString($resultDir.'/docker-compose.published.yml', $log); + $this->assertStringNotContainsString($workspace.'/.tmp/docker-compose.php.yml', $log); + $this->assertStringNotContainsString($cwd.'/docker-compose.php.yml', $log); + $this->assertStringNotContainsString($resultDir.'/docker-compose.php.yml', $log); $this->assertStringNotContainsString(' config ', $log); preg_match_all('/compose [^\n]* -f ([^ ]+)/', $log, $matches); @@ -642,7 +642,7 @@ public function test_staged_runner_rewrites_pass_record_when_cleanup_fails_after $repoRoot.'/scripts/conformance/php_sdk_replay_scenarios.py', dirname($stagedScript).'/php_sdk_replay_scenarios.py', ); - copy($repoRoot.'/docker-compose.published.yml', $serverCheckout.'/docker-compose.published.yml'); + copy($repoRoot.'/docker-compose.php.yml', $serverCheckout.'/docker-compose.php.yml'); // The staged runner hands its venv to the patched-pip bootstrap; // record that hand-off instead of downloading pip. diff --git a/tests/Unit/Support/composer-sinks.json b/tests/Unit/Support/composer-sinks.json index d0d04577b..11fdfdaab 100644 --- a/tests/Unit/Support/composer-sinks.json +++ b/tests/Unit/Support/composer-sinks.json @@ -19,7 +19,7 @@ }, { "path": ".github/workflows/phpunit-feature.yml", - "line": "-lc 'set -eu; tar --no-same-owner -xf - -C /app; cp -a /app/workflow-package /workflow; php scripts/ci/prepare-release-workflow-composer-metadata.php; composer update durable-workflow/workflow --with-dependencies --minimal-changes --no-install --no-scripts --no-autoloader --no-interaction --no-progress --prefer-dist; composer install --no-interaction --no-progress --prefer-dist; git --version; vendor/bin/phpunit --version; git rev-parse --verify \"$CORPUS_BASE_REF^{commit}\"; php scripts/ci/check-worker-openapi-evolution.php \"$CORPUS_BASE_REF\"; vendor/bin/phpunit tests/Feature tests/Unit/NexusContractTest.php tests/Unit/CodecRegressionCorpusTest.php tests/Unit/PhpunitFeatureWorkflowContractTest.php tests/Unit/OpenApiDocumentEvolutionTest.php tests/Unit/NamespaceConformanceRunnerContractTest.php tests/Unit/NamespaceRuntimeContractTest.php tests/Unit/SchedulesConformanceRunnerContractTest.php tests/Unit/SchedulesRuntimeContractTest.php tests/Unit/ServerParitySdkDirectionResponseSummaryTest.php tests/Unit/ServerParitySdkQueryTransportTest.php tests/Unit/ServerParitySdkUpdateTransportTest.php tests/Unit/ServerParityLockPressureRetryTest.php tests/Unit/ServerParityManyUpdatesRetryTest.php --no-progress --colors=never; vendor/bin/phpunit tests/Unit/ReleaseImageContinuationTest.php tests/Unit/ReleaseImageImmutabilityGuardTest.php --no-progress --colors=never; vendor/bin/phpunit tests/Unit/ReleaseImagePublishWorkflowContractTest.php --filter test_php_security_audits_fail_before_release_publication --no-progress --colors=never; python3 scripts/ci/validate-regression-corpus.py --base-ref \"$CORPUS_BASE_REF\" --verify-counterfactual'", + "line": "-lc 'set -eu; tar --no-same-owner -xf - -C /app; cp -a /app/workflow-package /workflow; php scripts/ci/prepare-release-workflow-composer-metadata.php; composer update durable-workflow/workflow --with-dependencies --minimal-changes --no-install --no-scripts --no-autoloader --no-interaction --no-progress --prefer-dist; composer install --no-interaction --no-progress --prefer-dist; git --version; vendor/bin/phpunit --version; git rev-parse --verify \"$CORPUS_BASE_REF^{commit}\"; php scripts/ci/check-worker-openapi-evolution.php \"$CORPUS_BASE_REF\"; vendor/bin/phpunit tests/Feature tests/Unit/NexusContractTest.php tests/Unit/NativePublishedComposeContractTest.php tests/Unit/PublishedServerAutoSelectionTest.php tests/Unit/CodecRegressionCorpusTest.php tests/Unit/PhpunitFeatureWorkflowContractTest.php tests/Unit/OpenApiDocumentEvolutionTest.php tests/Unit/NamespaceConformanceRunnerContractTest.php tests/Unit/NamespaceRuntimeContractTest.php tests/Unit/SchedulesConformanceRunnerContractTest.php tests/Unit/SchedulesRuntimeContractTest.php tests/Unit/ServerParitySdkDirectionResponseSummaryTest.php tests/Unit/ServerParitySdkQueryTransportTest.php tests/Unit/ServerParitySdkUpdateTransportTest.php tests/Unit/ServerParityLockPressureRetryTest.php tests/Unit/ServerParityManyUpdatesRetryTest.php --no-progress --colors=never; vendor/bin/phpunit tests/Unit/ReleaseImageContinuationTest.php tests/Unit/ReleaseImageImmutabilityGuardTest.php --no-progress --colors=never; vendor/bin/phpunit tests/Unit/ReleaseImagePublishWorkflowContractTest.php --filter test_php_security_audits_fail_before_release_publication --no-progress --colors=never; python3 scripts/ci/validate-regression-corpus.py --base-ref \"$CORPUS_BASE_REF\" --verify-counterfactual'", "count": 2, "confinement": "Runs in the source-free corpus tool image. The feature step checks its system bundle, hashed directory and Composer selection before fresh source and dependency installation, on cold builds and cache hits.", "guards": [ diff --git a/tests/Unit/Support/docker_workflow_isolation_policy_test.py b/tests/Unit/Support/docker_workflow_isolation_policy_test.py index ac7b87777..53941bd4c 100644 --- a/tests/Unit/Support/docker_workflow_isolation_policy_test.py +++ b/tests/Unit/Support/docker_workflow_isolation_policy_test.py @@ -2175,12 +2175,12 @@ def test_rejects_compose_port_variable_left_at_fixed_default(self) -> None: PROJECT: smoke-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} UNUSED_PORT: "0" steps: - - run: docker compose -p "$PROJECT" -f docker-compose.published.yml up -d + - run: docker compose -p "$PROJECT" -f docker-compose.php.yml up -d - if: always() - run: docker compose -p "$PROJECT" -f docker-compose.published.yml down -v || true + run: docker compose -p "$PROJECT" -f docker-compose.php.yml down -v || true """, { - "docker-compose.published.yml": """ + "docker-compose.php.yml": """ services: server: ports: