diff --git a/.github/workflows/drupal-contrib-integration-test.yml b/.github/workflows/drupal-contrib-integration-test.yml new file mode 100644 index 0000000..d1498b2 --- /dev/null +++ b/.github/workflows/drupal-contrib-integration-test.yml @@ -0,0 +1,386 @@ +name: Drupal Contrib Integration Tests + +# Creates drupal-contrib workspaces on staging-coder.ddev.com and verifies they +# reach a working state (DDEV running, Drupal installed, module enabled). +# +# Two jobs: +# contrib-plain — matrix of known modules on D11, runs on every +# push to drupal-contrib/** and nightly +# contrib-issue-fork — single workspace using a real drupal.org contrib issue +# fork, runs nightly only +# +# Repository variables (optional, set in GitHub → Settings → Variables): +# CONTRIB_TEST_PROJECT - Module machine name for issue fork test (default: token) +# CONTRIB_TEST_ISSUE_FORK - Issue number on drupal.org for that module (no default; +# skip issue-fork job if unset) +# CONTRIB_TEST_DRUPAL_VERSION - Drupal core version for the issue fork test (default: 11). +# NOTE: field_issue_version for contrib modules is the MODULE +# version, not the Drupal core version, so it cannot be used +# to infer the target Drupal version automatically. +# +# Requires: +# Repository variable: TEST_CODER_URL - https://staging-coder.ddev.com +# Repository secret: OP_SERVICE_ACCOUNT_TOKEN - 1Password service account with read access +# 1Password item: op://test-secrets/TEST_CODER_SESSION_TOKEN/credential + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +on: + schedule: + - cron: '0 5 * * *' + push: + branches: [main] + paths: + - 'drupal-contrib/**' + pull_request: + workflow_dispatch: + inputs: + debug_enabled: + description: 'Run the build with tmate set "debug_enabled"' + type: boolean + required: false + default: false + +jobs: + contrib-plain: + name: Contrib ${{ matrix.project }} D${{ matrix.drupal_version }} (plain) + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login == github.repository_owner }} + runs-on: [self-hosted, sysbox] + strategy: + matrix: + include: + - project: token + drupal_version: "11" + - project: pathauto + drupal_version: "11" + fail-fast: false + defaults: + run: + shell: bash -euo pipefail {0} + env: + WORKSPACE_NAME: cc-${{ matrix.project }}-d${{ matrix.drupal_version }}-${{ github.run_number }}-${{ github.run_attempt }} + CI: "true" + DDEV_NONINTERACTIVE: "true" + NO_COLOR: "1" + + steps: + - uses: actions/checkout@v6 + + - name: Load 1Password secrets + uses: 1password/load-secrets-action@v4 + with: + export-env: true + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + TEST_CODER_SESSION_TOKEN: "op://test-secrets/TEST_CODER_SESSION_TOKEN/credential" + + - name: Login to Coder + if: ${{ env.TEST_CODER_SESSION_TOKEN != '' }} + run: coder login --token "${{ env.TEST_CODER_SESSION_TOKEN }}" "${{ vars.TEST_CODER_URL }}" + + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + with: + limit-access-to-actor: true + github-token: ${{ secrets.GITHUB_TOKEN }} + if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} + + - name: Copy VERSION into template directory + run: cp VERSION drupal-contrib/VERSION + + - name: Push template (inactive) + run: | + coder templates push drupal-contrib \ + --directory drupal-contrib \ + --activate=false \ + --name ${{ env.WORKSPACE_NAME }} \ + --yes \ + --variable workspace_image_registry=index.docker.io/ddev/coder-ddev + + - name: Create workspace + run: | + coder create ${{ env.WORKSPACE_NAME }} \ + --template drupal-contrib \ + --template-version ${{ env.WORKSPACE_NAME }} \ + --parameter "vscode_extensions=[]" \ + --parameter project_name=${{ matrix.project }} \ + --parameter project_type=module \ + --parameter drupal_version=${{ matrix.drupal_version }} \ + --parameter issue_fork= \ + --parameter issue_branch= \ + --parameter install_profile=minimal \ + --parameter share_drupal_site=owner \ + --yes + + - name: Verify workspace — agent connected + run: coder ssh ${{ env.WORKSPACE_NAME }} --wait=yes -- echo "Agent connected" + + - name: Verify workspace — Docker daemon running + run: coder ssh ${{ env.WORKSPACE_NAME }} -- docker ps + + - name: Verify workspace — DDEV installed + run: coder ssh ${{ env.WORKSPACE_NAME }} -- ddev --version + + - name: Verify workspace — project repo cloned + run: coder ssh ${{ env.WORKSPACE_NAME }} -- test -d /home/coder/${{ matrix.project }}/.git + + - name: Verify workspace — Drupal web root exists + run: coder ssh ${{ env.WORKSPACE_NAME }} -- test -f /home/coder/${{ matrix.project }}/web/index.php + + - name: Verify workspace — Drush DB connected + run: | + coder ssh ${{ env.WORKSPACE_NAME }} -- env -C /home/coder/${{ matrix.project }} ddev drush status --fields=db-status \ + | grep -i connected + + - name: Dump setup status + if: always() + run: | + coder ssh ${{ env.WORKSPACE_NAME }} -- cat /home/coder/SETUP_STATUS.txt || true + echo "--- last 30 lines of setup log ---" + coder ssh ${{ env.WORKSPACE_NAME }} -- tail -30 /tmp/drupal-setup.log || true + + - name: Verify workspace — module enabled + run: | + MODULES=$(coder ssh ${{ env.WORKSPACE_NAME }} -- env -C /home/coder/${{ matrix.project }} ddev drush pm:list --status=enabled --format=list) + echo "--- enabled modules ---" + echo "$MODULES" + echo "--- end ---" + echo "$MODULES" | grep -iw "${{ matrix.project }}" + + - name: Record expected host directory + run: | + OWNER=$(coder whoami --output json | jq -r 'if type == "array" then .[0].username else .username end') + echo "OWNER=$OWNER" >> "$GITHUB_ENV" + echo "HOST_DIR=/coder-workspaces/${OWNER}-${{ env.WORKSPACE_NAME }}" >> "$GITHUB_ENV" + + - name: Verify workspace — Drupal site externally accessible + run: | + CODER_DOMAIN="${{ vars.TEST_CODER_URL }}" + CODER_DOMAIN="${CODER_DOMAIN#https://}" + SITE_URL="https://drupal-site--${{ env.WORKSPACE_NAME }}--${OWNER}.${CODER_DOMAIN}" + echo "Checking $SITE_URL" + HTTP_STATUS=$(curl -sL --max-time 30 --retry 3 --retry-delay 5 \ + -H "Coder-Session-Token: ${TEST_CODER_SESSION_TOKEN}" \ + -o /tmp/drupal-response.html \ + -w "%{http_code}" \ + "$SITE_URL") + echo "HTTP status: $HTTP_STATUS" + if [[ ! "$HTTP_STATUS" =~ ^[23] ]]; then + echo "ERROR: unexpected HTTP status $HTTP_STATUS" >&2 + head -50 /tmp/drupal-response.html >&2 || true + exit 1 + fi + if ! grep -qi "log in" /tmp/drupal-response.html; then + echo "ERROR: response does not contain 'log in' — Drupal may not have started" >&2 + head -50 /tmp/drupal-response.html >&2 + exit 1 + fi + echo "OK: Drupal site is accessible and shows login page" + + - name: Delete workspace + if: always() + run: coder delete ${{ env.WORKSPACE_NAME }} --yes || true + + - name: Verify host directory removed + if: always() + run: | + if [[ -z "${HOST_DIR:-}" ]]; then + echo "HOST_DIR not set — workspace may not have been created, skipping" + exit 0 + fi + if [[ -d "$HOST_DIR" ]]; then + echo "ERROR: Host directory was not removed by destroy provisioner: $HOST_DIR" >&2 + ls -la "$HOST_DIR" >&2 || true + exit 1 + fi + echo "OK: host directory removed: $HOST_DIR" + + - name: Archive CI template version + if: always() + run: coder templates versions archive drupal-contrib ${{ env.WORKSPACE_NAME }} --yes || true + + contrib-issue-fork: + name: Contrib ${{ vars.CONTRIB_TEST_PROJECT || 'token' }} issue fork + if: | + vars.CONTRIB_TEST_ISSUE_FORK != '' && + github.event_name != 'push' && + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login == github.repository_owner) + runs-on: [self-hosted, sysbox] + defaults: + run: + shell: bash -euo pipefail {0} + env: + WORKSPACE_NAME: cc-fork-${{ github.run_number }}-${{ github.run_attempt }} + CI: "true" + DDEV_NONINTERACTIVE: "true" + NO_COLOR: "1" + CONTRIB_PROJECT: ${{ vars.CONTRIB_TEST_PROJECT || 'token' }} + ISSUE_FORK: ${{ vars.CONTRIB_TEST_ISSUE_FORK }} + CONTRIB_TEST_DRUPAL_VERSION: ${{ vars.CONTRIB_TEST_DRUPAL_VERSION || '11' }} + ISSUE_BRANCH: "" + ISSUE_NUMBER: "" + ISSUE_FORK_VERSION: "" + + steps: + - uses: actions/checkout@v6 + + - name: Load 1Password secrets + uses: 1password/load-secrets-action@v4 + with: + export-env: true + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + TEST_CODER_SESSION_TOKEN: "op://test-secrets/TEST_CODER_SESSION_TOKEN/credential" + + - name: Login to Coder + if: ${{ env.TEST_CODER_SESSION_TOKEN != '' }} + run: coder login --token "${{ env.TEST_CODER_SESSION_TOKEN }}" "${{ vars.TEST_CODER_URL }}" + + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + with: + limit-access-to-actor: true + github-token: ${{ secrets.GITHUB_TOKEN }} + if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} + + - name: Resolve issue branch and Drupal version from issue number + run: | + # Strip project prefix if the variable was set as e.g. "token-2648180" instead of "2648180" + ISSUE_NUMBER="${ISSUE_FORK#${CONTRIB_PROJECT}-}" + FORK_SLUG="${CONTRIB_PROJECT}-${ISSUE_NUMBER}" + + # Fetch issue branch from the contrib issue fork GitLab API + ISSUE_BRANCH=$(curl -sf \ + "https://git.drupalcode.org/api/v4/projects/issue%2F${FORK_SLUG}" \ + | jq -r '.default_branch') + if [[ -z "$ISSUE_BRANCH" || "$ISSUE_BRANCH" == "null" ]]; then + echo "ERROR: Could not resolve default branch for issue fork ${FORK_SLUG}" >&2 + exit 1 + fi + + # For contrib modules, field_issue_version is the MODULE version (e.g. "8.x-1.x" + # for token module 1.x), not the Drupal core version. We cannot reliably infer + # Drupal core version from it. Default to 11; override via CONTRIB_TEST_DRUPAL_VERSION. + DRUPAL_VERSION="${CONTRIB_TEST_DRUPAL_VERSION:-11}" + + echo "Resolved: fork=${FORK_SLUG} branch=$ISSUE_BRANCH drupal_version=$DRUPAL_VERSION" + echo "ISSUE_NUMBER=$ISSUE_NUMBER" >> "$GITHUB_ENV" + echo "ISSUE_BRANCH=$ISSUE_BRANCH" >> "$GITHUB_ENV" + echo "ISSUE_FORK_VERSION=$DRUPAL_VERSION" >> "$GITHUB_ENV" + + - name: Copy VERSION into template directory + run: cp VERSION drupal-contrib/VERSION + + - name: Push template (inactive) + run: | + coder templates push drupal-contrib \ + --directory drupal-contrib \ + --activate=false \ + --name cc-${{ github.run_number }}-${{ github.run_attempt }} \ + --yes \ + --variable workspace_image_registry=index.docker.io/ddev/coder-ddev + + - name: Create workspace + run: | + coder create ${{ env.WORKSPACE_NAME }} \ + --template drupal-contrib \ + --template-version cc-${{ github.run_number }}-${{ github.run_attempt }} \ + --parameter "vscode_extensions=[]" \ + --parameter project_name=${{ env.CONTRIB_PROJECT }} \ + --parameter project_type=module \ + --parameter drupal_version=${{ env.ISSUE_FORK_VERSION }} \ + --parameter issue_fork=${{ env.ISSUE_NUMBER }} \ + --parameter issue_branch=${{ env.ISSUE_BRANCH }} \ + --parameter install_profile=minimal \ + --parameter share_drupal_site=owner \ + --yes + + - name: Verify workspace — agent connected + run: coder ssh ${{ env.WORKSPACE_NAME }} --wait=yes -- echo "Agent connected" + + - name: Verify workspace — Docker daemon running + run: coder ssh ${{ env.WORKSPACE_NAME }} -- docker ps + + - name: Verify workspace — DDEV installed + run: coder ssh ${{ env.WORKSPACE_NAME }} -- ddev --version + + - name: Verify workspace — project repo cloned + run: coder ssh ${{ env.WORKSPACE_NAME }} -- test -d /home/coder/${{ env.CONTRIB_PROJECT }}/.git + + - name: Verify workspace — Drupal web root exists + run: coder ssh ${{ env.WORKSPACE_NAME }} -- test -f /home/coder/${{ env.CONTRIB_PROJECT }}/web/index.php + + - name: Verify workspace — Drush DB connected + run: | + coder ssh ${{ env.WORKSPACE_NAME }} -- env -C /home/coder/${{ env.CONTRIB_PROJECT }} ddev drush status --fields=db-status \ + | grep -i connected + + - name: Dump setup status + if: always() + run: | + coder ssh ${{ env.WORKSPACE_NAME }} -- cat /home/coder/SETUP_STATUS.txt || true + echo "--- last 30 lines of setup log ---" + coder ssh ${{ env.WORKSPACE_NAME }} -- tail -30 /tmp/drupal-setup.log || true + + - name: Verify workspace — issue branch checked out + run: | + CURRENT=$(coder ssh ${{ env.WORKSPACE_NAME }} -- git -C /home/coder/${{ env.CONTRIB_PROJECT }} branch --show-current | tr -d '\r\n') + echo "Current branch: $CURRENT Expected: ${{ env.ISSUE_BRANCH }}" + [[ "$CURRENT" == "${{ env.ISSUE_BRANCH }}" ]] || { echo "ERROR: wrong branch" >&2; exit 1; } + + - name: Record expected host directory + run: | + OWNER=$(coder whoami --output json | jq -r 'if type == "array" then .[0].username else .username end') + echo "OWNER=$OWNER" >> "$GITHUB_ENV" + echo "HOST_DIR=/coder-workspaces/${OWNER}-${{ env.WORKSPACE_NAME }}" >> "$GITHUB_ENV" + + - name: Verify workspace — Drupal site externally accessible + run: | + CODER_DOMAIN="${{ vars.TEST_CODER_URL }}" + CODER_DOMAIN="${CODER_DOMAIN#https://}" + SITE_URL="https://drupal-site--${{ env.WORKSPACE_NAME }}--${OWNER}.${CODER_DOMAIN}" + echo "Checking $SITE_URL" + HTTP_STATUS=$(curl -sL --max-time 30 --retry 3 --retry-delay 5 \ + -H "Coder-Session-Token: ${TEST_CODER_SESSION_TOKEN}" \ + -o /tmp/drupal-response.html \ + -w "%{http_code}" \ + "$SITE_URL") + echo "HTTP status: $HTTP_STATUS" + if [[ ! "$HTTP_STATUS" =~ ^[23] ]]; then + echo "ERROR: unexpected HTTP status $HTTP_STATUS" >&2 + head -50 /tmp/drupal-response.html >&2 || true + exit 1 + fi + if ! grep -qi "log in" /tmp/drupal-response.html; then + echo "ERROR: response does not contain 'log in' — Drupal may not have started" >&2 + head -50 /tmp/drupal-response.html >&2 + exit 1 + fi + echo "OK: Drupal site is accessible and shows login page" + + - name: Delete workspace + if: always() + run: coder delete ${{ env.WORKSPACE_NAME }} --yes || true + + - name: Verify host directory removed + if: always() + run: | + if [[ -z "${HOST_DIR:-}" ]]; then + echo "HOST_DIR not set — workspace may not have been created, skipping" + exit 0 + fi + if [[ -d "$HOST_DIR" ]]; then + echo "ERROR: Host directory was not removed by destroy provisioner: $HOST_DIR" >&2 + ls -la "$HOST_DIR" >&2 || true + exit 1 + fi + echo "OK: host directory removed: $HOST_DIR" + + - name: Archive CI template version + if: always() + run: coder templates versions archive drupal-contrib cc-${{ github.run_number }}-${{ github.run_attempt }} --yes || true diff --git a/.github/workflows/drupal-integration-test.yml b/.github/workflows/drupal-integration-test.yml index 67192b7..1e97fb2 100644 --- a/.github/workflows/drupal-integration-test.yml +++ b/.github/workflows/drupal-integration-test.yml @@ -31,6 +31,10 @@ name: Drupal Integration Tests env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + on: schedule: - cron: '0 4 * * *' @@ -60,7 +64,7 @@ jobs: run: shell: bash -euo pipefail {0} env: - WORKSPACE_NAME: ci-drupal-${{ matrix.drupal_version }}-${{ github.run_id }} + WORKSPACE_NAME: cd-${{ matrix.drupal_version }}-${{ github.run_number }}-${{ github.run_attempt }} CI: "true" DDEV_NONINTERACTIVE: "true" NO_COLOR: "1" @@ -110,6 +114,7 @@ jobs: --parameter issue_fork= \ --parameter issue_branch= \ --parameter install_profile=minimal \ + --parameter share_drupal_site=owner \ --yes - &verify-agent @@ -200,7 +205,7 @@ jobs: run: shell: bash -euo pipefail {0} env: - WORKSPACE_NAME: ci-drupal-fork-${{ github.run_id }} + WORKSPACE_NAME: cd-fork-${{ github.run_number }}-${{ github.run_attempt }} CI: "true" DDEV_NONINTERACTIVE: "true" NO_COLOR: "1" @@ -261,7 +266,7 @@ jobs: coder templates push drupal-core \ --directory drupal-core \ --activate=false \ - --name ci-${{ github.run_id }} \ + --name cd-${{ github.run_number }}-${{ github.run_attempt }} \ --yes \ --variable workspace_image_registry=index.docker.io/ddev/coder-ddev \ --variable cache_path=/tmp/ci-no-cache @@ -270,12 +275,13 @@ jobs: run: | coder create ${{ env.WORKSPACE_NAME }} \ --template drupal-core \ - --template-version ci-${{ github.run_id }} \ + --template-version cd-${{ github.run_number }}-${{ github.run_attempt }} \ --parameter "vscode_extensions=[]" \ --parameter drupal_version=${{ env.ISSUE_FORK_VERSION }} \ --parameter issue_fork=${{ env.ISSUE_FORK }} \ --parameter issue_branch=${{ env.ISSUE_BRANCH }} \ --parameter install_profile=minimal \ + --parameter share_drupal_site=owner \ --yes - name: Verify workspace — agent connected @@ -350,4 +356,4 @@ jobs: - name: Archive CI template version if: always() - run: coder templates versions archive drupal-core ci-${{ github.run_id }} --yes || true + run: coder templates versions archive drupal-core cd-${{ github.run_number }}-${{ github.run_attempt }} --yes || true diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index c7a908c..3db9a02 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -44,7 +44,13 @@ env: on: push: branches: [main] + paths-ignore: + - 'drupal-core/**' + - 'drupal-contrib/**' pull_request: + paths-ignore: + - 'drupal-core/**' + - 'drupal-contrib/**' schedule: - cron: '0 3 * * *' workflow_dispatch: @@ -82,8 +88,8 @@ jobs: run: shell: bash -euo pipefail {0} env: - CI_TAG: ${{ github.run_id }}-${{ github.run_attempt }} - WORKSPACE_NAME: ci-${{ matrix.ws_name }}-${{ github.run_id }}-${{ github.run_attempt }} + CI_TAG: ${{ github.run_number }}-${{ github.run_attempt }} + WORKSPACE_NAME: ci-${{ matrix.ws_name }}-${{ github.run_number }}-${{ github.run_attempt }} CI: "true" DDEV_NONINTERACTIVE: "true" NO_COLOR: "1" diff --git a/CLAUDE.md b/CLAUDE.md index 0504e2c..489f80b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,25 @@ This project provides a Coder v2+ template for DDEV-based development environmen - Use `jq` (not `python3 -m json.tool`) for JSON pretty-printing and querying +## Before Pushing / Pre-push Checklist + +Run these before every push to avoid CI failures: + +```bash +# Terraform formatting (CI runs terraform fmt -check -recursive) +terraform fmt -recursive + +# Terraform validation for each template you touched +terraform -chdir=drupal-core init -backend=false && terraform -chdir=drupal-core validate +terraform -chdir=drupal-contrib init -backend=false && terraform -chdir=drupal-contrib validate + +# Terraform tests (plan-level, no real infrastructure) +terraform -chdir=drupal-core test +terraform -chdir=drupal-contrib test +``` + +`terraform fmt -recursive` must be run from the repo root. It is non-destructive (rewrites in place) and the CI check fails with exit code 3 if any file is not formatted. + ## Working with Coder Workspaces via SSH After running `coder config-ssh --yes`, workspaces are available as SSH hosts named `.coder`. Use `scp` to copy files in or out, then `ssh` to execute scripts non-interactively: @@ -39,6 +58,10 @@ ssh mp1.coder ddev list When running commands via `coder ssh -- ...` or piped heredocs, the PTY allocation causes interactive prompts and pipe-stall issues. Writing a script to `/tmp/` and executing it via `ssh workspace.coder bash /tmp/script.sh` is reliable for multi-step operations. +**CI scripting rule**: In GitHub Actions, for any `coder ssh` invocation that does more than a single trivial command, push a script file and run it — do not use `bash -c "..."`. Single commands with standard flags (e.g. `git -C /path branch --show-current`) are fine inline. Anything with `&&`, conditionals, or multiple statements belongs in a script file under the template's `scripts/` directory. + +To run a command in a specific directory without a shell wrapper, use `env -C ` (available on Ubuntu 24.04 via GNU coreutils): `coder ssh ws -- env -C /home/coder/myproject ddev drush status` + ## Essential Commands ### Template Management diff --git a/Makefile b/Makefile index 48b72c6..da64d9d 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ DOCKERFILE_DIR := image DOCKERFILE := $(DOCKERFILE_DIR)/Dockerfile # Template directories (name == directory name == Coder template name) -TEMPLATES := user-defined-web drupal-core freeform +TEMPLATES := user-defined-web drupal-core drupal-contrib freeform # Host path to the drupal-core seed cache (bind-mounted read-only into workspaces). # This path is specific to the server where the template is deployed. @@ -30,6 +30,7 @@ IMAGE_LATEST := $(IMAGE_NAME):latest TEMPLATE_VARS_user-defined-web := --variable workspace_image_registry=index.docker.io/$(IMAGE_NAME) TEMPLATE_VARS_drupal-core := --variable workspace_image_registry=index.docker.io/$(IMAGE_NAME) \ --variable cache_path=$(DRUPAL_CACHE_PATH) +TEMPLATE_VARS_drupal-contrib := --variable workspace_image_registry=index.docker.io/$(IMAGE_NAME) TEMPLATE_VARS_freeform := --variable workspace_image_registry=index.docker.io/$(IMAGE_NAME) # Per-template display metadata set via `coder templates edit` after push @@ -37,6 +38,8 @@ TEMPLATE_VARS_freeform := --variable workspace_image_registry=index.dock TEMPLATE_EDIT_user-defined-web := --display-name "DDEV Web Workspace" TEMPLATE_EDIT_drupal-core := --display-name "Drupal Core Development" \ --description "Drupal core dev environment: full DDEV stack, core clone, Umami demo site. Ready in about a minute." +TEMPLATE_EDIT_drupal-contrib := --display-name "Drupal Contrib Development" \ + --description "Drupal contrib module/theme dev: clone any drupal.org project, optional issue branch checkout. Ready in 5-10 minutes." TEMPLATE_EDIT_freeform := --display-name "DDEV Freeform (Traefik)" --default-ttl 24h # Shared recipe for pushing any template (call with template name as argument) @@ -167,12 +170,16 @@ push-template-user-defined-web: ## Push user-defined-web template to Coder push-template-drupal-core: ## Push drupal-core template to Coder $(call push_template,drupal-core) +.PHONY: push-template-drupal-contrib +push-template-drupal-contrib: ## Push drupal-contrib template to Coder + $(call push_template,drupal-contrib) + .PHONY: push-template-freeform push-template-freeform: ## Push freeform template to Coder $(call push_template,freeform) .PHONY: push-all-templates -push-all-templates: push-template-user-defined-web push-template-drupal-core push-template-freeform ## Push all templates to Coder (no image build) +push-all-templates: push-template-user-defined-web push-template-drupal-core push-template-drupal-contrib push-template-freeform ## Push all templates to Coder (no image build) @echo "All templates pushed!" # --- Deploy targets --- @@ -189,6 +196,10 @@ deploy-user-defined-web-no-cache: build-and-push-no-cache push-template-user-def deploy-drupal-core: push-template-drupal-core ## Deploy drupal-core template (uses existing image) @echo "Deployment of drupal-core complete!" +.PHONY: deploy-drupal-contrib +deploy-drupal-contrib: push-template-drupal-contrib ## Deploy drupal-contrib template (uses existing image) + @echo "Deployment of drupal-contrib complete!" + .PHONY: deploy-freeform deploy-freeform: push-template-freeform ## Deploy freeform template (uses existing image) @echo "Deployment of freeform complete!" diff --git a/README.md b/README.md index 4a1658f..676e203 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Fully automated Drupal core development environment, including issue fork suppor - **Setup**: Automatic (Drupal core cloned and installed, ~30s with seed cache) - **Use Case**: Drupal core development, contribution, patch testing - **Template Directory**: `drupal-core/` -- **Issue Picker**: [start.coder.ddev.com/drupal-issue](https://start.coder.ddev.com/drupal-issue) — paste any drupal.org issue URL to launch a workspace with the issue branch pre-checked-out +- **Issue Picker**: [start.coder.ddev.com/drupal-issue](https://start.coder.ddev.com/drupal-issue) — paste any drupal.org issue URL; the picker auto-detects core vs. contrib and routes to the right template - **Includes**: - Pre-cloned Drupal core (main branch by default) - Issue fork checkout with automatic Composer dependency resolution (Drupal 10, 11, and 12/main) @@ -136,20 +136,43 @@ Fully automated Drupal core development environment, including issue fork suppor coder create --template drupal-core my-drupal-dev ``` +### drupal-contrib (Drupal Contrib Development) + +Automated environment for developing Drupal contrib modules and themes, with optional issue branch support. + +- **Setup**: Automatic (module/theme cloned, Drupal installed as dev dependency via `ddev-drupal-contrib`, ~5-10 min) +- **Use Case**: Contrib module/theme development, issue queue work on any drupal.org project +- **Template Directory**: `drupal-contrib/` +- **Issue Picker**: [start.coder.ddev.com/drupal-issue](https://start.coder.ddev.com/drupal-issue) — paste a drupal.org issue or project URL; auto-detects contrib +- **Includes**: + - Any drupal.org project cloned (modules and themes) + - Issue fork checkout via `issue_fork` + `issue_branch` parameters + - DDEV with [`ddev-drupal-contrib`](https://github.com/ddev/ddev-drupal-contrib) addon (adds `ddev phpunit`, `ddev phpcs`, `ddev phpstan`) + - Drupal installed as a dev dependency (module/theme auto-symlinked into web root) + +**Create workspace:** +```bash +coder create --template drupal-contrib my-contrib-dev \ + --parameter project_name=token \ + --parameter drupal_version=11 +``` + ### Choosing a Template - Use **user-defined-web** for: - - Contrib module development - - Site building + - Site building and custom site projects - General Drupal/PHP projects - Maximum flexibility - Use **drupal-core** for: - - Drupal core patches - - Core issue queue work + - Drupal core patches and issue queue work - Testing Drupal core changes - Learning Drupal internals +- Use **drupal-contrib** for: + - Contrib module or theme development + - Working on drupal.org contrib issue queues + ## Usage Create a new workspace using your chosen template: @@ -160,6 +183,10 @@ coder create --template user-defined-web # Drupal core development environment coder create --template drupal-core + +# Drupal contrib module/theme development +coder create --template drupal-contrib \ + --parameter project_name=token --parameter drupal_version=11 ``` **Access your project:** diff --git a/docs/drupal-contrib-picker.html b/docs/drupal-contrib-picker.html new file mode 100644 index 0000000..42691d8 --- /dev/null +++ b/docs/drupal-contrib-picker.html @@ -0,0 +1,924 @@ + + + + + + Drupal Issue Picker (Beta) — DDEV Coder Workspaces + + + + + + + +
+ DDEV Coder Workspaces + + + Drupal Issue Picker +
+ + +
+

