@webship.co/workspace-drupal-cms 0.0.0-stage → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +222 -2
- package/agents/drupal-cms-site-builder.md +114 -0
- package/agents/drupal-cms-tester.md +146 -0
- package/commands/cmd-drupal-cms-project.sh +35 -0
- package/commands/cmd-tools-drupal-cms-cli.sh +26 -0
- package/dist/src/index.d.ts +24 -0
- package/dist/src/index.js +46 -0
- package/dist/src/index.js.map +1 -0
- package/features/drupal-cms.feature +17 -0
- package/package.json +73 -4
- package/scripts/args/arg-drupal-cms-cli.sh +32 -0
- package/scripts/args/arg-drupal-cms.sh +38 -0
- package/scripts/functions/fun-drupal-cms.sh +620 -0
- package/testing/site.feature +67 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Drupal CMS for the Webship Workspace: build a Drupal CMS site from a site template, apply add-on
|
|
4
|
+
# recipes, run Drush on it, add its default accounts, and wire its step pack into the testing setup.
|
|
5
|
+
#
|
|
6
|
+
# Placed by `plugins.sh --install drupal-cms` in core/scripts/plugins/drupal-cms/functions/, where
|
|
7
|
+
# bootstrap sources it after the core's own functions. Everything runs in the project's container
|
|
8
|
+
# (DDEV by default: `ddev composer …`, `ddev drush …`), never on the host. The container tool
|
|
9
|
+
# writes the database settings (settings.ddev.php), so nothing here writes a $databases block.
|
|
10
|
+
#
|
|
11
|
+
# The plugin is the whole of the platform: it builds with its own functions (drupal_cms_*) and
|
|
12
|
+
# calls only the workspace's general helpers (trim_project_name, drop_database, ddev_up,
|
|
13
|
+
# ddev_nodejs_version). It reads none of the Drupal functions the workspace base still carries,
|
|
14
|
+
# and none of the Drupal plugin's: either plugin works without the other.
|
|
15
|
+
#
|
|
16
|
+
# Four names are the ones the core's tools look for, under the name `drupal_cms`:
|
|
17
|
+
# set_drupal_cms_users the default accounts (cmd-tools-add-users.sh <project> drupal_cms)
|
|
18
|
+
# add_drupal_cms_users create them with Drush (the same tool, and --add-users of the builder)
|
|
19
|
+
# cancel_drupal_cms_users delete them (cmd-tools-cancel-users.sh <project> drupal_cms)
|
|
20
|
+
# testing_configure_drupal_cms the step pack in the testing stack (cmd-tools-testing.sh <project>)
|
|
21
|
+
|
|
22
|
+
# The step pack a Drupal CMS project is tested with, the core pack it loads by itself, and the
|
|
23
|
+
# release line both are required at.
|
|
24
|
+
readonly DRUPAL_CMS_STEPS_PACKAGE='@webship-js/drupal-cms-steps';
|
|
25
|
+
readonly DRUPAL_CMS_STEPS_VERSION='^2.2.0';
|
|
26
|
+
readonly DRUPAL_CMS_CORE_STEPS_PACKAGE='@webship-js/drupal-core-steps';
|
|
27
|
+
readonly DRUPAL_CMS_CORE_STEPS_VERSION='^2.2.0';
|
|
28
|
+
|
|
29
|
+
# The project template every Drupal CMS site is created from.
|
|
30
|
+
readonly DRUPAL_CMS_PROJECT_TEMPLATE='drupal/cms';
|
|
31
|
+
|
|
32
|
+
# The default accounts: an administrator, a content editor (the role Drupal CMS creates, with no
|
|
33
|
+
# account of its own) and an account with no role, the three a scenario logs in as. Accounts are
|
|
34
|
+
# configuration, created once on the site; a test suite never creates or deletes one. `_none_` is
|
|
35
|
+
# an account with no role.
|
|
36
|
+
function drupal_cms_default_users() {
|
|
37
|
+
users=(administrator content_editor authenticated);
|
|
38
|
+
user_administrator_name="administrator";
|
|
39
|
+
user_administrator_mail="administrator@example.com";
|
|
40
|
+
user_administrator_password="dD.123123ddd";
|
|
41
|
+
user_administrator_role="administrator";
|
|
42
|
+
user_content_editor_name="content_editor";
|
|
43
|
+
user_content_editor_mail="content_editor@example.com";
|
|
44
|
+
user_content_editor_password="dD.123123ddd";
|
|
45
|
+
user_content_editor_role="content_editor";
|
|
46
|
+
user_authenticated_name="authenticated";
|
|
47
|
+
user_authenticated_mail="authenticated@example.com";
|
|
48
|
+
user_authenticated_password="dD.123123ddd";
|
|
49
|
+
user_authenticated_role="_none_";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# The name the core's tools ask for the list by.
|
|
53
|
+
function set_drupal_cms_users() {
|
|
54
|
+
drupal_cms_default_users ;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Is this folder a Drupal CMS project? Its DDEV project type is Drupal's and its composer.json
|
|
58
|
+
# carries the Drupal CMS packages.
|
|
59
|
+
function drupal_cms_is_project() {
|
|
60
|
+
local project_path="$1";
|
|
61
|
+
grep -qE '^type:[[:space:]]*"?drupal[0-9]*"?[[:space:]]*$' "${project_path}/.ddev/config.yaml" 2>/dev/null || return 1 ;
|
|
62
|
+
grep -qE '"drupal/(cms|drupal_cms_[a-z0-9_]+)"' "${project_path}/composer.json" 2>/dev/null ;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Go to the project a tool was asked about, or say why not.
|
|
66
|
+
function drupal_cms_enter_project() {
|
|
67
|
+
local project_path="${WORKSPACE_ROOT}/${doc_name}/${PROJECT_NAME}";
|
|
68
|
+
if [ ! -d "${project_path}" ] ; then
|
|
69
|
+
echo "No project ${PROJECT_NAME} in ${WORKSPACE_ROOT}/${doc_name}." ;
|
|
70
|
+
return 1 ;
|
|
71
|
+
fi
|
|
72
|
+
if ! drupal_cms_is_project "${project_path}" ; then
|
|
73
|
+
echo "${doc_name}/${PROJECT_NAME} is not a Drupal CMS project (its composer.json does not carry the Drupal CMS packages)." ;
|
|
74
|
+
return 1 ;
|
|
75
|
+
fi
|
|
76
|
+
cd "${project_path}" || return 1 ;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# What a version asks for: `2.x` (or `2`) is the latest release of that line, `2.2.9` is that
|
|
80
|
+
# release, `2.x-dev` is the development branch. Sets drupal_cms_constraint (what composer
|
|
81
|
+
# create-project is given) and drupal_cms_stability ('' is composer's own: stable).
|
|
82
|
+
function drupal_cms_resolve_version() {
|
|
83
|
+
local version="${1:-2.x}" ;
|
|
84
|
+
drupal_cms_constraint="" ; drupal_cms_stability="" ;
|
|
85
|
+
if [[ "${version}" =~ ^([0-9]+)(\.x)?$ ]] ; then
|
|
86
|
+
drupal_cms_constraint="^${BASH_REMATCH[1]}" ;
|
|
87
|
+
elif [[ "${version}" =~ ^[0-9]+\.x-dev$ ]] ; then
|
|
88
|
+
drupal_cms_constraint="${version}" ;
|
|
89
|
+
drupal_cms_stability="dev" ;
|
|
90
|
+
elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)[0-9]*)?$ ]] ; then
|
|
91
|
+
drupal_cms_constraint="${version}" ;
|
|
92
|
+
drupal_cms_stability="${BASH_REMATCH[2]}" ;
|
|
93
|
+
[ "${drupal_cms_stability}" == 'rc' ] && drupal_cms_stability="RC" ;
|
|
94
|
+
else
|
|
95
|
+
echo "--version ${version} is not a version this builder reads: a release line (2.x), a release (2.2.9) or a development branch (2.x-dev)." ;
|
|
96
|
+
return 1 ;
|
|
97
|
+
fi
|
|
98
|
+
return 0 ;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# The add-on recipe a short name stands for: `forms` is drupal_cms_forms, `seo_tools` is
|
|
102
|
+
# drupal_cms_seo_tools. A name that already says drupal_cms_ is taken as it is.
|
|
103
|
+
function drupal_cms_recipe_name() {
|
|
104
|
+
local name="$1" ;
|
|
105
|
+
case "${name}" in
|
|
106
|
+
drupal_cms_*) printf '%s' "${name}" ;;
|
|
107
|
+
*) printf 'drupal_cms_%s' "${name}" ;;
|
|
108
|
+
esac
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# The site template a name stands for: `starter` and `blank` are the two Drupal CMS ships or
|
|
112
|
+
# names itself (drupal_cms_starter, drupal_cms_blank); any other name is the recipe folder of
|
|
113
|
+
# that template, as it is.
|
|
114
|
+
function drupal_cms_template_name() {
|
|
115
|
+
local name="$1" ;
|
|
116
|
+
case "${name}" in
|
|
117
|
+
starter|blank) printf 'drupal_cms_%s' "${name}" ;;
|
|
118
|
+
*) printf '%s' "${name}" ;;
|
|
119
|
+
esac
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# The recipes --recipes asked for, as one name per word: "forms,search" is
|
|
123
|
+
# "drupal_cms_forms drupal_cms_search". Nothing for the default.
|
|
124
|
+
function drupal_cms_recipe_list() {
|
|
125
|
+
local given="$1" name list="" ;
|
|
126
|
+
[ "${given}" == '_none_' ] && return 0 ;
|
|
127
|
+
for name in ${given//,/ } ; do
|
|
128
|
+
list="${list}${list:+ }$(drupal_cms_recipe_name "${name}")" ;
|
|
129
|
+
done
|
|
130
|
+
printf '%s' "${list}" ;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# composer install, once more if it fails: the first install of a fresh project can fail on a
|
|
134
|
+
# network or cache hiccup and pass on the second try. A real dependency problem fails both times.
|
|
135
|
+
function drupal_cms_composer_install_retry() {
|
|
136
|
+
ddev composer install --no-interaction && return 0 ;
|
|
137
|
+
echo "composer install failed once, trying it again before giving up." ;
|
|
138
|
+
ddev composer install --no-interaction ;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# composer create-project in the current project folder, which holds only its .ddev directory.
|
|
142
|
+
function drupal_cms_composer_create_project() {
|
|
143
|
+
local package="$1" stability="$2" ;
|
|
144
|
+
if ddev composer create-project "${package}" --no-interaction ${stability:+--stability=${stability}} ; then
|
|
145
|
+
return 0 ;
|
|
146
|
+
fi
|
|
147
|
+
# When the project reached the install step, composer.json is already there: install once more.
|
|
148
|
+
if [ -f composer.json ] && drupal_cms_composer_install_retry ; then
|
|
149
|
+
echo "composer create-project needed a second install, which worked." ;
|
|
150
|
+
return 0 ;
|
|
151
|
+
fi
|
|
152
|
+
return 1 ;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# The project template ships a .ddev/config.yaml of its own, which replaces the one the build
|
|
156
|
+
# started with: no project name, a database of its choice, sometimes a container tool newer than
|
|
157
|
+
# the one on this machine. Put the project's own values back, keep the database the build already
|
|
158
|
+
# started, and start again.
|
|
159
|
+
function drupal_cms_align_container() {
|
|
160
|
+
local database="$1" php_version="$2" ;
|
|
161
|
+
local settings=(--project-name=${PROJECT_NAME} --project-type=drupal --docroot=web --nodejs-version=$(ddev_nodejs_version)) ;
|
|
162
|
+
[ -n "${database}" ] && settings+=(--database=${database}) ;
|
|
163
|
+
[ -n "${php_version}" ] && settings+=(--php-version=${php_version}) ;
|
|
164
|
+
if ! ddev config "${settings[@]}" ; then
|
|
165
|
+
if grep -q '^ddev_version_constraint:' .ddev/config.yaml 2>/dev/null ; then
|
|
166
|
+
echo "The project template asks for a newer DDEV than this machine has ($(grep '^ddev_version_constraint:' .ddev/config.yaml))." ;
|
|
167
|
+
echo "The line is taken out so this DDEV runs the project. Update DDEV when you can." ;
|
|
168
|
+
sed -i '/^ddev_version_constraint:/d' .ddev/config.yaml ;
|
|
169
|
+
ddev config "${settings[@]}" || return 1 ;
|
|
170
|
+
else
|
|
171
|
+
return 1 ;
|
|
172
|
+
fi
|
|
173
|
+
fi
|
|
174
|
+
drupal_cms_telemetry_off ;
|
|
175
|
+
ddev_up restart ;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# A site built here is a local one: keep it out of the install statistics Drupal CMS reports.
|
|
179
|
+
function drupal_cms_telemetry_off() {
|
|
180
|
+
ddev config --web-environment-add=DRUPAL_CMS_TELEMETRY=0 > /dev/null ;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# `ddev drush` answering. DDEV rewrites its shared command directory whenever any project on the
|
|
184
|
+
# machine starts, and `ddev drush` is missing from it for a moment: wait for it, instead of
|
|
185
|
+
# failing the install on its first line. Drupal CMS brings Drush with it; an older release that
|
|
186
|
+
# does not gets it here.
|
|
187
|
+
function drupal_cms_wait_for_drush() {
|
|
188
|
+
local attempt ;
|
|
189
|
+
if [ ! -d vendor/drush/drush ] ; then
|
|
190
|
+
ddev composer require drush/drush --no-interaction || return 1 ;
|
|
191
|
+
fi
|
|
192
|
+
for attempt in 1 2 3 4 5 6 7 8 9 10 ; do
|
|
193
|
+
ddev drush --version > /dev/null 2>&1 && return 0 ;
|
|
194
|
+
echo "ddev drush is not available yet (attempt ${attempt} of 10), waiting." ;
|
|
195
|
+
sleep 6 ;
|
|
196
|
+
done
|
|
197
|
+
echo "ddev drush is still not available." ;
|
|
198
|
+
return 1 ;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# composer require, once more if it fails: a site template brings some sixty packages, and one
|
|
202
|
+
# download that times out fails the whole command. A package that does not exist fails both times.
|
|
203
|
+
function drupal_cms_composer_require_retry() {
|
|
204
|
+
ddev composer require "$@" --with-all-dependencies --no-interaction && return 0 ;
|
|
205
|
+
echo "composer require failed once, trying it again before giving up." ;
|
|
206
|
+
sleep 20 ;
|
|
207
|
+
ddev composer require "$@" --with-all-dependencies --no-interaction ;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# A recipe in recipes/, required with composer when it is not there yet. The project template
|
|
211
|
+
# brings the installer and no site template, so the Starter template is required here too. The
|
|
212
|
+
# package is drupal/<recipe> unless one is named.
|
|
213
|
+
function drupal_cms_require_recipe() {
|
|
214
|
+
local recipe="$1" package="${2:-}" ;
|
|
215
|
+
# What a package ships is unpacked into recipes/ by composer; asking again is harmless.
|
|
216
|
+
ddev composer drupal:recipe-unpack --no-interaction > /dev/null 2>&1 || true ;
|
|
217
|
+
[ -d "recipes/${recipe}" ] && return 0 ;
|
|
218
|
+
package="${package:-drupal/${recipe}}" ;
|
|
219
|
+
echo "The ${recipe} recipe is not in recipes/: requiring ${package}." ;
|
|
220
|
+
drupal_cms_composer_require_retry "${package}" || return 1 ;
|
|
221
|
+
ddev composer drupal:recipe-unpack --no-interaction > /dev/null 2>&1 || true ;
|
|
222
|
+
if [ ! -d "recipes/${recipe}" ] ; then
|
|
223
|
+
echo "The ${recipe} recipe is still not in recipes/ after requiring ${package}." ;
|
|
224
|
+
return 1 ;
|
|
225
|
+
fi
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# Apply add-on recipes to an installed site, one after the other.
|
|
229
|
+
function drupal_cms_apply_recipes() {
|
|
230
|
+
local recipe ;
|
|
231
|
+
for recipe in "$@" ; do
|
|
232
|
+
drupal_cms_require_recipe "${recipe}" || return 1 ;
|
|
233
|
+
echo "Applying the ${recipe} recipe." ;
|
|
234
|
+
ddev drush cache:rebuild ;
|
|
235
|
+
ddev drush recipe "/var/www/html/recipes/${recipe}" || { echo "The ${recipe} recipe could not be applied." ; return 1 ; } ;
|
|
236
|
+
done
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# What the site was built from, kept where the test suite reads it: the environment of the
|
|
240
|
+
# project's container (DRUPAL_SITE_TEMPLATE, DRUPAL_CMS_RECIPES). The recipes are separated by
|
|
241
|
+
# spaces there, because the container tool splits its own list on commas.
|
|
242
|
+
function drupal_cms_record_build() {
|
|
243
|
+
local template="$1" recipes="$2" ;
|
|
244
|
+
[ -n "${template}" ] && ddev config --web-environment-add="DRUPAL_SITE_TEMPLATE=${template}" > /dev/null ;
|
|
245
|
+
[ -n "${recipes}" ] && ddev config --web-environment-add="DRUPAL_CMS_RECIPES=${recipes}" > /dev/null ;
|
|
246
|
+
return 0 ;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# The recipes recorded for the project, one per word.
|
|
250
|
+
function drupal_cms_recorded_recipes() {
|
|
251
|
+
local project_path="$1" ;
|
|
252
|
+
grep -E '^[[:space:]]*-[[:space:]]*"?DRUPAL_CMS_RECIPES=' "${project_path}/.ddev/config.yaml" 2>/dev/null | head -1 \
|
|
253
|
+
| sed -E 's/^[[:space:]]*-[[:space:]]*"?DRUPAL_CMS_RECIPES=//; s/"?[[:space:]]*$//' ;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# The first account of the site is the workspace account. The platform's installer names it and
|
|
257
|
+
# sets its password by itself on some releases, whatever the install command was given, so the
|
|
258
|
+
# name, the password and the address are set again here. They are handed over on standard input,
|
|
259
|
+
# so no character in them is read as code.
|
|
260
|
+
function drupal_cms_set_owner() {
|
|
261
|
+
printf '%s\n%s\n%s' "${account_name}" "${account_pass}" "${account_mail}" | ddev drush php:eval '
|
|
262
|
+
$given = explode("\n", stream_get_contents(STDIN));
|
|
263
|
+
$owner = \Drupal\user\Entity\User::load(1);
|
|
264
|
+
if ($owner && count($given) === 3 && $given[0] !== "") {
|
|
265
|
+
$owner->setUsername($given[0])->setPassword($given[1])->setEmail($given[2])->save();
|
|
266
|
+
print "The first account of the site is " . $owner->getAccountName() . ".\n";
|
|
267
|
+
}' ;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Is the site installed and answering Drush?
|
|
271
|
+
function drupal_cms_is_installed() {
|
|
272
|
+
ddev drush status --field=bootstrap 2>/dev/null | grep -qi successful ;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# Does a role exist on the site?
|
|
276
|
+
function drupal_cms_role_exists() {
|
|
277
|
+
ddev drush config:get "user.role.$1" id > /dev/null 2>&1 ;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# Does an account exist on the site?
|
|
281
|
+
function drupal_cms_user_exists() {
|
|
282
|
+
ddev drush user:information "$1" > /dev/null 2>&1 ;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# Build a Drupal CMS site from nothing: the container, the code, and with --install the site
|
|
286
|
+
# itself, from the site template and with the add-on recipes that were asked for.
|
|
287
|
+
function drupal_cms_build() {
|
|
288
|
+
trim_project_name ;
|
|
289
|
+
local base_url="https://${PROJECT_NAME}.ddev.site" ;
|
|
290
|
+
local version="${VERSION:-${site_version:-2.x}}" ;
|
|
291
|
+
local php_version="${PHP_VERSION:-}" ;
|
|
292
|
+
|
|
293
|
+
drupal_cms_resolve_version "${version}" || exit 1 ;
|
|
294
|
+
local package="${DRUPAL_CMS_PROJECT_TEMPLATE}:${drupal_cms_constraint}" ;
|
|
295
|
+
local template ; template="$(drupal_cms_template_name "${TEMPLATE:-starter}")" ;
|
|
296
|
+
local template_package="${TEMPLATE_PACKAGE:-}" ;
|
|
297
|
+
local recipes ; recipes="$(drupal_cms_recipe_list "${RECIPES:-_none_}")" ;
|
|
298
|
+
|
|
299
|
+
echo "Drupal CMS for ${doc_name}/${PROJECT_NAME}:" ;
|
|
300
|
+
echo " code ${package}${drupal_cms_stability:+ (stability ${drupal_cms_stability})}" ;
|
|
301
|
+
echo " container project type drupal, PHP ${php_version:-the one the project template names}, Node.js $(ddev_nodejs_version)" ;
|
|
302
|
+
echo " site template ${template}${template_package:+ (from ${template_package})}" ;
|
|
303
|
+
echo " recipes ${recipes:-none}" ;
|
|
304
|
+
echo " install $([ "${INSTALL}" == 'yes' ] && echo 'yes' || echo 'no')$([ "${ADD_USERS}" == 'yes' ] && echo ', with the default accounts')" ;
|
|
305
|
+
echo " address ${base_url}" ;
|
|
306
|
+
if [ "${DRY_RUN}" == 'yes' ] ; then
|
|
307
|
+
echo "A dry run: nothing was built." ;
|
|
308
|
+
return 0 ;
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
cd ${WORKSPACE_ROOT}/${doc_name} || exit 1 ;
|
|
312
|
+
|
|
313
|
+
drop_database ;
|
|
314
|
+
rm -rf "${PROJECT_NAME}" ;
|
|
315
|
+
mkdir -p "${PROJECT_NAME}" ;
|
|
316
|
+
cd ${WORKSPACE_ROOT}/${doc_name}/${PROJECT_NAME} || exit 1 ;
|
|
317
|
+
|
|
318
|
+
ddev config --project-type=drupal --docroot=web --project-name=${PROJECT_NAME} \
|
|
319
|
+
--nodejs-version=$(ddev_nodejs_version) ${php_version:+--php-version=${php_version}} --auto || { echo "The container could not be configured." ; exit 1 ; } ;
|
|
320
|
+
drupal_cms_telemetry_off ;
|
|
321
|
+
ddev_up start -y || exit 1 ;
|
|
322
|
+
|
|
323
|
+
# The database the container started with, before the project template can name another.
|
|
324
|
+
local database ;
|
|
325
|
+
database=$(ddev describe -j 2>/dev/null | python3 -c 'import json, sys
|
|
326
|
+
try:
|
|
327
|
+
raw = json.load(sys.stdin)["raw"]
|
|
328
|
+
print("%s:%s" % (raw["database_type"], raw["database_version"]))
|
|
329
|
+
except Exception:
|
|
330
|
+
pass' 2>/dev/null) ;
|
|
331
|
+
|
|
332
|
+
if ! drupal_cms_composer_create_project "${package}" "${drupal_cms_stability}" ; then
|
|
333
|
+
echo "composer create-project failed for ${package}: the build stops here, nothing was installed." ;
|
|
334
|
+
exit 1 ;
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
drupal_cms_align_container "${database}" "${php_version}" || { echo "The container could not be started again with the project template's configuration." ; exit 1 ; } ;
|
|
338
|
+
|
|
339
|
+
if [ "${INSTALL}" == 'yes' ] ; then
|
|
340
|
+
drupal_cms_wait_for_drush || { echo "Drush does not answer: the build stops here." ; exit 1 ; } ;
|
|
341
|
+
drupal_cms_require_recipe "${template}" "${template_package}" || { echo "The site template is not in the code: the build stops here." ; exit 1 ; } ;
|
|
342
|
+
|
|
343
|
+
# The installer applies the site template: the form value is the folder name of its recipe.
|
|
344
|
+
if ! ddev drush site:install drupal_cms_installer "installer_site_template_form.add_ons=${template}" --yes \
|
|
345
|
+
--site-name="${doc_name} ${PROJECT_NAME}" \
|
|
346
|
+
--account-name="${account_name}" --account-pass="${account_pass}" --account-mail="${account_mail}" ; then
|
|
347
|
+
echo "The install of ${doc_name} ${PROJECT_NAME} failed: drush site:install did not finish." ;
|
|
348
|
+
echo "The project is kept for a look: ${base_url} (ddev logs, ddev drush status)." ;
|
|
349
|
+
exit 1 ;
|
|
350
|
+
fi
|
|
351
|
+
drupal_cms_set_owner ;
|
|
352
|
+
if ! drupal_cms_is_installed ; then
|
|
353
|
+
echo "${doc_name} ${PROJECT_NAME} was installed, but the site does not bootstrap. Run: ddev drush status ; ddev logs." ;
|
|
354
|
+
exit 1 ;
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
if [ -n "${recipes}" ] ; then
|
|
358
|
+
drupal_cms_apply_recipes ${recipes} || { echo "The site is installed; a recipe was not applied. Apply it again with cmd-tools-drupal-cms-cli.sh ${PROJECT_NAME} --recipe <name>." ; exit 1 ; } ;
|
|
359
|
+
fi
|
|
360
|
+
drupal_cms_record_build "${template}" "${recipes}" ;
|
|
361
|
+
|
|
362
|
+
if [ "${ADD_USERS}" == 'yes' ] ; then
|
|
363
|
+
add_drupal_cms_users ;
|
|
364
|
+
cd ${WORKSPACE_ROOT}/${doc_name}/${PROJECT_NAME} || exit 1 ;
|
|
365
|
+
fi
|
|
366
|
+
ddev drush cache:rebuild ;
|
|
367
|
+
|
|
368
|
+
echo "${doc_name} ${PROJECT_NAME} has been installed: Drupal CMS on Drupal $(ddev drush status --field=drupal-version 2>/dev/null), the ${template} site template${recipes:+, with ${recipes}}.";
|
|
369
|
+
echo "Go to ${base_url}/user/login and log in as ${account_name}";
|
|
370
|
+
else
|
|
371
|
+
[ "${ADD_USERS}" == 'yes' ] && echo "--add-users needs --install: there is no site to add accounts to yet." ;
|
|
372
|
+
[ -n "${recipes}" ] && echo "--recipes needs --install: a recipe is applied to an installed site." ;
|
|
373
|
+
echo "${doc_name} ${PROJECT_NAME} is ready to install.";
|
|
374
|
+
echo "Go to ${base_url}";
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
if [ "${LAUNCH}" == 'yes' ] ; then
|
|
378
|
+
ddev launch ;
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
cd ${WORKSPACE_ROOT}/${doc_name};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
# Create the default accounts with Drush. An account that is already there is left as it is, and
|
|
385
|
+
# a role the site does not have is said and skipped.
|
|
386
|
+
#
|
|
387
|
+
# The list is always this plugin's own: the workspace base still carries an older list under the
|
|
388
|
+
# same name, and its tools load that one just before they call this function.
|
|
389
|
+
function add_drupal_cms_users() {
|
|
390
|
+
drupal_cms_enter_project || return 1 ;
|
|
391
|
+
drupal_cms_default_users ;
|
|
392
|
+
local user user_name user_mail user_password user_role ;
|
|
393
|
+
for user in "${users[@]}" ; do
|
|
394
|
+
user_name="user_${user}_name"; user_mail="user_${user}_mail";
|
|
395
|
+
user_password="user_${user}_password"; user_role="user_${user}_role";
|
|
396
|
+
|
|
397
|
+
echo " ---------------------------------------------------------------- ";
|
|
398
|
+
echo " User name: ${!user_name}";
|
|
399
|
+
echo " User mail: ${!user_mail}";
|
|
400
|
+
echo " User password: ${!user_password}";
|
|
401
|
+
echo " User role: ${!user_role}";
|
|
402
|
+
echo " ================================================================= ";
|
|
403
|
+
|
|
404
|
+
if drupal_cms_user_exists "${!user_name}" ; then
|
|
405
|
+
echo " ${!user_name} is already there." ;
|
|
406
|
+
else
|
|
407
|
+
ddev drush user:create "${!user_name}" --mail="${!user_mail}" --password="${!user_password}" ;
|
|
408
|
+
fi
|
|
409
|
+
if [ "${!user_role}" == '_none_' ] ; then
|
|
410
|
+
echo " No role for this account." ;
|
|
411
|
+
elif drupal_cms_role_exists "${!user_role}" ; then
|
|
412
|
+
ddev drush user:role:add "${!user_role}" "${!user_name}" ;
|
|
413
|
+
else
|
|
414
|
+
echo " The site has no ${!user_role} role: ${!user_name} is left without one." ;
|
|
415
|
+
fi
|
|
416
|
+
done
|
|
417
|
+
cd ${WORKSPACE_ROOT}/${doc_name};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Delete the default accounts, and what they wrote.
|
|
421
|
+
function cancel_drupal_cms_users() {
|
|
422
|
+
drupal_cms_enter_project || return 1 ;
|
|
423
|
+
drupal_cms_default_users ;
|
|
424
|
+
local user user_name ;
|
|
425
|
+
for user in "${users[@]}" ; do
|
|
426
|
+
user_name="user_${user}_name";
|
|
427
|
+
echo " ---------------------------------------------------------------- ";
|
|
428
|
+
echo " User name: ${!user_name}";
|
|
429
|
+
echo " ================================================================= ";
|
|
430
|
+
if drupal_cms_user_exists "${!user_name}" ; then
|
|
431
|
+
ddev drush user:cancel --delete-content "${!user_name}" --yes ;
|
|
432
|
+
else
|
|
433
|
+
echo " ${!user_name} is not there." ;
|
|
434
|
+
fi
|
|
435
|
+
done
|
|
436
|
+
cd ${WORKSPACE_ROOT}/${doc_name};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
# What cmd-tools-drupal-cms-cli.sh does: the first of --run, --recipe, --update, --cache-rebuild,
|
|
440
|
+
# --login, --add-users, --cancel-users that was asked for; with none of them, what the site is.
|
|
441
|
+
function drupal_cms_cli_tool() {
|
|
442
|
+
trim_project_name ;
|
|
443
|
+
drupal_cms_enter_project || exit 1 ;
|
|
444
|
+
local project_path="${WORKSPACE_ROOT}/${doc_name}/${PROJECT_NAME}" ;
|
|
445
|
+
|
|
446
|
+
if ! ddev describe -j 2>/dev/null | grep -q '"status":"running"' ; then
|
|
447
|
+
ddev start -y || exit 1 ;
|
|
448
|
+
fi
|
|
449
|
+
|
|
450
|
+
if [ ! "${RUN}" == '_none_' ] ; then
|
|
451
|
+
# Handed to the shell of the container as one line, so an argument in quotes stays one
|
|
452
|
+
# argument: --run "sql:query 'select name from users_field_data'".
|
|
453
|
+
ddev exec "drush ${RUN}" ;
|
|
454
|
+
exit $? ;
|
|
455
|
+
fi
|
|
456
|
+
|
|
457
|
+
if [ ! "${RECIPE}" == '_none_' ] ; then
|
|
458
|
+
local asked recorded recipe ;
|
|
459
|
+
asked="$(drupal_cms_recipe_list "${RECIPE}")" ;
|
|
460
|
+
drupal_cms_apply_recipes ${asked} || exit 1 ;
|
|
461
|
+
recorded="$(drupal_cms_recorded_recipes "${project_path}")" ;
|
|
462
|
+
for recipe in ${asked} ; do
|
|
463
|
+
case " ${recorded} " in *" ${recipe} "*) : ;; *) recorded="${recorded}${recorded:+ }${recipe}" ;; esac
|
|
464
|
+
done
|
|
465
|
+
drupal_cms_record_build "" "${recorded}" ;
|
|
466
|
+
ddev drush cache:rebuild ;
|
|
467
|
+
echo "Applied: ${asked}. The test suite reads the recipes of the site from the container: ddev restart before the next run." ;
|
|
468
|
+
elif [ "${UPDATE}" == 'yes' ] ; then
|
|
469
|
+
ddev composer update --with-all-dependencies --no-interaction || exit 1 ;
|
|
470
|
+
ddev drush updatedb --yes ;
|
|
471
|
+
ddev drush cache:rebuild ;
|
|
472
|
+
elif [ "${CACHE_REBUILD}" == 'yes' ] ; then
|
|
473
|
+
ddev drush cache:rebuild ;
|
|
474
|
+
elif [ "${LOGIN}" == 'yes' ] ; then
|
|
475
|
+
echo "A one-time login link for the site's first account:" ;
|
|
476
|
+
ddev drush user:login --no-browser ;
|
|
477
|
+
elif [ "${ADD_USERS}" == 'yes' ] ; then
|
|
478
|
+
add_drupal_cms_users ;
|
|
479
|
+
elif [ "${CANCEL_USERS}" == 'yes' ] ; then
|
|
480
|
+
cancel_drupal_cms_users ;
|
|
481
|
+
else
|
|
482
|
+
echo "${doc_name}/${PROJECT_NAME}:" ;
|
|
483
|
+
ddev drush status --fields=drupal-version,uri,install-profile,theme,admin-theme,php-version,db-driver,bootstrap ;
|
|
484
|
+
echo "" ; echo "Site template: $(grep -E 'DRUPAL_SITE_TEMPLATE=' .ddev/config.yaml 2>/dev/null | head -1 | sed -E 's/.*DRUPAL_SITE_TEMPLATE=//; s/"?[[:space:]]*$//')" ;
|
|
485
|
+
echo "Recipes applied with these tools: $(drupal_cms_recorded_recipes "${project_path}")" ;
|
|
486
|
+
echo "" ; echo "Recipes in the code:" ; ls recipes 2>/dev/null | tr '\n' ' ' ; echo "" ;
|
|
487
|
+
echo "" ; echo "Accounts:" ;
|
|
488
|
+
ddev drush sql:query "SELECT u.name, u.mail, COALESCE(GROUP_CONCAT(r.roles_target_id), '') AS roles FROM users_field_data u LEFT JOIN user__roles r ON r.entity_id = u.uid WHERE u.uid > 0 GROUP BY u.uid, u.name, u.mail ORDER BY u.uid" ;
|
|
489
|
+
echo "" ; echo "Pending database updates:" ;
|
|
490
|
+
ddev drush updatedb:status ;
|
|
491
|
+
fi
|
|
492
|
+
cd ${WORKSPACE_ROOT}/${doc_name};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
# The step pack in the testing stack of a Drupal CMS project. The core's testing_configure calls
|
|
496
|
+
# this in the project folder once webship-js is scaffolded and before the dependencies are
|
|
497
|
+
# installed; for a project that is not Drupal CMS it changes nothing.
|
|
498
|
+
#
|
|
499
|
+
# It adds the pack (and the Drupal core pack it depends on) to package.json, lists its step
|
|
500
|
+
# definitions in cucumber.js after webship-js's own — the pack loads the core pack by itself, so
|
|
501
|
+
# that one is not listed — and gives cucumber.js what the pack reads: the users registry of the
|
|
502
|
+
# default accounts (a handle a scenario reads, mapped to an account; never the workspace account),
|
|
503
|
+
# the site template and the recipes of the site, the cookie consent and the third-party requests.
|
|
504
|
+
# It writes a first feature when the project has none of its own. Every part is skipped when it is
|
|
505
|
+
# already there, so a second run adds what the first missed and never resets a suite.
|
|
506
|
+
function testing_configure_drupal_cms() {
|
|
507
|
+
local project_path="$1";
|
|
508
|
+
drupal_cms_is_project "${project_path}" || return 0 ;
|
|
509
|
+
if [ ! -f "${project_path}/cucumber.js" ] || [ ! -f "${project_path}/package.json" ] ; then
|
|
510
|
+
echo " Drupal CMS: cucumber.js is not here yet — run the testing setup again once webship-js is scaffolded." ;
|
|
511
|
+
return 1 ;
|
|
512
|
+
fi
|
|
513
|
+
|
|
514
|
+
echo "Adding the Drupal CMS step pack (${DRUPAL_CMS_STEPS_PACKAGE} ${DRUPAL_CMS_STEPS_VERSION}) and the Drupal core pack it uses.";
|
|
515
|
+
drupal_cms_default_users ;
|
|
516
|
+
# handle | the name of its environment variables | account | password | mail | role
|
|
517
|
+
local registry="" user user_name user_password user_mail user_role handle env ;
|
|
518
|
+
for user in "${users[@]}" ; do
|
|
519
|
+
user_name="user_${user}_name"; user_password="user_${user}_password";
|
|
520
|
+
user_mail="user_${user}_mail"; user_role="user_${user}_role";
|
|
521
|
+
# The pack's features call the accounts "admin", "content editor" and "authenticated".
|
|
522
|
+
case "${user}" in
|
|
523
|
+
administrator) handle="admin" ; env="ADMIN" ;;
|
|
524
|
+
content_editor) handle="content editor" ; env="EDITOR" ;;
|
|
525
|
+
*) handle="${user}" ; env="${user^^}" ;;
|
|
526
|
+
esac
|
|
527
|
+
registry="${registry}${handle}|${env}|${!user_name}|${!user_password}|${!user_mail}|${!user_role}"$'\n' ;
|
|
528
|
+
done
|
|
529
|
+
|
|
530
|
+
python3 - "${project_path}" "${DRUPAL_CMS_STEPS_PACKAGE}" "${DRUPAL_CMS_STEPS_VERSION}" \
|
|
531
|
+
"${DRUPAL_CMS_CORE_STEPS_PACKAGE}" "${DRUPAL_CMS_CORE_STEPS_VERSION}" "${registry}" <<'PYEOF' || return 1
|
|
532
|
+
import json, re, sys
|
|
533
|
+
|
|
534
|
+
project, pack, version, core_pack, core_version, registry = sys.argv[1:7]
|
|
535
|
+
|
|
536
|
+
# package.json: the pack and the core pack it uses as dev dependencies, at the release lines this
|
|
537
|
+
# plugin was proven on.
|
|
538
|
+
pkg_file = project + '/package.json'
|
|
539
|
+
pkg = json.load(open(pkg_file))
|
|
540
|
+
changed = False
|
|
541
|
+
for name, wanted in ((pack, version), (core_pack, core_version)):
|
|
542
|
+
if name not in pkg.get('dependencies', {}) and name not in pkg.get('devDependencies', {}):
|
|
543
|
+
pkg.setdefault('devDependencies', {})[name] = wanted
|
|
544
|
+
changed = True
|
|
545
|
+
print(' package.json: %s %s' % (name, wanted))
|
|
546
|
+
if changed:
|
|
547
|
+
open(pkg_file, 'w').write(json.dumps(pkg, indent=2) + '\n')
|
|
548
|
+
|
|
549
|
+
# cucumber.js: the pack's step definitions after webship-js's own, and what the pack reads.
|
|
550
|
+
cucumber_file = project + '/cucumber.js'
|
|
551
|
+
src = open(cucumber_file).read()
|
|
552
|
+
before = src
|
|
553
|
+
|
|
554
|
+
if ('node_modules/%s/' % pack) not in src:
|
|
555
|
+
line = "'node_modules/%s/tests/step-definitions/**/*.js'," % pack
|
|
556
|
+
core = re.search(r"^([ \t]*)'node_modules/webship-js/tests/step-definitions/[^']*',.*$", src, re.M)
|
|
557
|
+
opening = re.search(r"^([ \t]*)require:[ \t]*\[[ \t]*$", src, re.M)
|
|
558
|
+
if core:
|
|
559
|
+
src = src[:core.end()] + '\n' + core.group(1) + line + src[core.end():]
|
|
560
|
+
elif opening:
|
|
561
|
+
src = src[:opening.end()] + '\n' + opening.group(1) + ' ' + line + src[opening.end():]
|
|
562
|
+
else:
|
|
563
|
+
sys.exit(" cucumber.js has no require list this can add to. Add this line to it:\n " + line)
|
|
564
|
+
print(' cucumber.js: the step definitions of %s' % pack)
|
|
565
|
+
|
|
566
|
+
world = re.search(r"^([ \t]*)worldParameters:[ \t]*\{[ \t]*$", src, re.M)
|
|
567
|
+
block = []
|
|
568
|
+
if world:
|
|
569
|
+
pad = world.group(1) + ' '
|
|
570
|
+
if not re.search(r"^[ \t]*siteTemplate:", src, re.M):
|
|
571
|
+
block += [
|
|
572
|
+
pad + "// The site template the site was installed with and the add-on recipes applied to it. The builder",
|
|
573
|
+
pad + "// keeps both in the environment of the project's container; a scenario that needs another",
|
|
574
|
+
pad + "// template or recipe is skipped.",
|
|
575
|
+
pad + "siteTemplate: process.env.DRUPAL_SITE_TEMPLATE || 'drupal_cms_starter',",
|
|
576
|
+
pad + "recipes: (process.env.DRUPAL_CMS_RECIPES || '').split(/[\\s,]+/).filter(Boolean),",
|
|
577
|
+
pad + "// Requests to other hosts are stopped, so a run does not wait for a third party.",
|
|
578
|
+
pad + "blockThirdPartyRequests: process.env.ALLOW_THIRD_PARTY_REQUESTS !== '1',",
|
|
579
|
+
pad + "// The cookie consent is accepted before every scenario, so the notice never covers what a",
|
|
580
|
+
pad + "// scenario clicks; a scenario about the consent resets it with a step.",
|
|
581
|
+
pad + "acceptCookieConsent: process.env.KEEP_COOKIE_CONSENT !== '1',",
|
|
582
|
+
]
|
|
583
|
+
print(' cucumber.js: the site template, the recipes and the cookie consent')
|
|
584
|
+
if not re.search(r"^[ \t]*users:[ \t]*\{", src, re.M):
|
|
585
|
+
rows = [r.split('|') for r in registry.splitlines() if r.strip()]
|
|
586
|
+
block += [
|
|
587
|
+
pad + "// The users registry: a handle a scenario reads, mapped to an account of this site (the default",
|
|
588
|
+
pad + "// accounts: cmd-tools-add-users.sh). Override one with DRUPAL_<NAME>_USER, DRUPAL_<NAME>_PASS and",
|
|
589
|
+
pad + "// DRUPAL_<NAME>_MAIL (ADMIN, EDITOR, AUTHENTICATED).",
|
|
590
|
+
pad + "users: {",
|
|
591
|
+
]
|
|
592
|
+
for handle, env, name, password, mail, role in rows:
|
|
593
|
+
key = handle if re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', handle) else "'%s'" % handle
|
|
594
|
+
roles = '[]' if role == '_none_' else "['%s']" % role
|
|
595
|
+
block.append(pad + " %s: { username: process.env.DRUPAL_%s_USER || '%s', password: process.env.DRUPAL_%s_PASS || '%s', email: process.env.DRUPAL_%s_MAIL || '%s', roles: %s },"
|
|
596
|
+
% (key, env, name, env, password, env, mail, roles))
|
|
597
|
+
block.append(pad + "},")
|
|
598
|
+
print(' cucumber.js: the users registry (%s)' % ', '.join(r[0] for r in rows))
|
|
599
|
+
if block:
|
|
600
|
+
src = src[:world.end()] + '\n' + '\n'.join(block) + src[world.end():]
|
|
601
|
+
elif not re.search(r"^[ \t]*users:[ \t]*\{", src, re.M):
|
|
602
|
+
sys.exit(" cucumber.js has no worldParameters block to add the users registry to.")
|
|
603
|
+
|
|
604
|
+
if src != before:
|
|
605
|
+
open(cucumber_file, 'w').write(src)
|
|
606
|
+
PYEOF
|
|
607
|
+
|
|
608
|
+
# A first feature, when the project has none for the site yet: the front page, the login page,
|
|
609
|
+
# each default account, the dashboard, a Canvas page and the cookie consent.
|
|
610
|
+
local feature="${WORKSPACE_SCRIPTS}/plugins/drupal-cms/site.feature" ;
|
|
611
|
+
if [ -f "${feature}" ] && [ ! -f "${project_path}/tests/features/site.feature" ] ; then
|
|
612
|
+
mkdir -p "${project_path}/tests/features" && cp "${feature}" "${project_path}/tests/features/site.feature" \
|
|
613
|
+
&& echo " tests/features/site.feature: a first feature for the site" ;
|
|
614
|
+
fi
|
|
615
|
+
|
|
616
|
+
# The scenarios log in as the default accounts: make sure an installed site has them.
|
|
617
|
+
if drupal_cms_is_installed ; then
|
|
618
|
+
( add_drupal_cms_users > /dev/null ) && echo " the default accounts are on the site (${users[*]})" ;
|
|
619
|
+
fi
|
|
620
|
+
}
|