Drupal Issue Picker BETA

+

Enter a Drupal issue URL or number to launch a workspace. Also accepts a project URL or machine name for plain contrib development (no specific issue). Targets staging-coder.ddev.com — add ?coder=https://coder.ddev.com to use production.

+ +
+ + + + +
+ + +
+ + +
+
+ Issue + + +
+ +
+ +
+ + +
+ +
+ + +
demo_umami uses a pre-built database snapshot. Issue fork workspaces always run a full site install regardless of profile.
+
+ +
+ + +
Auto-detected from the issue. Sets the DDEV project type (drupal10/11/12).
+
+ +
+ + +
Must be lowercase letters, numbers, and hyphens only.
+
+
+ + +
+ + +
+
+ Contrib Project + + drupal-contrib template +
+ +
+
+ + +
Sets the Drupal version installed as a dev dependency.
+
+ +
+ + +
minimal is recommended for contrib development.
+
+ +
+ + +
Must be lowercase letters, numbers, and hyphens only.
+
+
+ + +
+ + +
+
+ Could not fetch issue data automatically. Enter values manually below. +
+ +
+
+ + +
For core: drupal-{issue}. For contrib: {module}-{issue}
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+ +
+

Notes

+
    +
  • Requires a GitHub account — sign in at staging-coder.ddev.com first
  • +
  • Core issues use the drupal-core template; contrib issues use the drupal-contrib template — detected automatically
  • +
  • Issue fork workspaces always run a full ddev drush si (branch code may differ from the cached database)
  • +
  • The demo_umami fast path (about a minute) is only used for standard Drupal core without an issue fork
  • +
  • Issue fork must exist on git.drupalcode.org — click "Get push access" on the issue page to create one
  • +
  • Contrib dev without an issue: enter a project URL like drupal.org/project/token or just the machine name token
  • +
+
+ +
+

Feedback & Community

+

Any problems or friction? Create an issue at ddev/coder-ddev or join us for conversation on Drupal Slack #ddev or DDEV Discord.

+
+
+ + + + + diff --git a/docs/index.html b/docs/index.html index dc0b28b..cd3756b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -291,11 +291,18 @@

Drupal issue development

-

Drupal Core Issue Picker

-

Enter a Drupal.org core issue number — auto-selects branch and launches a workspace with the issue fork checked out

+

Drupal Issue Picker

+

Enter a Drupal.org issue URL or number — launches a Drupal core workspace with the issue fork checked out

Open Picker
+
+
+

Drupal Issue Picker BETA

+

New unified picker — handles both Drupal core and contrib modules/themes. Targets staging-coder.ddev.com. Try it out and report any issues.

+
+ Open Beta Picker +
diff --git a/docs/user/quickstart.md b/docs/user/quickstart.md index 1929c3a..8b2a31e 100644 --- a/docs/user/quickstart.md +++ b/docs/user/quickstart.md @@ -66,7 +66,7 @@ ddev ssh ## Working on a Drupal issue -The fastest way: use the **[Drupal Issue Picker](https://start.coder.ddev.com/drupal-issue)**. Paste a drupal.org issue URL or bare issue number — it fetches the available branches, lets you pick one, and opens a pre-configured workspace with the issue branch already checked out and all Composer dependencies resolved for that branch. +The fastest way: use the **[Drupal Issue Picker](https://start.coder.ddev.com/drupal-issue)**. Paste a drupal.org issue URL, issue number, or project URL — it auto-detects whether the issue is for Drupal core or a contrib module/theme, fetches the available branches, and opens a pre-configured workspace with the issue branch already checked out. Entering a project URL (e.g. `drupal.org/project/token`) or bare machine name launches a plain contrib dev workspace without a specific issue. When working on an issue, the workspace surfaces issue info in several places: diff --git a/drupal-contrib/.terraform.lock.hcl b/drupal-contrib/.terraform.lock.hcl new file mode 100644 index 0000000..483d65d --- /dev/null +++ b/drupal-contrib/.terraform.lock.hcl @@ -0,0 +1,64 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/coder/coder" { + version = "2.16.0" + constraints = ">= 2.5.0, >= 2.13.0" + hashes = [ + "h1:GZ71aeLqkBjTfALHYIf9dWF4WDnRALPDMU5++8GY0Y8=", + "zh:04c070bc17816ff4fb785a57c5d217c4c81f0c564cc6634a0c635e079fb68393", + "zh:15b70d49e8a1fcab72ec9497ab7af90094a476bee8039a10aecdae2b05e644c1", + "zh:24a731d6f94e3711a6f8f88995a0389e4efc96f926fcd97b5eed2c21e0481302", + "zh:4ebaa6775c9c8f85e0a121c395cac4f759ecfa01242dc70895023f59eaa30ede", + "zh:50ffd0ccfdd9ebc2b4e98316ba89178200d2a0beb58b557c29ebc7d63b7bb8a6", + "zh:5b8b30e28e1cecff15b1c5311d091b0d9d932ce91b8e49962a6b9ef4dd330f4f", + "zh:6255fabf82a4baf50eab6c5f90f60daaeb22f522338ae0fcfa1b28eff3386ddc", + "zh:8eeb2c74e804087c3959d37c5cf773ec00beba431d3a312518aea5934bc8c73b", + "zh:9d68e6aa6a26a320113e269b116c7e376e5de873c1bd152ef74460c0d3b22f44", + "zh:b28ee23e53c98b948d211c7cf78a628cf513f84e5b340b882fa519098466fb94", + "zh:b2a5df2dc02db5da8b08b18237f748c1b9ef9c0d67061847667c9999dcfa7f1a", + "zh:b38f6b2abb2b860b31528546bc8beae00dcd95ba35a7db851abee77ad5d252d8", + "zh:ea90af990761df5687b35a3e3f981d773a67d02e342fb836ec68375df5c2ba08", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.4" + hashes = [ + "h1:L5V05xwp/Gto1leRryuesxjMfgZwjb7oool4WS1UEFQ=", + "zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43", + "zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a", + "zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991", + "zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f", + "zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e", + "zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615", + "zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442", + "zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5", + "zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f", + "zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f", + ] +} + +provider "registry.terraform.io/kreuzwerker/docker" { + version = "3.9.0" + constraints = "~> 3.0" + hashes = [ + "h1:p65AjYSOmmHPjKkIYlEnxSMyrprTHKf1qBzKhCnCtG8=", + "zh:0ead8281830e9b9496651282235d9a139ba1b1b6ff79e395eb8c78658dc446b9", + "zh:0f17d37d8d3872df3fb75c68b5272e0c981343f53b506a9675b4405191edd3ef", + "zh:11d50b37323874427c6d2a08b737d3c7707c8301fdd236c94485cf2828d0b14b", + "zh:32f6f9b847446054e2db3d72886ef2f1d1aa51a6d0dac42340b07dad18e3f28f", + "zh:5ea5c67668b5dcbda560dc6104b788a9bfc974d52f02f7886889b77cc0e5d248", + "zh:5fb19a0b07edc344cd3ddeeb9cfb3d183089deb7a6a94a7b22a583aa1712596b", + "zh:602a7ece444e2a142ec5245abb98e7a1a990a68afae2df63b6c85ec084f0c5d7", + "zh:693dce278524ad8a6d6c9dd7a01bcd63bb85189639198f8d0b044ab0e5099401", + "zh:72e9911568103576c6a78fa38841cfd45eeb88ad22a2c649eb140a377a5b3c26", + "zh:956b62b6857cbb467b50158601f01b1203daa34cbd447dcc7f044c327e878b68", + "zh:9d372bac0d4479868b34485fb4966ba7bb525938f818b6a625f4977004ea83f9", + "zh:e06658a51427f9f53dbdb06263406fc1bc56d1a4fb5e7eb660d7cdfc22f596bd", + "zh:eee38dadf672b946419af25160eae7c03fc2afbb14f39f2f1d2a7404d647e2f7", + ] +} diff --git a/drupal-contrib/README.md b/drupal-contrib/README.md new file mode 100644 index 0000000..f482869 --- /dev/null +++ b/drupal-contrib/README.md @@ -0,0 +1,157 @@ +# Drupal Contrib Development Template + +A Coder workspace template for developing Drupal contrib modules and themes, with full support for working on specific issue branches from drupal.org. + +## Features + +- **Full DDEV environment** — DDEV with the official [`ddev-drupal-contrib`](https://github.com/ddev/ddev-drupal-contrib) addon +- **Any contrib project** — Clone any module or theme from git.drupalcode.org by machine name +- **Issue branch support** — Automatically fetch and checkout issue forks from drupal.org +- **Symlinked module** — Module/theme is symlinked into the Drupal web root automatically (`ddev symlink-project`) +- **Dev tools included** — PHPUnit, PHPStan, PHPCS, ESLint, Stylelint via DDEV commands +- **Multiple Drupal versions** — Supports Drupal 10.x, 11.x, and 12.x as the dev dependency +- **VS Code for Web** — Opens at your module root; PHP Debug, Intelephense, and more + +## Quick Start + +### Via the Issue Picker (recommended) + +Visit the Drupal Issue Picker at your Coder instance. Paste any drupal.org project or issue URL — the picker detects contrib projects automatically and routes to this template. + +### Via CLI + +```bash +# Plain development on a contrib module (HEAD) +coder create --template drupal-contrib my-workspace \ + --parameter project_name=token \ + --parameter drupal_version=11 \ + --yes + +# Working on a specific issue +coder create --template drupal-contrib issue-3568144 \ + --parameter project_name=views \ + --parameter issue_fork=3568144 \ + --parameter issue_branch=3568144-fix-something-2.x \ + --parameter drupal_version=11 \ + --yes +``` + +## Setup Time + +| Scenario | Time | +|----------|------| +| First workspace creation | 5–10 min (downloads Drupal + dependencies) | +| Subsequent starts | ~1 min (restarts existing containers) | + +## Workspace Structure + +``` +/home/coder/{project_name}/ # Module/theme git repo +├── {source files} +├── composer.json # Module's own composer.json (unchanged) +├── composer.contrib.json # Temp file by ddev poser (gitignored) +├── .ddev/ # DDEV configuration +├── vendor/ # Installed dependencies +└── web/ # Drupal docroot + └── modules/custom/{name} -> /home/coder/{name} (symlink) + # (or themes/custom/{name} for themes) +``` + +The module/theme repo is the project root. Drupal core is installed as a dev dependency in `web/`. A symlink makes Drupal aware of your module without copying files. + +## Workspace Parameters + +| Parameter | Default | Notes | +|-----------|---------|-------| +| `project_name` | — | Drupal.org machine name (required, not mutable after creation) | +| `project_type` | `module` | `module` or `theme` (not mutable after creation) | +| `drupal_version` | `11` | `10`, `11`, or `12` | +| `issue_fork` | — | Issue number; empty = plain HEAD | +| `issue_branch` | — | Branch name; empty = default branch HEAD | +| `install_profile` | `minimal` | `minimal`, `standard`, or `demo_umami` | + +## Access + +After setup, access your Drupal site through: + +1. **Coder dashboard** — Click the "Drupal Site" app button +2. **CLI** — `coder ssh ` then `ddev launch` + +**Admin credentials:** admin / admin + +## Development Commands + +```bash +# Inside the workspace (via coder ssh or VS Code terminal): + +ddev launch # Show site URL and one-time login link +ddev describe # Show project details and URLs + +# Testing and quality (from ddev-drupal-contrib addon) +ddev phpunit # Run PHPUnit tests for your module +ddev phpcs # Check Drupal coding standards +ddev phpcbf # Auto-fix coding standard issues +ddev phpstan # Run PHPStan static analysis +ddev eslint # JavaScript linting +ddev stylelint # CSS linting + +# Drupal management +ddev drush en {module} # Enable a module +ddev drush cr # Rebuild cache +ddev drush uli # One-time login link +ddev drush status # Check status + +# DDEV management +ddev start / ddev stop # Start or stop containers +ddev logs # View container logs +ddev ssh # SSH into the web container +ddev restart # Restart containers +``` + +## Working on an Issue + +1. Find an issue at `drupal.org/project/{module}/issues` +2. Use the Drupal Issue Picker on your Coder instance to launch a workspace, OR pass `issue_fork` and `issue_branch` parameters manually +3. The issue branch is automatically checked out in `/home/coder/{project_name}` +4. Make your changes, run tests, commit and push + +```bash +# View your working branch +git -C ~/views branch --show-current + +# Run tests for your module +cd ~/views +ddev phpunit web/modules/custom/views/tests + +# Check coding standards +ddev phpcs web/modules/custom/views +``` + +## Themes + +When `project_type=theme`, the symlink is placed at `web/themes/custom/{name}` instead of `web/modules/custom/{name}`. Enable your theme with: + +```bash +ddev drush theme:enable {theme_name} -y +ddev drush config-set system.theme default {theme_name} -y +``` + +## Requirements + +- Coder v2.13+ +- Sysbox runtime on agent nodes (`sysbox-runc`) +- Network access to git.drupalcode.org and packagist.org + +## Troubleshooting + +**Clone failed:** Check that `project_name` is a valid drupal.org machine name. Verify it exists at `git.drupalcode.org/project/{name}`. + +**Issue fork not found:** The issue fork URL (`git.drupalcode.org/issue/{module}-{issue}`) may not exist yet. Create it from the issue page on drupal.org by clicking "Create issue fork." + +**ddev poser failed:** Check setup logs with `cat /tmp/drupal-setup.log`. The module's `composer.json` may need adjustments for the target Drupal version. + +**Module not found after symlink:** Run `ddev restart` to retrigger `ddev symlink-project`. Then `ddev drush en {module_name}`. + +**Full setup log:** `cat /tmp/drupal-setup.log` + +**Setup status summary:** `cat ~/SETUP_STATUS.txt` diff --git a/drupal-contrib/VERSION b/drupal-contrib/VERSION new file mode 100644 index 0000000..1811f96 --- /dev/null +++ b/drupal-contrib/VERSION @@ -0,0 +1 @@ +v0.4 diff --git a/drupal-contrib/scripts/create-test-workspaces.sh b/drupal-contrib/scripts/create-test-workspaces.sh new file mode 100755 index 0000000..3553a18 --- /dev/null +++ b/drupal-contrib/scripts/create-test-workspaces.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# create-test-workspaces.sh — batch create/check/delete drupal-contrib test workspaces +# +# Usage: +# ./create-test-workspaces.sh # create all test workspaces +# ./create-test-workspaces.sh --check # show logs for existing workspaces +# ./create-test-workspaces.sh --delete # delete all test workspaces +# +# Format: PROJECT:DRUPAL_VERSION[:ISSUE:BRANCH] +# Examples: +# token:11 +# views:11:3568144:3568144-some-fix-2.x + +set -euo pipefail + +TEMPLATE="drupal-contrib" + +# Test matrix: PROJECT:DRUPAL_VERSION or PROJECT:DRUPAL_VERSION:ISSUE:BRANCH +TESTS=( + "token:11" + "views:11" + "pathauto:11" +) + +MODE="${1:-create}" + +workspace_name() { + local project="$1" + local drupal_version="$2" + local issue="${3:-}" + if [ -n "$issue" ]; then + echo "t-${project}-issue-${issue}" + else + echo "t-${project}-d${drupal_version}" + fi +} + +for spec in "$${TESTS[@]}"; do + IFS=':' read -r project drupal_version issue branch <<< "$spec" + issue="${issue:-}" + branch="${branch:-}" + ws=$(workspace_name "$project" "$drupal_version" "$issue") + + case "$MODE" in + --delete) + echo "--- Deleting workspace $ws ---" + coder delete "$ws" --yes 2>/dev/null || echo " (not found or already deleted)" + ;; + --check) + echo "--- Checking workspace $ws ---" + coder ssh "$ws" -- bash -c "cat ~/SETUP_STATUS.txt 2>/dev/null || echo 'SETUP_STATUS.txt not found'" 2>/dev/null || echo " (workspace not accessible)" + echo "" + ;; + *) + echo "--- Creating workspace $ws (project=$project drupal=$drupal_version issue=${issue:-none}) ---" + PARAMS="--parameter project_name=$project --parameter drupal_version=$drupal_version" + if [ -n "$issue" ]; then + PARAMS="$PARAMS --parameter issue_fork=$issue" + fi + if [ -n "$branch" ]; then + PARAMS="$PARAMS --parameter issue_branch=$branch" + fi + # shellcheck disable=SC2086 + coder create --template "$TEMPLATE" "$ws" $PARAMS --yes || echo " (creation failed)" + ;; + esac +done + +echo "Done." diff --git a/drupal-contrib/scripts/test-issue-branches.sh b/drupal-contrib/scripts/test-issue-branches.sh new file mode 100755 index 0000000..f8bde7f --- /dev/null +++ b/drupal-contrib/scripts/test-issue-branches.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash +# test-issue-branches.sh — Test drupal-contrib workspace setup locally +# using ddev-drupal-contrib (ddev poser + drush si + module enable). +# +# Usage: +# bash drupal-contrib/scripts/test-issue-branches.sh # run all default tests +# bash drupal-contrib/scripts/test-issue-branches.sh PROJECT:DRUPAL_VERSION +# bash drupal-contrib/scripts/test-issue-branches.sh PROJECT:DRUPAL_VERSION:ISSUE:BRANCH +# +# Format: PROJECT:DRUPAL_VERSION[:ISSUE[:BRANCH]] +# PROJECT — drupal.org machine name (e.g. token, pathauto) +# DRUPAL_VERSION — 10, 11, or 12 +# ISSUE — optional issue NID (e.g. 3568144); omit for plain dev +# BRANCH — optional branch; required when ISSUE is set +# +# Requirements: ddev, git, jq, curl +# Projects are created in ~/tmp/contrib-test-[-]/ and left in +# place so you can inspect or restart them. To clean up: +# cd ~/tmp/contrib-test- && ddev delete -Oy && cd && rm -rf ~/tmp/contrib-test- + +DEFAULT_TESTS=( + "token:11" + "pathauto:11" + "token:10" +) + +if [ $# -gt 0 ]; then + TESTS=("$@") +else + TESTS=("${DEFAULT_TESTS[@]}") +fi + +declare -A RESULTS +declare -A DURATIONS + +log() { echo "[$(date '+%H:%M:%S')] $*"; } + +for SPEC in "${TESTS[@]}"; do + IFS=':' read -r PROJECT DRUPAL_VERSION ISSUE BRANCH <<< "$SPEC" + DRUPAL_VERSION="${DRUPAL_VERSION:-11}" + ISSUE="${ISSUE:-}" + BRANCH="${BRANCH:-}" + + DIR_KEY="${PROJECT}${ISSUE:+-$ISSUE}" + PROJECT_DIR="$HOME/tmp/contrib-test-$DIR_KEY" + START=$SECONDS + + log "========================================================" + if [ -n "$ISSUE" ]; then + log "Testing $PROJECT (D$DRUPAL_VERSION) issue #$ISSUE branch: $BRANCH" + else + log "Testing $PROJECT (D$DRUPAL_VERSION) plain dev" + fi + log "Project dir: $PROJECT_DIR" + log "========================================================" + + mkdir -p "$PROJECT_DIR" + + # --- Clone project if needed --- + if [ ! -d "$PROJECT_DIR/$PROJECT/.git" ]; then + log "Cloning $PROJECT from git.drupalcode.org..." + if ! git clone "https://git.drupalcode.org/project/$PROJECT.git" \ + "$PROJECT_DIR/$PROJECT" 2>&1 | tail -5; then + log "ERROR: git clone failed" + RESULTS["$DIR_KEY"]="FAIL (git clone)" + DURATIONS["$DIR_KEY"]=$((SECONDS - START)) + continue + fi + else + log "Project already cloned — skipping" + fi + + cd "$PROJECT_DIR/$PROJECT" + + # --- Issue fork checkout --- + if [ -n "$ISSUE" ] && [ -n "$BRANCH" ]; then + REMOTE_NAME="issue-$ISSUE" + FORK_URL="https://git.drupalcode.org/issue/$PROJECT-$ISSUE.git" + log "Adding issue fork remote: $FORK_URL" + git remote remove "$REMOTE_NAME" 2>/dev/null || true + git remote add "$REMOTE_NAME" "$FORK_URL" + if ! git fetch "$REMOTE_NAME" 2>&1 | tail -3; then + log "ERROR: git fetch from issue remote failed" + RESULTS["$DIR_KEY"]="FAIL (git fetch)" + DURATIONS["$DIR_KEY"]=$((SECONDS - START)) + cd "$HOME" + continue + fi + if ! (git checkout -b "$BRANCH" "$REMOTE_NAME/$BRANCH" 2>&1 || \ + git checkout "$BRANCH" 2>&1); then + log "ERROR: branch checkout failed" + RESULTS["$DIR_KEY"]="FAIL (git checkout)" + DURATIONS["$DIR_KEY"]=$((SECONDS - START)) + cd "$HOME" + continue + fi + log "Checked out issue branch: $BRANCH" + fi + + # --- Configure DDEV if needed --- + if ! ddev describe >/dev/null 2>&1; then + log "Configuring DDEV (drupal$DRUPAL_VERSION, docroot=web)..." + ddev config \ + --project-name "contrib-test-$DIR_KEY" \ + --project-type "drupal$DRUPAL_VERSION" \ + --docroot web 2>&1 | tail -5 + log "Installing ddev-drupal-contrib addon..." + ddev add-on get ddev/ddev-drupal-contrib 2>&1 | tail -5 + + log "Starting DDEV..." + if ! ddev start 2>&1 | tail -10; then + log "ERROR: ddev start failed" + RESULTS["$DIR_KEY"]="FAIL (ddev start)" + DURATIONS["$DIR_KEY"]=$((SECONDS - START)) + cd "$HOME" + continue + fi + else + log "DDEV already configured — skipping init" + fi + + # --- Add drush as require-dev and run ddev poser --- + if [ ! -f web/index.php ]; then + log "Adding drush as require-dev..." + ddev exec composer require --dev drush/drush --no-update --no-interaction 2>&1 | tail -5 + + log "Running ddev poser..." + if ! ddev poser 2>&1 | tail -10; then + log "ERROR: ddev poser failed" + RESULTS["$DIR_KEY"]="FAIL (ddev poser)" + DURATIONS["$DIR_KEY"]=$((SECONDS - START)) + cd "$HOME" + continue + fi + + log "Restarting DDEV to trigger symlink-project..." + ddev restart 2>&1 | tail -5 + + log "Installing Drupal (minimal profile)..." + if ! ddev drush si minimal -y --account-pass=admin 2>&1 | tail -10; then + log "ERROR: drush si failed" + RESULTS["$DIR_KEY"]="FAIL (drush si)" + DURATIONS["$DIR_KEY"]=$((SECONDS - START)) + cd "$HOME" + continue + fi + + log "Enabling $PROJECT..." + if ! ddev drush en "$PROJECT" -y 2>&1; then + log "WARNING: could not enable $PROJECT (may need manual enable)" + fi + else + log "Drupal already installed — skipping install" + fi + + # --- Verify --- + log "Verifying DB connection..." + DB_STATUS=$(ddev drush status --fields=db-status 2>/dev/null | grep -i connected || echo "") + if [ -z "$DB_STATUS" ]; then + log "ERROR: drush status shows DB not connected" + RESULTS["$DIR_KEY"]="FAIL (db not connected)" + DURATIONS["$DIR_KEY"]=$((SECONDS - START)) + cd "$HOME" + continue + fi + + log "Checking module is enabled..." + MODULE_STATUS=$(ddev drush pm:list --status=enabled --type=module 2>/dev/null | grep -i "$PROJECT" || echo "") + if [ -z "$MODULE_STATUS" ]; then + log "WARNING: $PROJECT not found in enabled modules" + RESULTS["$DIR_KEY"]="PASS (module not enabled — may need manual enable)" + else + RESULTS["$DIR_KEY"]="PASS" + fi + + DURATIONS["$DIR_KEY"]=$((SECONDS - START)) + log "${RESULTS[$DIR_KEY]} — $DIR_KEY (${DURATIONS[$DIR_KEY]}s)" + cd "$HOME" +done + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +echo "" +echo "============================= SUMMARY =============================" +printf "%-30s %-8s %s\n" "Test" "Time" "Result" +printf "%-30s %-8s %s\n" "----" "----" "------" +for SPEC in "${TESTS[@]}"; do + IFS=':' read -r PROJECT DRUPAL_VERSION ISSUE BRANCH <<< "$SPEC" + DIR_KEY="${PROJECT}${ISSUE:+-$ISSUE}" + LABEL="${PROJECT} D${DRUPAL_VERSION:-11}${ISSUE:+ #$ISSUE}" + printf "%-30s %-8s %s\n" \ + "$LABEL" "${DURATIONS[$DIR_KEY]:-?}s" "${RESULTS[$DIR_KEY]:-unknown}" +done +echo "===================================================================" diff --git a/drupal-contrib/template.tf b/drupal-contrib/template.tf new file mode 100644 index 0000000..267df98 --- /dev/null +++ b/drupal-contrib/template.tf @@ -0,0 +1,1139 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.13" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 3.0" + } + } +} + +provider "docker" { + host = var.docker_host + + dynamic "registry_auth" { + for_each = var.registry_username != "" && var.registry_password != "" ? [1] : [] + content { + address = "https://index.docker.io/v1/" + username = var.registry_username + password = var.registry_password + } + } +} + +variable "docker_host" { + description = "Docker host socket path" + type = string + default = "unix:///var/run/docker.sock" +} + +variable "registry_username" { + description = "Username for GitLab Container Registry authentication" + type = string + default = "" + sensitive = true +} + +variable "registry_password" { + description = "Password/Token for GitLab Container Registry authentication" + type = string + default = "" + sensitive = true +} + +variable "image_version" { + description = "The version of the Docker image to use" + type = string + default = "v0.1" +} + +variable "docker_gid" { + description = "Docker group GID (must match host Docker group for socket access)" + type = number + default = 988 +} + +variable "docker_registry_mirror" { + description = "Optional Docker registry mirror URL override (e.g. http://your-host:5000). When empty, startup auto-detects a mirror at http://:5000 if reachable." + type = string + default = "" +} + +# Per-workspace user parameters +data "coder_parameter" "project_name" { + name = "project_name" + display_name = "Project Machine Name" + description = "The Drupal.org machine name of the contrib module or theme (e.g. 'views', 'token', 'pathauto'). Must match the git.drupalcode.org project slug." + type = "string" + mutable = false + order = 0 +} + +data "coder_parameter" "project_type" { + name = "project_type" + display_name = "Project Type" + description = "Whether this is a module or a theme. Controls the symlink path inside the Drupal web root." + type = "string" + default = "module" + mutable = false + order = 1 + option { + name = "Module" + value = "module" + } + option { + name = "Theme" + value = "theme" + } +} + +data "coder_parameter" "issue_fork" { + name = "issue_fork" + display_name = "Issue Fork" + description = "Drupal.org issue number (e.g. 3568144). Leave empty for plain HEAD development." + type = "string" + default = "" + mutable = true + order = 2 +} + +data "coder_parameter" "issue_branch" { + name = "issue_branch" + display_name = "Issue Branch" + description = "Issue branch to check out (e.g. 3568144-fix-something-2.x). Leave empty for HEAD." + type = "string" + default = "" + mutable = true + order = 3 +} + +data "coder_parameter" "drupal_version" { + name = "drupal_version" + display_name = "Drupal Version" + description = "Major Drupal version to install as the dev dependency. Match the version the issue targets." + type = "string" + default = "11" + mutable = true + order = 4 + option { + name = "11.x (stable)" + value = "11" + } + option { + name = "12.x (main branch)" + value = "12" + } + option { + name = "10.x (stable)" + value = "10" + } +} + +data "coder_parameter" "install_profile" { + name = "install_profile" + display_name = "Install Profile" + description = "Drupal install profile. 'minimal' is recommended for contrib development." + type = "string" + default = "minimal" + mutable = true + order = 5 + option { + name = "minimal" + value = "minimal" + } + option { + name = "standard" + value = "standard" + } + option { + name = "demo_umami" + value = "demo_umami" + } +} + +data "coder_parameter" "share_drupal_site" { + name = "share_drupal_site" + display_name = "Drupal Site Sharing" + description = "Who can access the Drupal site URL. Change to 'public' when you want to share a work-in-progress with someone outside Coder." + type = "string" + default = "owner" + mutable = true + order = 90 + + option { + name = "Private (owner only)" + value = "owner" + } + option { + name = "Authenticated (any Coder user)" + value = "authenticated" + } + option { + name = "Public (anyone with the link)" + value = "public" + } +} + +data "coder_parameter" "vscode_extensions" { + name = "vscode_extensions" + display_name = "VS Code Extensions" + description = "Select extensions to enable in VS Code for Web" + type = "list(string)" + form_type = "multi-select" + default = jsonencode([for e in var.vscode_extensions : e.id if e.default]) + mutable = true + order = 100 + + dynamic "option" { + for_each = var.vscode_extensions + content { + name = option.value.name + value = option.value.id + } + } +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +locals { + workspace_home = "/home/coder" + selected_extensions = jsondecode(data.coder_parameter.vscode_extensions.value) + project_name = data.coder_parameter.project_name.value + project_dir = "/home/coder/${data.coder_parameter.project_name.value}" + issue_fork = data.coder_parameter.issue_fork.value + issue_url = local.issue_fork != "" ? "https://www.drupal.org/project/${local.project_name}/issues/${local.issue_fork}" : "" + # Coerce share value — mock_data in tftest returns "[]" for all parameters; + # fall back to "owner" if the value is not a valid share level. + drupal_site_share = contains(["owner", "authenticated", "public"], data.coder_parameter.share_drupal_site.value) ? data.coder_parameter.share_drupal_site.value : "owner" +} + +locals { + image_version = try(trimspace(file("${path.module}/VERSION")), var.image_version) + registry_without_version = replace(var.workspace_image_registry, ":${local.image_version}", "") + workspace_image_registry_base = replace(local.registry_without_version, ":latest", "") +} + +variable "vscode_extensions" { + description = "List of VS Code extensions to offer in the workspace creation UI" + type = list(object({ + id = string + name = string + default = bool + })) + default = [ + { id = "xdebug.php-debug", name = "PHP Debug", default = true }, + { id = "bmewburn.vscode-intelephense-client", name = "Intelephense", default = true }, + { id = "dbaeumer.vscode-eslint", name = "ESLint", default = true }, + { id = "esbenp.prettier-vscode", name = "Prettier", default = true }, + { id = "sanderronde.phpstan-vscode", name = "PHPStan", default = true }, + { id = "streetsidesoftware.code-spell-checker", name = "Code Spell Checker", default = true }, + { id = "stylelint.vscode-stylelint", name = "Stylelint", default = true }, + { id = "valeryanm.vscode-phpsab", name = "PHPSAB", default = true }, + { id = "biati.ddev-manager", name = "DDEV Manager", default = true }, + { id = "deque-systems.vscode-axe-linter", name = "Axe Linter", default = false }, + { id = "andrewdavidblum.drupal-smart-snippets", name = "Drupal Smart Snippets", default = false }, + { id = "redhat.vscode-yaml", name = "YAML", default = false }, + { id = "sleistner.vscode-fileutils", name = "File Utils", default = false }, + { id = "GitHub.vscode-pull-request-github", name = "GitHub Pull Requests", default = false }, + ] +} + +variable "workspace_image_registry" { + description = "Docker registry URL for the workspace base image (without tag, version is added automatically)" + type = string + default = "index.docker.io/ddev/coder-ddev" +} + +resource "docker_image" "workspace_image" { + name = "${local.workspace_image_registry_base}:${local.image_version}" + pull_triggers = [ + local.image_version, + local.workspace_image_registry_base, + "${local.workspace_image_registry_base}:${local.image_version}", + ] + keep_locally = true + lifecycle { + create_before_destroy = true + } +} + +variable "cpu" { + description = "CPU cores" + type = number + default = 4 + validation { + condition = var.cpu >= 1 && var.cpu <= 32 + error_message = "CPU must be between 1 and 32" + } +} + +variable "memory" { + description = "Memory in GB" + type = number + default = 8 + validation { + condition = var.memory >= 2 && var.memory <= 128 + error_message = "Memory must be between 2 and 128 GB" + } +} + +resource "coder_agent" "main" { + arch = "amd64" + os = "linux" + + shutdown_script = < /dev/null 2>&1; then + SUDO="sudo" + else + SUDO="" + fi + + sudo chown coder:coder /home/coder + + if [ ! -f "/home/coder/.bashrc" ]; then + echo "Initializing home directory..." + cp -rT /etc/skel/. /home/coder/ + fi + + cd /home/coder + + echo "==========================================" + echo "Starting workspace setup..." + echo "==========================================" + echo "Workspace Home: $HOME" + + if [ -z "$GIT_SSH_COMMAND" ]; then + CODER_GITSSH=$(find /tmp -name "coder" -path "*/coder.*/*" -type f -executable 2>/dev/null | head -1) + if [ -n "$CODER_GITSSH" ]; then + export GIT_SSH_COMMAND="$CODER_GITSSH gitssh" + echo "✓ Coder GitSSH wrapper found and configured for this session" + else + echo "Note: Coder GitSSH wrapper not found. Git operations may require manual SSH key setup." + fi + else + echo "✓ GIT_SSH_COMMAND already set: $GIT_SSH_COMMAND" + fi + + echo "Copying files from /home/coder-files to ~/..." + if [ -d /home/coder-files ]; then + if [ -d /home/coder-files/.vscode ]; then + mkdir -p ~/.vscode + if [ -f /home/coder-files/.vscode/settings.json ]; then + cp /home/coder-files/.vscode/settings.json ~/.vscode/settings.json + chown coder:coder ~/.vscode/settings.json 2>/dev/null || true + fi + fi + else + echo "Warning: /home/coder-files not found in image" + fi + + if [ ! -f "$HOME/.gitconfig" ] && [ -f /home/coder-files/.gitconfig ]; then + cp /home/coder-files/.gitconfig "$HOME/.gitconfig" + fi + if [ ! -f "$HOME/.gitignore_global" ] && [ -f /home/coder-files/.gitignore_global ]; then + cp /home/coder-files/.gitignore_global "$HOME/.gitignore_global" + elif [ -f "$HOME/.gitignore_global" ]; then + grep -qxF 'config.coder.yaml' "$HOME/.gitignore_global" || \ + echo 'config.coder.yaml' >> "$HOME/.gitignore_global" + fi + if [ -n "$CODER_WORKSPACE_OWNER_NAME" ]; then + git config --global user.name "$CODER_WORKSPACE_OWNER_NAME" + fi + if [ -n "$CODER_WORKSPACE_OWNER_EMAIL" ]; then + git config --global user.email "$CODER_WORKSPACE_OWNER_EMAIL" + fi + + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + if ! grep -q "LC_ALL=en_US.UTF-8" ~/.bashrc; then + echo "export LANG=en_US.UTF-8" >> ~/.bashrc + echo "export LC_ALL=en_US.UTF-8" >> ~/.bashrc + fi + + sed -i '/export GIT_SSH_COMMAND=/d' ~/.bashrc || true + + if ! grep -q "git_prompt()" ~/.bashrc; then + echo '' >> ~/.bashrc + echo '# Git branch in prompt' >> ~/.bashrc + echo 'git_prompt() {' >> ~/.bashrc + echo ' local branch' >> ~/.bashrc + echo ' branch="$(git symbolic-ref HEAD 2>/dev/null | cut -d/ -f3-)"' >> ~/.bashrc + echo ' [ -n "$branch" ] && echo " ($branch)"' >> ~/.bashrc + echo '}' >> ~/.bashrc + echo 'PS1='\''\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]$(git_prompt)\$ '\''' >> ~/.bashrc + fi + + REGISTRY_MIRROR="${var.docker_registry_mirror}" + if [ -z "$REGISTRY_MIRROR" ] && [ -n "$CODER_AGENT_URL" ]; then + CODER_HOST=$(echo "$CODER_AGENT_URL" | sed -E 's#^https?://([^/:]+).*$#\1#') + CANDIDATE_MIRROR="http://$CODER_HOST:5000" + if [ -n "$CODER_HOST" ] && curl -fsS --max-time 2 "$CANDIDATE_MIRROR/v2/" > /dev/null 2>&1; then + REGISTRY_MIRROR="$CANDIDATE_MIRROR" + echo "Detected registry mirror on Coder host: $REGISTRY_MIRROR" + else + echo "No reachable registry mirror detected on Coder host; continuing without mirror" + fi + fi + if [ -n "$REGISTRY_MIRROR" ]; then + echo "Configuring Docker registry mirror: $REGISTRY_MIRROR" + MIRROR_HOST=$(echo "$REGISTRY_MIRROR" | sed 's|https\?://||') + sudo mkdir -p /etc/docker + sudo tee /etc/docker/daemon.json > /dev/null < /dev/null; then + echo "Starting Docker Daemon..." + sudo dockerd > /tmp/dockerd.log 2>&1 & + + echo "Waiting for Docker Socket..." + for i in $(seq 1 30); do + if [ -S /var/run/docker.sock ]; then + echo "Docker Socket found!" + break + fi + sleep 1 + done + + if [ -S /var/run/docker.sock ]; then + sudo chmod 666 /var/run/docker.sock + else + echo "Error: Docker Socket not found after 30s!" + fi + else + echo "Docker Daemon already running." + fi + + mkdir -p ~/.ddev + echo "Configuring DDEV to omit ddev-router..." + ddev config global --omit-containers=ddev-router --instrumentation-opt-in=false > /dev/null 2>&1 || true + mkcert -install 2>/dev/null || true + + _t_images=$SECONDS + echo "Pre-pulling DDEV images..." + ddev utility download-images || true + IMAGES_TIME=$((SECONDS - _t_images)) + echo " ddev utility download-images complete ($${IMAGES_TIME}s)" + + # ========================================== + # DRUPAL CONTRIB AUTOMATIC SETUP + # ========================================== + echo "" + echo "==========================================" + echo "Drupal Contrib Automatic Setup" + echo "==========================================" + + PROJECT_NAME="${data.coder_parameter.project_name.value}" + PROJECT_TYPE="${data.coder_parameter.project_type.value}" + PROJ_DIR="/home/coder/$PROJECT_NAME" + SETUP_LOG="/tmp/drupal-setup.log" + SETUP_STATUS="$HOME/SETUP_STATUS.txt" + + cat > "$SETUP_STATUS" << 'STATUS_HEADER' +Drupal Contrib Setup Status +============================ +STATUS_HEADER + echo "Started: $(date)" >> "$SETUP_STATUS" + echo "Project: $PROJECT_NAME ($PROJECT_TYPE)" >> "$SETUP_STATUS" + echo "" >> "$SETUP_STATUS" + + log_setup() { + echo "$1" | tee -a "$SETUP_LOG" + } + + update_status() { + echo "$1" >> "$SETUP_STATUS" + } + + SETUP_FAILED=false + SETUP_START=$SECONDS + + # ========================================== + # Phase 2: Clone project (idempotent) + # ========================================== + if [ ! -d "$PROJ_DIR/.git" ]; then + log_setup "Cloning $PROJECT_NAME from git.drupalcode.org..." + update_status "⏳ Clone: In progress..." + if git clone "https://git.drupalcode.org/project/$PROJECT_NAME.git" "$PROJ_DIR" >> "$SETUP_LOG" 2>&1; then + log_setup "✓ Cloned $PROJECT_NAME" + update_status "✓ Clone: Success" + else + log_setup "✗ Failed to clone $PROJECT_NAME" + update_status "✗ Clone: Failed" + SETUP_FAILED=true + fi + else + log_setup "✓ $PROJECT_NAME repo already present — skipping clone" + update_status "✓ Clone: Already present" + # Keep repo current on restart + git -C "$PROJ_DIR" fetch --all --prune >> "$SETUP_LOG" 2>&1 || true + fi + + cd "$PROJ_DIR" || { log_setup "✗ Cannot cd to $PROJ_DIR"; SETUP_FAILED=true; } + + # ========================================== + # Phase 3: Issue fork checkout (if requested) + # ========================================== + ISSUE_FORK="${data.coder_parameter.issue_fork.value}" + ISSUE_BRANCH="${data.coder_parameter.issue_branch.value}" + + if [ "$SETUP_FAILED" = "false" ] && [ -n "$ISSUE_FORK" ]; then + log_setup "Issue fork mode: ISSUE_FORK=$ISSUE_FORK ISSUE_BRANCH=$ISSUE_BRANCH" + log_setup "🔗 Issue: https://www.drupal.org/project/$PROJECT_NAME/issues/$ISSUE_FORK" + + # Fetch issue title for display + ISSUE_TITLE=$(curl -sf "https://www.drupal.org/api-d7/node/$${ISSUE_FORK}.json" 2>/dev/null | jq -r '.title // ""' 2>/dev/null || echo "") + if [ -n "$ISSUE_TITLE" ]; then + log_setup " Title: $ISSUE_TITLE" + fi + + REMOTE_NAME="issue-$ISSUE_FORK" + FORK_URL="https://git.drupalcode.org/issue/$PROJECT_NAME-$ISSUE_FORK.git" + + update_status "⏳ Issue fork fetch: In progress..." + if ! git remote | grep -qx "$REMOTE_NAME"; then + git remote add "$REMOTE_NAME" "$FORK_URL" >> "$SETUP_LOG" 2>&1 + fi + + if git fetch "$REMOTE_NAME" >> "$SETUP_LOG" 2>&1; then + log_setup "✓ Fetched issue fork remote" + update_status "✓ Issue fork fetch: Success" + else + log_setup "⚠ Warning: Failed to fetch issue fork remote $FORK_URL (non-critical)" + update_status "⚠ Issue fork fetch: Warning (remote may not exist yet)" + fi + + if [ -n "$ISSUE_BRANCH" ]; then + CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "") + if [ "$CURRENT_BRANCH" != "$ISSUE_BRANCH" ]; then + update_status "⏳ Branch checkout: In progress..." + if git checkout "$ISSUE_BRANCH" >> "$SETUP_LOG" 2>&1 || \ + git checkout -b "$ISSUE_BRANCH" "$REMOTE_NAME/$ISSUE_BRANCH" >> "$SETUP_LOG" 2>&1; then + log_setup "✓ Checked out branch $ISSUE_BRANCH" + update_status "✓ Branch checkout: Success" + else + log_setup "⚠ Warning: Could not checkout $ISSUE_BRANCH — remaining on current branch" + update_status "⚠ Branch checkout: Warning" + fi + else + log_setup "✓ Already on branch $ISSUE_BRANCH" + update_status "✓ Branch checkout: Already on correct branch" + fi + fi + fi + + # ========================================== + # Phase 4: DDEV configuration + # ========================================== + if [ "$SETUP_FAILED" = "false" ]; then + DRUPAL_VERSION="${data.coder_parameter.drupal_version.value}" + case "$DRUPAL_VERSION" in + 10) DDEV_PROJECT_TYPE="drupal10" ;; + 11) DDEV_PROJECT_TYPE="drupal11" ;; + *) DDEV_PROJECT_TYPE="drupal12" ;; + esac + + # Compute Coder domain + CODER_DOMAIN="" + if [ -n "$CODER_WORKSPACE_OWNER_NAME" ] && ([ -n "$VSCODE_PROXY_URI" ] || [ -n "$CODER_AGENT_URL" ]); then + if [ -n "$VSCODE_PROXY_URI" ]; then + CODER_DOMAIN=$(echo "$VSCODE_PROXY_URI" | sed -E 's|https?://[^.]+\.(.+?)(/.*)?$|\1|') + else + CODER_DOMAIN=$(echo "$CODER_AGENT_URL" | sed -E 's|https?://(.+?)(/.*)?$|\1|') + fi + export CODER_DOMAIN + fi + + # Always regenerate .ddev/config.yaml so DDEV picks correct PHP version + rm -f .ddev/config.yaml + log_setup "Configuring DDEV for Drupal $DRUPAL_VERSION ($DDEV_PROJECT_TYPE)..." + update_status "⏳ DDEV config: In progress..." + + mkdir -p .ddev + + cat > .ddev/config.coder.yaml << CODER_YAML_EOF +# Auto-generated by workspace startup -- do not edit. +#ddev-silent-no-warn +project_tld: "$CODER_DOMAIN" +use_dns_when_possible: false +host_webserver_port: "8080" +host_mailpit_port: "8025" +hooks: + post-start: + - exec-host: 'echo "" && echo " Site: https://drupal-site--$${CODER_WORKSPACE_NAME}--$${CODER_WORKSPACE_OWNER_NAME}.$CODER_DOMAIN" && echo " Mailpit: https://mailpit--$${CODER_WORKSPACE_NAME}--$${CODER_WORKSPACE_OWNER_NAME}.$CODER_DOMAIN" && echo ""' +CODER_YAML_EOF + + # For themes, set symlink target path via config.local.yaml + if [ "$PROJECT_TYPE" = "theme" ]; then + cat > .ddev/config.local.yaml << 'THEME_YAML_EOF' +#ddev-silent-no-warn +web_environment: + - DRUPAL_PROJECTS_PATH=themes/custom +THEME_YAML_EOF + log_setup "✓ Theme symlink path configured (themes/custom)" + fi + + if ddev config --project-type="$DDEV_PROJECT_TYPE" --docroot=web \ + --project-name="$${CODER_WORKSPACE_NAME}--$${CODER_WORKSPACE_OWNER_NAME}" >> "$SETUP_LOG" 2>&1; then + log_setup "✓ DDEV configured (project-type=$DDEV_PROJECT_TYPE docroot=web)" + update_status "✓ DDEV config: Success" + else + log_setup "✗ Failed to configure DDEV" + update_status "✗ DDEV config: Failed" + SETUP_FAILED=true + fi + + if [ -n "$CODER_DOMAIN" ]; then + cat > .ddev/docker-compose.coder-describe.yaml << COMPOSE_EOF +# Auto-generated by workspace startup -- do not edit. +services: + web: + x-ddev: + describe-url-port: | + https://drupal-site--$${CODER_WORKSPACE_NAME}--$${CODER_WORKSPACE_OWNER_NAME}.$CODER_DOMAIN + Mailpit: https://mailpit--$${CODER_WORKSPACE_NAME}--$${CODER_WORKSPACE_OWNER_NAME}.$CODER_DOMAIN + describe-info: "Admin: admin / admin" +COMPOSE_EOF + log_setup "✓ .ddev/docker-compose.coder-describe.yaml written" + fi + + ddev config global --omit-containers=ddev-router >> "$SETUP_LOG" 2>&1 || true + + # Install ddev-drupal-contrib addon (idempotent) + log_setup "Installing ddev-drupal-contrib addon..." + update_status "⏳ ddev-drupal-contrib addon: In progress..." + if ddev add-on get ddev/ddev-drupal-contrib >> "$SETUP_LOG" 2>&1; then + log_setup "✓ ddev-drupal-contrib addon installed" + update_status "✓ ddev-drupal-contrib addon: Success" + else + log_setup "⚠ Warning: ddev-drupal-contrib addon install had issues (non-critical)" + update_status "⚠ ddev-drupal-contrib addon: Warning" + fi + + # Start DDEV + ddev poweroff 2>&1 | tee -a "$SETUP_LOG" || true + log_setup "Starting DDEV environment..." + update_status "⏳ DDEV start: In progress..." + + ddev start 2>&1 | tee -a "$SETUP_LOG" + DDEV_START_RC=$${PIPESTATUS[0]} + if [ $DDEV_START_RC -eq 0 ]; then + log_setup "✓ DDEV started successfully" + update_status "✓ DDEV start: Success" + else + log_setup "✗ Failed to start DDEV" + update_status "✗ DDEV start: Failed" + SETUP_FAILED=true + fi + fi + + # ========================================== + # Phase 5: Drupal installation (idempotent) + # ========================================== + if [ "$SETUP_FAILED" = "false" ]; then + # Check if Drupal is already installed (web/ dir with settings.php is the indicator) + DRUPAL_INSTALLED=false + if [ -d "$PROJ_DIR/web/sites/default/files" ] && \ + ddev drush status --field=db-status 2>/dev/null | grep -qi "connected"; then + DRUPAL_INSTALLED=true + log_setup "✓ Drupal already installed — skipping site install" + update_status "✓ Drupal install: Already installed" + fi + + if [ "$DRUPAL_INSTALLED" = "false" ]; then + # Add drush as require-dev so expand-composer-json includes it in composer.contrib.json + # --no-update avoids triggering plugin checks before Drupal is installed + log_setup "Adding drush to require-dev..." + update_status "⏳ drush: Adding to composer.json..." + ddev exec composer require --dev drush/drush --no-update --no-interaction >> "$SETUP_LOG" 2>&1 || true + + # Run ddev poser: expands composer.json → composer.contrib.json (includes require-dev), + # then runs composer install (installs Drupal + drush together), then removes composer.contrib.json + log_setup "Running ddev poser (installs Drupal as dev dependency)..." + update_status "⏳ ddev poser: In progress..." + _t=$SECONDS + if ddev poser >> "$SETUP_LOG" 2>&1; then + log_setup "✓ ddev poser complete ($((SECONDS - _t))s)" + update_status "✓ ddev poser: Success" + else + log_setup "✗ ddev poser failed ($((SECONDS - _t))s)" + update_status "✗ ddev poser: Failed" + SETUP_FAILED=true + fi + + if [ "$SETUP_FAILED" = "false" ]; then + # Restart triggers ddev symlink-project automatically + log_setup "Restarting DDEV to trigger symlink-project..." + ddev restart >> "$SETUP_LOG" 2>&1 || true + + # Install Drupal + INSTALL_PROFILE="${data.coder_parameter.install_profile.value}" + log_setup "Installing Drupal ($INSTALL_PROFILE profile)..." + update_status "⏳ Drupal install: In progress..." + _t=$SECONDS + if ddev drush si "$INSTALL_PROFILE" -y --account-pass=admin >> "$SETUP_LOG" 2>&1; then + log_setup "✓ Drupal installed ($((SECONDS - _t))s)" + update_status "✓ Drupal install: Success" + else + log_setup "✗ Drupal install failed ($((SECONDS - _t))s)" + update_status "✗ Drupal install: Failed" + SETUP_FAILED=true + fi + + if [ "$SETUP_FAILED" = "false" ]; then + # Enable the module or theme + log_setup "Enabling $PROJECT_NAME ($PROJECT_TYPE)..." + if [ "$PROJECT_TYPE" = "theme" ]; then + if ddev drush theme:enable "$PROJECT_NAME" -y >> "$SETUP_LOG" 2>&1; then + log_setup "✓ $PROJECT_NAME enabled" + else + log_setup "⚠ Warning: could not enable theme $PROJECT_NAME (may need manual enable)" + fi + else + if ddev drush en "$PROJECT_NAME" -y >> "$SETUP_LOG" 2>&1; then + log_setup "✓ $PROJECT_NAME enabled" + else + log_setup "⚠ Warning: could not enable module $PROJECT_NAME (may need manual enable)" + fi + fi + fi + fi + fi + + # Cache rebuild (always, on every start) + log_setup "Running cache rebuild..." + ddev drush cr >> "$SETUP_LOG" 2>&1 || true + fi + + # ========================================== + # Phase 6: Custom DDEV launch command + # ========================================== + mkdir -p ~/.ddev/commands/host + cat > ~/.ddev/commands/host/launch << 'LAUNCH_EOF' +#!/usr/bin/env bash + +## Description: Show Coder URLs for this Drupal contrib workspace +## Usage: launch [path] [-m|--mailpit] +## Example: "ddev launch" or "ddev launch /admin" or "ddev launch -m" +## Flags: [{"Name":"mailpit","Shorthand":"m","Usage":"ddev launch -m shows the Mailpit URL"}] + +if [ -z "$${CODER_WORKSPACE_NAME:-}" ] || ([ -z "$${VSCODE_PROXY_URI:-}" ] && [ -z "$${CODER_AGENT_URL:-}" ]); then + echo "Primary URL: $${DDEV_PRIMARY_URL:-unknown}" + echo "(Not running in a Coder workspace; cannot open a browser.)" + exit 0 +fi + +WORKSPACE="$${CODER_WORKSPACE_NAME}" +OWNER="$${CODER_WORKSPACE_OWNER_NAME}" +if [ -n "$${VSCODE_PROXY_URI:-}" ]; then + DOMAIN=$(echo "$${VSCODE_PROXY_URI}" | sed -E 's|https?://[^.]+\.(.+?)(/.*)?$|\1|') +else + DOMAIN=$(echo "$${CODER_AGENT_URL}" | sed -E 's|https?://(.+?)(/.*)?$|\1|') +fi +MAILPIT=false +PATH_SUFFIX="" + +while :; do + case $${1:-} in + -m | --mailpit | --mailhog) MAILPIT=true ;; + --) shift; break ;; + -?*) printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2 ;; + *) break ;; + esac + shift +done + +if [ -n "$${1:-}" ]; then + PATH_SUFFIX="/$${1#/}" +fi + +if [ "$${MAILPIT}" = "true" ]; then + echo "https://mailpit--$${WORKSPACE}--$${OWNER}.$${DOMAIN}" + exit 0 +fi + +SITE_URL="https://drupal-site--$${WORKSPACE}--$${OWNER}.$${DOMAIN}" +echo "" +echo "Coder URLs for this Drupal workspace:" +echo " Site: $${SITE_URL}$${PATH_SUFFIX}" +echo " Mailpit: https://mailpit--$${WORKSPACE}--$${OWNER}.$${DOMAIN}" +echo "" +echo "Admin login: admin / admin" +ULI=$(ddev drush uli --uri="$${SITE_URL}" 2>/dev/null || true) +if [ -n "$${ULI}" ]; then + echo "One-time login: $${ULI}" +else + echo "One-time login: ddev drush uli (run when Drupal is installed)" +fi +echo "" +LAUNCH_EOF + + chmod +x ~/.ddev/commands/host/launch + log_setup "✓ Custom DDEV launch command installed" + + # ========================================== + # Phase 6.5: Write welcome message + # ========================================== + { + echo "Drupal Contrib Development Workspace" + echo "======================================" + echo "Project: $PROJECT_NAME ($PROJECT_TYPE)" + echo "Admin: admin / admin" + echo "" + echo "Commands:" + echo " ddev launch # Show site URL and one-time login link" + echo " ddev describe # Show project details and URLs" + echo " ddev drush status # Check Drupal status" + echo " ddev phpunit # Run PHPUnit tests" + echo " ddev phpcs # Check coding standards" + echo " ddev phpstan # Run static analysis" + echo " ddev logs # View container logs" + echo " ddev ssh # SSH into web container" + echo "" + echo "Docs: https://docs.ddev.com/" + if [ -n "$ISSUE_FORK" ]; then + echo "" + ISSUE_LINE="Issue #$${ISSUE_FORK}" + [ -n "$ISSUE_TITLE" ] && ISSUE_LINE="$ISSUE_LINE: $ISSUE_TITLE" + echo "$ISSUE_LINE" + echo " https://www.drupal.org/project/$PROJECT_NAME/issues/$${ISSUE_FORK}" + fi + } > ~/WELCOME.txt + chown coder:coder ~/WELCOME.txt 2>/dev/null || true + + # ========================================== + # Shell environment setup + # ========================================== + mkdir -p ~/.npm-global + npm config set prefix "~/.npm-global" + export PATH="$HOME/.npm-global/bin:$PATH" + if ! echo "$PATH" | grep -q "$HOME/.npm-global/bin"; then + echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc + fi + + if ! echo "$PATH" | grep -q "/home/linuxbrew/.linuxbrew/bin"; then + echo 'export PATH="$PATH:/home/linuxbrew/.linuxbrew/bin"' >> ~/.bashrc + fi + + if ! grep -q 'bash_completion' ~/.bashrc 2>/dev/null; then + cat >> ~/.bashrc << 'BASHCOMP' +# Bash completion (for non-login interactive shells) +if ! shopt -oq posix; then + if [ -f /usr/share/bash-completion/bash_completion ]; then + . /usr/share/bash-completion/bash_completion + elif [ -f /etc/bash_completion ]; then + . /etc/bash_completion + fi +fi +BASHCOMP + fi + + if [ ! -f ~/.bash_profile ]; then + cat > ~/.bash_profile << 'BASHPROFILE' +# Source system-wide settings (bash_completion etc.) for login shells +if [ -f /etc/bash.bashrc ]; then + . /etc/bash.bashrc +fi + +# Source user .bashrc +if [ -f ~/.bashrc ]; then + . ~/.bashrc +fi + +# Display welcome message on SSH login (login shells only) +if [ -f ~/WELCOME.txt ]; then + cat ~/WELCOME.txt + echo "" +fi +BASHPROFILE + chmod 644 ~/.bash_profile + elif ! grep -q "WELCOME.txt" ~/.bash_profile 2>/dev/null; then + cat >> ~/.bash_profile << 'BASHPROFILE_WELCOME' +# Display welcome message on SSH login (login shells only) +if [ -f ~/WELCOME.txt ]; then + cat ~/WELCOME.txt + echo "" +fi +BASHPROFILE_WELCOME + fi + if ! grep -q 'etc/bash.bashrc' ~/.bash_profile 2>/dev/null; then + printf '\n# Source system-wide settings (bash_completion etc.) for login shells\nif [ -f /etc/bash.bashrc ]; then\n . /etc/bash.bashrc\nfi\n' >> ~/.bash_profile + fi + + # ========================================== + # Timing summary and final status + # ========================================== + TOTAL_TIME=$((SECONDS - SCRIPT_START)) + INSTALL_TIME=$((SECONDS - SETUP_START)) + FAILURE_SUMMARY=$(grep "✗" "$SETUP_LOG" 2>/dev/null | grep -v "^$" | head -20 || true) + + update_status "" + update_status "Completed: $(date)" + update_status "" + update_status "--- Timing ---" + update_status " ddev utility download-images: $${IMAGES_TIME}s" + update_status " Install phase: $${INSTALL_TIME}s" + update_status " Total workspace startup: $${TOTAL_TIME}s" + update_status "" + update_status "View full logs: $SETUP_LOG" + + if [ "$SETUP_FAILED" = "true" ]; then + { + echo "⚠ WORKSPACE SETUP INCOMPLETE" + echo "================================" + echo "" + echo "Setup encountered errors. Run this for details:" + echo " cat $SETUP_LOG" + echo "" + echo "Errors:" + echo "$FAILURE_SUMMARY" | sed 's/^/ /' + echo "" + echo "Status file: cat $SETUP_STATUS" + } > ~/WELCOME.txt + + echo "" + echo "==========================================" + echo "✗ SETUP FAILED" + echo "==========================================" + echo "$FAILURE_SUMMARY" | sed 's/^/ /' + echo "" + echo "Full log: cat $SETUP_LOG" + else + echo "=== Setup Complete ===" + echo "" + echo "⏱ Timing: images=$${IMAGES_TIME}s install=$${INSTALL_TIME}s total=$${TOTAL_TIME}s" + echo "" + echo "📁 $PROJECT_NAME ready at ~/~/$PROJECT_NAME" + echo "📄 Welcome message saved to ~/WELCOME.txt" + echo "" + fi + + cat ~/WELCOME.txt + echo "" + + exit 0 + EOT + + env = { + CODER_AGENT_FORCE_UPDATE = "1" + CODER_WORKSPACE_ID = data.coder_workspace.me.id + CODER_WORKSPACE_NAME = data.coder_workspace.me.name + CODER_WORKSPACE_OWNER_NAME = data.coder_workspace_owner.me.name + CODER_WORKSPACE_OWNER_EMAIL = data.coder_workspace_owner.me.email + HOME = "/home/coder" + } + + metadata { + display_name = "Coder DDEV Base" + key = "0" + script = "coder stat" + interval = 1 + timeout = 1 + } +} + +resource "docker_volume" "coder_dind_cache" { + name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}-dind-cache" +} + +module "vscode-web" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/vscode-web/coder" + version = "~> 1.0" + agent_id = coder_agent.main.id + folder = "/home/coder/${data.coder_parameter.project_name.value}" + accept_license = true + order = 2 + extensions = local.selected_extensions +} + +resource "coder_app" "ddev-web" { + agent_id = coder_agent.main.id + slug = "ddev-web" + display_name = "DDEV Web" + order = 1 + url = "http://localhost:8080" + icon = "https://raw.githubusercontent.com/ddev/ddev/main/docs/content/developers/logos/SVG/Logo.svg" + subdomain = true + share = "owner" + + healthcheck { + url = "http://localhost:8080" + interval = 10 + threshold = 30 + } +} + +resource "coder_app" "drupal-site" { + agent_id = coder_agent.main.id + slug = "drupal-site" + display_name = "Drupal Site" + order = 2 + url = "http://localhost:8080" + icon = "https://api.iconify.design/heroicons:check-circle.svg?color=white" + subdomain = true + share = local.drupal_site_share + + healthcheck { + url = "http://localhost:8080/user/login" + interval = 10 + threshold = 3 + } +} + +resource "coder_app" "mailpit" { + agent_id = coder_agent.main.id + slug = "mailpit" + display_name = "Mailpit" + order = 3 + url = "http://localhost:8025" + icon = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mailpit.svg" + subdomain = true + share = "owner" + + healthcheck { + url = "http://localhost:8025" + interval = 10 + threshold = 10 + } +} + +resource "coder_script" "ddev_shutdown" { + agent_id = coder_agent.main.id + display_name = "Stop DDEV Projects" + icon = "/icon/docker.svg" + run_on_stop = true + script = <<-EOT + #!/bin/bash + export PATH="$PATH:/home/linuxbrew/.linuxbrew/bin:/usr/local/bin" + for i in $(seq 1 10); do + [ -S /var/run/docker.sock ] && break + sleep 1 + done + if [ ! -S /var/run/docker.sock ]; then + echo "Docker socket not available; skipping ddev poweroff" + exit 0 + fi + echo "Running ddev poweroff..." + ddev poweroff || true + echo "ddev poweroff complete" + EOT +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = docker_image.workspace_image.image_id + name = "coder-${data.coder_workspace.me.id}" + hostname = "${data.coder_workspace.me.name}-${data.coder_workspace_owner.me.name}" + user = "coder" + group_add = [tostring(var.docker_gid)] + + stop_timeout = 180 + stop_signal = "SIGINT" + destroy_grace_seconds = 180 + + working_dir = local.workspace_home + + cpu_shares = var.cpu * 1024 + memory = var.memory * 1024 * 1024 * 1024 + + runtime = "sysbox-runc" + + volumes { + container_path = local.workspace_home + host_path = "/coder-workspaces/${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}" + read_only = false + } + + mounts { + type = "volume" + source = docker_volume.coder_dind_cache.name + target = "/var/lib/docker" + } + + env = [ + "CODER_AGENT_TOKEN=${coder_agent.main.token}", + "CODER_WORKSPACE_NAME=${data.coder_workspace.me.name}", + "ELECTRON_DISABLE_SANDBOX=1", + "ELECTRON_NO_SANDBOX=1", + ] + + command = ["sh", "-c", coder_agent.main.init_script] + + depends_on = [null_resource.workspace_cleanup] + + restart = "unless-stopped" + + security_opts = [ + "apparmor:unconfined", + "seccomp:unconfined" + ] + + privileged = false +} + +resource "null_resource" "workspace_cleanup" { + triggers = { + host_path = "/coder-workspaces/${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}" + } + + provisioner "local-exec" { + when = destroy + command = "sudo /usr/local/bin/coder-delete-workspace-dir '${self.triggers.host_path}'" + } +} + +resource "coder_metadata" "workspace_info" { + resource_id = docker_container.workspace[0].id + count = data.coder_workspace.me.start_count + + item { + key = "template" + value = "Drupal Contrib Development" + } + item { + key = "project" + value = "${data.coder_parameter.project_name.value} (${data.coder_parameter.project_type.value})" + } + item { + key = "project_location" + value = "/home/coder/${data.coder_parameter.project_name.value}" + } + item { + key = "admin_credentials" + value = "admin / admin" + } + item { + key = "image" + value = "${docker_image.workspace_image.name} (version: ${local.image_version})" + } + item { + key = "issue" + value = local.issue_fork != "" ? "#${local.issue_fork}" : "(standard workspace)" + } + item { + key = "issue_url" + value = local.issue_url + } +} diff --git a/drupal-contrib/tests/validate.tftest.hcl b/drupal-contrib/tests/validate.tftest.hcl new file mode 100644 index 0000000..609d7a9 --- /dev/null +++ b/drupal-contrib/tests/validate.tftest.hcl @@ -0,0 +1,80 @@ +mock_provider "coder" { + mock_data "coder_workspace" { + defaults = { + start_count = 1 + id = "mock-workspace-id" + name = "test-workspace" + } + } + mock_data "coder_workspace_owner" { + defaults = { + name = "testuser" + } + } + # vscode_extensions.value is jsondecode()d in locals; must be valid JSON + mock_data "coder_parameter" { + defaults = { + value = "[]" + } + } +} + +mock_provider "docker" {} + +# project_name has no default (validation requires non-empty) so we override it in every run block. +# The mock coder_parameter value "[]" is used for all parameters by default; individual run blocks +# override specific parameters where the value matters for assertions. +# +# Note: drupal_version and project_type are coder_parameters with option constraints, not +# Terraform variables with validation blocks. The Coder API enforces allowed values at workspace +# creation time; there is nothing to test at the terraform test layer. + +run "plan_succeeds_with_token_module" { + command = plan + + # project_name must be a valid machine name; use a well-known module for tests + variables { + # project_name is validated by coder_parameter regex at API level; no tf var to override + } +} + +run "container_created_when_started" { + command = plan + + assert { + condition = length(docker_container.workspace) == 1 + error_message = "docker_container.workspace must be created when start_count=1" + } +} + +run "cpu_below_minimum" { + command = plan + variables { + cpu = 0 + } + expect_failures = [var.cpu] +} + +run "cpu_above_maximum" { + command = plan + variables { + cpu = 33 + } + expect_failures = [var.cpu] +} + +run "memory_below_minimum" { + command = plan + variables { + memory = 1 + } + expect_failures = [var.memory] +} + +run "memory_above_maximum" { + command = plan + variables { + memory = 129 + } + expect_failures = [var.memory] +} diff --git a/drupal-core/.terraform.lock.hcl b/drupal-core/.terraform.lock.hcl index 6b712ef..684d6df 100644 --- a/drupal-core/.terraform.lock.hcl +++ b/drupal-core/.terraform.lock.hcl @@ -3,9 +3,10 @@ provider "registry.terraform.io/coder/coder" { version = "2.13.1" - constraints = ">= 0.23.0, >= 2.5.0, >= 2.13.0" + constraints = ">= 2.5.0, >= 2.13.0" hashes = [ "h1:oo6ST/RHdch2u6x7OV3ojkQCM1EmBF3KrEuiJehT23E=", + "h1:wVR3Sg+hRjNbIiWLAPbolmoBMHqCFsNSOR71GW16YzQ=", "zh:04e38e4e37c89b78401c7689ade07a708635340138974bc12840920deed24c1b", "zh:0b32684dcc4d8f24a9535649d47c74a116e9682dc73551f429a98a774981b98f", "zh:0cb5ae4b1f1ae0e7d8a3a8c50ce516c230c1ba4853500d6b83b8cbb144e70f84", @@ -24,10 +25,30 @@ provider "registry.terraform.io/coder/coder" { ] } +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.4" + hashes = [ + "h1:L5V05xwp/Gto1leRryuesxjMfgZwjb7oool4WS1UEFQ=", + "zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43", + "zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a", + "zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991", + "zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f", + "zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e", + "zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615", + "zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442", + "zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5", + "zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f", + "zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f", + ] +} + provider "registry.terraform.io/kreuzwerker/docker" { version = "3.6.2" constraints = "~> 3.0" hashes = [ + "h1:/Oe7tViXf/xyQ4Pg8cDifMlD3RthOYkslwQiRgx7BTE=", "h1:1K3j0xUY2D0+E+DBDQc6k1u6Al9MkuNWrIC9rnvwFSM=", "zh:22b51a8fb63481d290bdad9a221bc8c9e45d66d1a0cd45beed3f3627bf1debd8", "zh:2b902eb80a1ae033af1135cc165d192668820a7f8ea15beb5472f811c18bea1f", diff --git a/drupal-core/template.tf b/drupal-core/template.tf index e2c8142..a1c2b18 100644 --- a/drupal-core/template.tf +++ b/drupal-core/template.tf @@ -135,6 +135,29 @@ data "coder_parameter" "install_profile" { } } +data "coder_parameter" "share_drupal_site" { + name = "share_drupal_site" + display_name = "Drupal Site Sharing" + description = "Who can access the Drupal site URL. Change to 'public' when you want to share a work-in-progress with someone outside Coder." + type = "string" + default = "owner" + mutable = true + order = 90 + + option { + name = "Private (owner only)" + value = "owner" + } + option { + name = "Authenticated (any Coder user)" + value = "authenticated" + } + option { + name = "Public (anyone with the link)" + value = "public" + } +} + data "coder_parameter" "vscode_extensions" { name = "vscode_extensions" display_name = "VS Code Extensions" @@ -175,6 +198,9 @@ locals { selected_extensions = jsondecode(data.coder_parameter.vscode_extensions.value) issue_fork_clean = trimprefix(data.coder_parameter.issue_fork.value, "drupal-") issue_url = local.issue_fork_clean != "" ? "https://www.drupal.org/project/drupal/issues/${local.issue_fork_clean}" : "" + # Coerce share value — mock_data in tftest returns "[]" for all parameters; + # fall back to "owner" if the value is not a valid share level. + drupal_site_share = contains(["owner", "authenticated", "public"], data.coder_parameter.share_drupal_site.value) ? data.coder_parameter.share_drupal_site.value : "owner" } locals { @@ -1510,7 +1536,7 @@ resource "coder_app" "drupal-site" { url = "http://localhost:80" icon = "https://api.iconify.design/heroicons:check-circle.svg?color=white" subdomain = true - share = "owner" + share = local.drupal_site_share # Healthy only when Drupal returns 200. /user/login returns 500 when the # database isn't set up (before drush si) and 200 when Drupal is fully