@webship.co/workspace 1.0.3 → 1.0.5
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/core/docs/releases.md +1 -1
- package/core/scripts/args/arg-website.sh +2 -2
- package/core/scripts/functions/fun-build-distribution.sh +48 -39
- package/core/scripts/functions/fun-build-webships.sh +3 -2
- package/core/scripts/functions/fun-build-website.sh +19 -7
- package/core/scripts/functions/fun-distribution-cucumber.sh +18 -3
- package/core/scripts/functions/fun-distribution-webship.sh +4 -2
- package/core/scripts/functions/fun-distribution-website.sh +4 -2
- package/core/scripts/functions/fun-users.sh +11 -0
- package/core/scripts/tests/cmd-smoke-test.sh +15 -1
- package/core/workspace-app/docs/README.md +4 -3
- package/core/workspace-app/docs/USER-GUIDE.md +2 -2
- package/core/workspace-app/docs/screenshots/assistant/avatar-board.png +0 -0
- package/core/workspace-app/docs/screenshots/assistant/avatar-colours.png +0 -0
- package/core/workspace-app/docs/screenshots/assistant/avatar-icons.png +0 -0
- package/core/workspace-app/docs/screenshots/assistant/avatar-moves.png +0 -0
- package/core/workspace-app/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
- package/core/workspace-app/docs/screenshots/create-card/create-card.png +0 -0
- package/core/workspace-app/docs/screenshots/create-card/start-ai.png +0 -0
- package/core/workspace-app/docs/screenshots/guide-clone-address.png +0 -0
- package/core/workspace-app/docs/screenshots/guide-clone-worktree-off.png +0 -0
- package/core/workspace-app/docs/screenshots/guide-clone-worktree-on.png +0 -0
- package/core/workspace-app/docs/screenshots/guide-ddev-menu.png +0 -0
- package/core/workspace-app/docs/screenshots/guide-domains.png +0 -0
- package/core/workspace-app/docs/screenshots/guide-home.png +0 -0
- package/core/workspace-app/docs/screenshots/guide-ide-menu.png +0 -0
- package/core/workspace-app/docs/screenshots/guide-remove-refusal.png +0 -0
- package/core/workspace-app/docs/screenshots/guide-rows-worktree.png +0 -0
- package/core/workspace-app/docs/screenshots/guide-screens.png +0 -0
- package/core/workspace-app/docs/screenshots/guide-settings.png +0 -0
- package/core/workspace-app/docs/screenshots/guide-workspace-page.png +0 -0
- package/core/workspace-app/docs/screenshots/home/card-settings.png +0 -0
- package/core/workspace-app/docs/screenshots/home/drag-reorder.png +0 -0
- package/core/workspace-app/docs/screenshots/home/edit-card.png +0 -0
- package/core/workspace-app/docs/screenshots/images/claude-code-image.png +0 -0
- package/core/workspace-app/docs/screenshots/images/edit-with-ai.png +0 -0
- package/core/workspace-app/docs/screenshots/images/gallery.png +0 -0
- package/core/workspace-app/docs/screenshots/images/generate-description.png +0 -0
- package/core/workspace-app/docs/screenshots/images/generate-plan.png +0 -0
- package/core/workspace-app/docs/screenshots/images/providers.png +0 -0
- package/core/workspace-app/docs/screenshots/images/svg-form.png +0 -0
- package/core/workspace-app/docs/screenshots/images/svg-result.png +0 -0
- package/core/workspace-app/docs/screenshots/images/upload.png +0 -0
- package/core/workspace-app/docs/screenshots/new-folder/2-form.png +0 -0
- package/core/workspace-app/docs/screenshots/new-folder/8-ddev-options.png +0 -0
- package/core/workspace-app/docs/screenshots/new-folder/files-folder.png +0 -0
- package/core/workspace-app/docs/screenshots/projects/ddev-custom-commands.png +0 -0
- package/core/workspace-app/docs/screenshots/projects/state-chip-stop.png +0 -0
- package/core/workspace-app/docs/screenshots/readme/dashboard-dark.png +0 -0
- package/core/workspace-app/docs/screenshots/readme/dashboard-light.png +0 -0
- package/core/workspace-app/docs/screenshots/readme/dev-dark.png +0 -0
- package/core/workspace-app/docs/screenshots/readme/dev-light.png +0 -0
- package/core/workspace-app/docs/screenshots/readme/docs-dark.png +0 -0
- package/core/workspace-app/docs/screenshots/readme/docs-light.png +0 -0
- package/core/workspace-app/docs/screenshots/readme/videos-dark.png +0 -0
- package/core/workspace-app/docs/screenshots/readme/videos-light.png +0 -0
- package/core/workspace-app/docs/screenshots/screens-menu.png +0 -0
- package/core/workspace-app/docs/screenshots/screens-new.png +0 -0
- package/core/workspace-app/docs/screenshots/settings/auth-google-lists.png +0 -0
- package/core/workspace-app/docs/screenshots/settings/delete-1-row.png +0 -0
- package/core/workspace-app/docs/screenshots/settings/delete-2-confirm.png +0 -0
- package/core/workspace-app/docs/screenshots/settings/delete-3-type-name.png +0 -0
- package/core/workspace-app/docs/screenshots/settings/delete-4-type-yes.png +0 -0
- package/core/workspace-app/package.json +1 -1
- package/core/workspace-app/public/avatar/druplicon.vrm +0 -0
- package/core/workspace-app/server.js +1 -1
- package/core/workspace-app/tests/unit/build-distribution.test.js +44 -0
- package/core/workspace-app/tests/unit/build-website.test.js +116 -0
- package/core/workspace-app/tests/unit/distribution-users.test.js +109 -0
- package/core/workspace-app/tests/unit/files-route-stream.test.js +16 -0
- package/package.json +1 -1
- package/core/workspace-app/docs/screenshots/assistant/avatar-coffee.png +0 -0
- package/core/workspace-app/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
- package/core/workspace-app/docs/screenshots/assistant/avatar-status.png +0 -0
- package/core/workspace-app/docs/screenshots/readme/test-report-dark.png +0 -0
- package/core/workspace-app/docs/screenshots/readme/test-report-light.png +0 -0
- package/core/workspace-app/docs/screenshots/readme/test-scenarios.png +0 -0
- package/core/workspace-app/docs/screenshots/readme/test-steps.png +0 -0
package/core/docs/releases.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
-
Every package is versioned together: `@webship.co/workspace` (the core) and every `@webship.co/workspace-<name>`. Versions so far: 1.0.0-alpha1 (the first publish), 1.0.0-beta1, 1.0.0-beta2, 1.0.0-rc1, 1.0.0, 1.0.1 (the core and the git, ide and graph plugins only), 1.0.2, 1.0.3 (the core only).
|
|
3
|
+
Every package is versioned together: `@webship.co/workspace` (the core) and every `@webship.co/workspace-<name>`. Versions so far: 1.0.0-alpha1 (the first publish), 1.0.0-beta1, 1.0.0-beta2, 1.0.0-rc1, 1.0.0, 1.0.1 (the core and the git, ide and graph plugins only), 1.0.2, 1.0.3, 1.0.4 and 1.0.5 (the core only).
|
|
4
4
|
|
|
5
5
|
## How a change lands
|
|
6
6
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
3
|
# The Website project template (drupal/website) is installed by the Webship
|
|
4
|
-
# installer, which offers the
|
|
4
|
+
# installer, which offers the five site templates the project ships. Pick one.
|
|
5
5
|
|
|
6
6
|
argparse "$@" <<EOF || exit 1
|
|
7
7
|
parser.add_argument('PROJECT_NAME',
|
|
8
8
|
help='The name of the project. It becomes https://<PROJECT_NAME>.ddev.site')
|
|
9
9
|
parser.add_argument('-t', '--template',
|
|
10
10
|
default="website_starter",
|
|
11
|
-
choices=['website_starter', 'webship_starter', 'webship_portal'],
|
|
11
|
+
choices=['website_starter', 'webship_starter', 'webship_portal', 'webapi_starter', 'webships_starter'],
|
|
12
12
|
help='The site template the Webship installer installs [default %(default)s].')
|
|
13
13
|
parser.add_argument('-a', '--add-users',
|
|
14
14
|
action='store_true',
|
|
@@ -8,6 +8,44 @@ function composer_install_retry() {
|
|
|
8
8
|
ddev composer install --no-interaction ;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
# composer create-project in the current (empty) project folder, for every build family:
|
|
12
|
+
# composer_create_project <template>:<version> [more create-project options]
|
|
13
|
+
#
|
|
14
|
+
# A release line may need two things before its first install, or composer cannot resolve it at all:
|
|
15
|
+
# site_composer_requires root requirements, e.g. an alias. a Drupal 10 line may ask for a package
|
|
16
|
+
# that moved to Drupal 11, so the line aliases the last Drupal 10 release.
|
|
17
|
+
# site_patches_ignore "<package> <patch-url>" pairs that webship/patches declares and the release
|
|
18
|
+
# already contains, so applying the patch fails (patches-ignore, added to composer.json).
|
|
19
|
+
# Both are declared by the builder as bash arrays. A builder that declares neither installs in one step as before.
|
|
20
|
+
# Returns 1 when the codebase could not be installed; the caller says which build stops and where.
|
|
21
|
+
function composer_create_project() {
|
|
22
|
+
local project_template="$1" ; shift ;
|
|
23
|
+
site_requires=("${site_composer_requires[@]}") ;
|
|
24
|
+
site_ignores=("${site_patches_ignore[@]}") ;
|
|
25
|
+
if [ -n "${site_requires[0]:-}${site_ignores[0]:-}" ] ; then
|
|
26
|
+
ddev composer create-project ${project_template} . --no-interaction --no-install --no-scripts "$@" || return 1 ;
|
|
27
|
+
if [ -n "${site_requires[0]:-}" ] ; then
|
|
28
|
+
ddev composer require "${site_requires[@]}" --no-update --no-interaction || return 1 ;
|
|
29
|
+
fi
|
|
30
|
+
if [ -n "${site_ignores[0]:-}" ] ; then
|
|
31
|
+
patches_ignore_json=$(python3 -c 'import sys, json; d = {}; [d.setdefault("webship/patches", {}).setdefault(a.split(" ", 1)[0], []).append(a.split(" ", 1)[1]) for a in sys.argv[1:]]; print(json.dumps(d))' "${site_ignores[@]}") ;
|
|
32
|
+
ddev composer config --json extra.patches-ignore "${patches_ignore_json}" || return 1 ;
|
|
33
|
+
fi
|
|
34
|
+
if ! composer_install_retry || ! ddev composer run-script post-create-project-cmd ; then
|
|
35
|
+
echo "composer could not install ${project_template}." ;
|
|
36
|
+
return 1 ;
|
|
37
|
+
fi
|
|
38
|
+
elif ! ddev composer create-project ${project_template} . --no-interaction "$@" ; then
|
|
39
|
+
# When the project reached the install step, composer.json is already in place: install once more.
|
|
40
|
+
if [ -f composer.json ] && composer_install_retry && ddev composer run-script post-create-project-cmd ; then
|
|
41
|
+
echo "composer create-project needed a second install, which worked." ;
|
|
42
|
+
else
|
|
43
|
+
return 1 ;
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
return 0 ;
|
|
47
|
+
}
|
|
48
|
+
|
|
11
49
|
function build_distribution() {
|
|
12
50
|
# Include distribution functions.
|
|
13
51
|
source ${WORKSPACE_SCRIPTS}/functions/fun-distribution-${distribution_name}.sh || exit 1 ;
|
|
@@ -79,50 +117,21 @@ function build_distribution() {
|
|
|
79
117
|
|
|
80
118
|
# A failed create-project (e.g. an upstream patch no longer applying) must stop the
|
|
81
119
|
# build here — every step after this assumes a real vendor/autoload.php exists.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# site_patches_ignore "<package> <patch-url>" pairs that webship/patches declares and the release
|
|
87
|
-
# already contains, so applying the patch fails (patches-ignore, added to composer.json).
|
|
88
|
-
# Both are declared by the builder as bash arrays. A builder that declares neither installs in one step as before.
|
|
89
|
-
site_requires=("${site_composer_requires[@]}") ;
|
|
90
|
-
site_ignores=("${site_patches_ignore[@]}") ;
|
|
91
|
-
if [ -n "${site_requires[0]:-}${site_ignores[0]:-}" ] ; then
|
|
92
|
-
if ! ddev composer create-project ${distribution_project_template}:${site_version} . --no-interaction --no-install --no-scripts ${dev_stability:-} ; then
|
|
93
|
-
echo "composer create-project failed for ${distribution_project_template}:${site_version} — aborting, not installing." ;
|
|
94
|
-
cd ${WORKSPACE_ROOT}/${doc_name};
|
|
95
|
-
return 1 ;
|
|
96
|
-
fi
|
|
97
|
-
if [ -n "${site_requires[0]:-}" ] ; then
|
|
98
|
-
ddev composer require "${site_requires[@]}" --no-update --no-interaction || { cd ${WORKSPACE_ROOT}/${doc_name}; return 1 ; } ;
|
|
99
|
-
fi
|
|
100
|
-
if [ -n "${site_ignores[0]:-}" ] ; then
|
|
101
|
-
patches_ignore_json=$(python3 -c 'import sys, json; d = {}; [d.setdefault("webship/patches", {}).setdefault(a.split(" ", 1)[0], []).append(a.split(" ", 1)[1]) for a in sys.argv[1:]]; print(json.dumps(d))' "${site_ignores[@]}") ;
|
|
102
|
-
ddev composer config --json extra.patches-ignore "${patches_ignore_json}" || { cd ${WORKSPACE_ROOT}/${doc_name}; return 1 ; } ;
|
|
103
|
-
fi
|
|
104
|
-
if ! composer_install_retry || ! ddev composer run-script post-create-project-cmd ; then
|
|
105
|
-
echo "composer could not install ${distribution_project_template}:${site_version} — aborting, not installing." ;
|
|
106
|
-
cd ${WORKSPACE_ROOT}/${doc_name};
|
|
107
|
-
return 1 ;
|
|
108
|
-
fi
|
|
109
|
-
elif ! ddev composer create-project ${distribution_project_template}:${site_version} . --no-interaction ${dev_stability:-} ; then
|
|
110
|
-
# When the project reached the install step, composer.json is already in place: install once more.
|
|
111
|
-
if [ -f composer.json ] && composer_install_retry && ddev composer run-script post-create-project-cmd ; then
|
|
112
|
-
echo "composer create-project needed a second install, which worked." ;
|
|
113
|
-
else
|
|
114
|
-
echo "composer create-project failed for ${distribution_project_template}:${site_version} — aborting, not installing." ;
|
|
115
|
-
cd ${WORKSPACE_ROOT}/${doc_name};
|
|
116
|
-
return 1 ;
|
|
117
|
-
fi
|
|
120
|
+
if ! composer_create_project ${distribution_project_template}:${site_version} ${dev_stability:-} ; then
|
|
121
|
+
echo "composer create-project failed for ${distribution_project_template}:${site_version} — aborting, not installing." ;
|
|
122
|
+
cd ${WORKSPACE_ROOT}/${doc_name};
|
|
123
|
+
return 1 ;
|
|
118
124
|
fi
|
|
119
125
|
|
|
120
126
|
# Some project templates ship their own .ddev config that
|
|
121
127
|
# changes the docroot/database/webimage packages after our initial ddev
|
|
122
128
|
# config. Re-align: keep the database type we already provisioned, drop
|
|
123
|
-
# optional apt extras (avoids third-party repo key failures), keep our Node.js
|
|
124
|
-
#
|
|
125
|
-
|
|
129
|
+
# optional apt extras (avoids third-party repo key failures), keep our Node.js and the PHP
|
|
130
|
+
# this build asked for (--php-version or the builder's site_php_version: composer resolved the
|
|
131
|
+
# codebase on it, so the template's own php_version would stop drush on the platform check),
|
|
132
|
+
# and restart so the webserver serves the template's real docroot.
|
|
133
|
+
ddev config --database=mariadb:${provisioned_db_type} --webimage-extra-packages="" \
|
|
134
|
+
${php_version:+--php-version=${php_version}} --nodejs-version=$(ddev_nodejs_version) ;
|
|
126
135
|
ddev_telemetry_off ;
|
|
127
136
|
ddev_up restart ;
|
|
128
137
|
|
|
@@ -56,8 +56,9 @@ function webships_create_project() {
|
|
|
56
56
|
# the installer finds them in recipes/ without Composer unpacking them.
|
|
57
57
|
ddev composer config --json --merge extra.drupal-recipe-unpack.ignore '["drupal/webships_starter", "drupal/webapi_starter"]' ;
|
|
58
58
|
# The project template ships its own .ddev/config.yaml, which pins an older Node.js:
|
|
59
|
-
# re-align to the workspace's Node.js (24, like Drupal core) and
|
|
60
|
-
|
|
59
|
+
# re-align to the workspace's Node.js (24, like Drupal core) and to the PHP this build asked
|
|
60
|
+
# for, and restart on them.
|
|
61
|
+
ddev config --php-version=${PHP_VERSION:-${webships_php_version}} --nodejs-version=$(ddev_nodejs_version) --webimage-extra-packages="" ;
|
|
61
62
|
ddev restart ;
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -29,7 +29,10 @@ function website_create_project() {
|
|
|
29
29
|
ddev config --project-type=drupal --docroot=${distribution_webroot} --php-version=${PHP_VERSION:-${website_php_version}} --nodejs-version=$(ddev_nodejs_version) --project-name=${PROJECT_NAME} --auto ;
|
|
30
30
|
ddev start -y ;
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
# composer_create_project (fun-build-distribution.sh) honours the builder's site_composer_requires
|
|
33
|
+
# and site_patches_ignore, so a requirement or a patch of the template that no longer resolves or
|
|
34
|
+
# applies can be worked around from the builder, as in build_distribution.
|
|
35
|
+
if ! composer_create_project "$1" ; then
|
|
33
36
|
echo "composer create-project failed for $1 — aborting, nothing was installed." ;
|
|
34
37
|
cd ${WORKSPACE_ROOT}/${doc_name};
|
|
35
38
|
return 1 ;
|
|
@@ -39,9 +42,11 @@ function website_create_project() {
|
|
|
39
42
|
ddev composer config minimum-stability dev ;
|
|
40
43
|
ddev composer config prefer-stable true ;
|
|
41
44
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
+
# The project template ships its own .ddev/config.yaml, which replaces the one written above and
|
|
46
|
+
# pins an older Node.js: re-align to the workspace's Node.js (24, like Drupal core) and to the PHP
|
|
47
|
+
# version this build asked for, and restart on them.
|
|
48
|
+
ddev config --php-version=${PHP_VERSION:-${website_php_version}} --nodejs-version=$(ddev_nodejs_version) ;
|
|
49
|
+
ddev restart ;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
# -----------------------------------------------------------------------------
|
|
@@ -121,11 +126,11 @@ function build_website_project() {
|
|
|
121
126
|
|
|
122
127
|
website_create_project "${distribution_project_template}:${site_version}" || return 1 ;
|
|
123
128
|
|
|
124
|
-
# The Website project requires the Webship installer and
|
|
129
|
+
# The Website project requires the Webship installer and its five site
|
|
125
130
|
# templates. Add whatever the published template does not require yet.
|
|
126
131
|
local require_block missing=() ;
|
|
127
132
|
require_block=$(sed -n '/"require": {/,/}/p' composer.json) ;
|
|
128
|
-
for package in "drupal/webship:12.0.x-dev" "drupal/website_starter:1.0.x-dev" "drupal/webship_starter:1.0.x-dev" "drupal/webship_portal:1.0.x-dev" ; do
|
|
133
|
+
for package in "drupal/webship:12.0.x-dev" "drupal/website_starter:1.0.x-dev" "drupal/webship_starter:1.0.x-dev" "drupal/webship_portal:1.0.x-dev" "drupal/webapi_starter:2.0.x-dev" "drupal/webships_starter:2.0.x-dev" ; do
|
|
129
134
|
# The installer is drupal/webship or webship/webship; the pending fixes pin the latter.
|
|
130
135
|
if [ "${package%%:*}" == "drupal/webship" ] && { [ "${WITH_PENDING_FIXES}" == 'yes' ] || echo "${require_block}" | grep -q '"webship/webship"' ; } ; then
|
|
131
136
|
continue ;
|
|
@@ -136,7 +141,7 @@ function build_website_project() {
|
|
|
136
141
|
fi
|
|
137
142
|
done
|
|
138
143
|
|
|
139
|
-
# The codebase carries
|
|
144
|
+
# The codebase carries every site template, so it needs every pending fix.
|
|
140
145
|
website_add_pending_fixes webship $(website_pending_fixes_for webship_portal) ;
|
|
141
146
|
|
|
142
147
|
# Stage everything, then one full update, so no pin is held back by the lock file.
|
|
@@ -176,6 +181,13 @@ function build_website_site_template() {
|
|
|
176
181
|
|
|
177
182
|
website_add_pending_fixes $(website_pending_fixes_for ${site_template_name}) ;
|
|
178
183
|
|
|
184
|
+
# Keep the site templates as recipes, like drupal/website does: unpacking them into the root
|
|
185
|
+
# composer.json fails on aliased dev branches (the pending fixes). Only here: the recommended
|
|
186
|
+
# project has no such list, and this runs before the require that writes the lock file.
|
|
187
|
+
# drupal/website carries its own list; merging into it after its lock file was written left
|
|
188
|
+
# duplicates and a lock file that `composer validate --check-lock` calls out of date.
|
|
189
|
+
ddev composer config --json --merge extra.drupal-recipe-unpack.ignore '["drupal/website_starter", "drupal/webship_starter", "drupal/webship_portal"]' ;
|
|
190
|
+
|
|
179
191
|
if ! ddev composer require -W --no-interaction "drush/drush:^13" "${site_template_package}:${site_version}" ; then
|
|
180
192
|
echo "composer require ${site_template_package}:${site_version} failed — aborting before the install." ;
|
|
181
193
|
cd ${WORKSPACE_ROOT}/${doc_name};
|
|
@@ -11,9 +11,19 @@ function enable_cucumber_extra_components() {
|
|
|
11
11
|
echo "Enable Cucumber extra components.";
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
# The
|
|
14
|
+
# The six work roles of Cucumber exist only once their role modules are on, and a
|
|
15
|
+
# `drush site:install cucumber` build leaves them off. `add_users` runs this before it
|
|
16
|
+
# creates the accounts, so each one can be given its role on a fresh build.
|
|
17
|
+
function prepare_cucumber_users() {
|
|
18
|
+
echo "Enable the Cucumber role modules of the default users.";
|
|
19
|
+
ddev drush pm:enable --yes cucumber_user_role_developer cucumber_user_role_tester cucumber_user_role_analyst \
|
|
20
|
+
cucumber_user_role_designer cucumber_user_role_coordinator cucumber_user_role_product_owner ;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# The default set of users `add_users` creates for Cucumber (--add-users):
|
|
24
|
+
# one account per work role, and one with the Admin role of the profile.
|
|
15
25
|
function set_cucumber_users() {
|
|
16
|
-
users=(authenticated developer tester analyst coordinator product_owner administrator);
|
|
26
|
+
users=(authenticated developer tester analyst designer coordinator product_owner administrator);
|
|
17
27
|
user_authenticated_name="Authenticated user";
|
|
18
28
|
user_authenticated_mail="test.authenticated@webship.org";
|
|
19
29
|
user_authenticated_password="dD.123123ddd";
|
|
@@ -30,6 +40,10 @@ function set_cucumber_users() {
|
|
|
30
40
|
user_analyst_mail="test.analyst@webship.org";
|
|
31
41
|
user_analyst_password="dD.123123ddd";
|
|
32
42
|
user_analyst_role="analyst";
|
|
43
|
+
user_designer_name="Designer";
|
|
44
|
+
user_designer_mail="test.designer@webship.org";
|
|
45
|
+
user_designer_password="dD.123123ddd";
|
|
46
|
+
user_designer_role="designer";
|
|
33
47
|
user_coordinator_name="Coordinator";
|
|
34
48
|
user_coordinator_mail="test.coordinator@webship.org";
|
|
35
49
|
user_coordinator_password="dD.123123ddd";
|
|
@@ -41,5 +55,6 @@ function set_cucumber_users() {
|
|
|
41
55
|
user_administrator_name="Administrator";
|
|
42
56
|
user_administrator_mail="test.administrator@webship.org";
|
|
43
57
|
user_administrator_password="dD.123123ddd";
|
|
44
|
-
|
|
58
|
+
# Cucumber has no `administrator` role: its administration role is Admin (`admin`).
|
|
59
|
+
user_administrator_role="admin";
|
|
45
60
|
}
|
|
@@ -21,8 +21,10 @@ function set_webship_users() {
|
|
|
21
21
|
user_content_editor_mail="test.content_editor@webship.org";
|
|
22
22
|
user_content_editor_password="dD.123123ddd";
|
|
23
23
|
user_content_editor_role="content_editor";
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
# Not "Webmaster": the installer's own account is `webmaster` (account.name in settings.yml), and
|
|
25
|
+
# user names are case-insensitive, so that account could never be created.
|
|
26
|
+
user_administrator_name="Administrator";
|
|
27
|
+
user_administrator_mail="test.administrator@webship.org";
|
|
26
28
|
user_administrator_password="dD.123123ddd";
|
|
27
29
|
user_administrator_role="administrator";
|
|
28
30
|
}
|
|
@@ -12,8 +12,10 @@ function set_website_users() {
|
|
|
12
12
|
user_content_editor_mail="test.content_editor@webship.org";
|
|
13
13
|
user_content_editor_password="dD.123123ddd";
|
|
14
14
|
user_content_editor_role="content_editor";
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# Not "Webmaster": the installer's own account is `webmaster` (account.name in settings.yml), and
|
|
16
|
+
# user names are case-insensitive, so that account could never be created.
|
|
17
|
+
user_administrator_name="Administrator";
|
|
18
|
+
user_administrator_mail="test.administrator@webship.org";
|
|
17
19
|
user_administrator_password="dD.123123ddd";
|
|
18
20
|
user_administrator_role="administrator";
|
|
19
21
|
}
|
|
@@ -32,6 +32,17 @@ function add_users () {
|
|
|
32
32
|
|
|
33
33
|
cd ${WORKSPACE_ROOT}/${doc_name}/${PROJECT_NAME}/${distribution_webroot}/;
|
|
34
34
|
|
|
35
|
+
# A distribution whose roles only exist once something is switched on (Cucumber's work roles
|
|
36
|
+
# come from their role modules) declares prepare_<distribution>_users next to its list. It runs
|
|
37
|
+
# here, so the accounts get their roles on a fresh build and from cmd-tools-add-users.sh alike.
|
|
38
|
+
if declare -f prepare_${distribution_name}_users > /dev/null ; then
|
|
39
|
+
if ! prepare_${distribution_name}_users ; then
|
|
40
|
+
echo "The roles of the default users could not be prepared — no user was added." ;
|
|
41
|
+
cd ${WORKSPACE_ROOT}/${doc_name};
|
|
42
|
+
return 1 ;
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
|
|
35
46
|
for user in ${users[@]}
|
|
36
47
|
do
|
|
37
48
|
user_name="user_${user}_name";
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
# prints usage and exits before any real work),
|
|
9
9
|
# which exercises bootstrap.sh, parse_yaml, the
|
|
10
10
|
# workspace + distribution configs and arg-*.sh
|
|
11
|
-
# 3.
|
|
11
|
+
# 3. the last call — the function a script ends by calling exists, and a builder that
|
|
12
|
+
# ends with build_distribution has its install function
|
|
13
|
+
# 4. workspace-name — builder scripts must carry the "# workspace-name:"
|
|
12
14
|
# header the dashboard shows in Build dropdowns
|
|
13
15
|
#
|
|
14
16
|
# Exit code: number of failing scripts (0 = all green).
|
|
@@ -78,6 +80,18 @@ for ws in "${workspaces[@]}" commands core/scripts; do
|
|
|
78
80
|
done
|
|
79
81
|
fi
|
|
80
82
|
|
|
83
|
+
# 3b. A builder that ends with build_distribution is installed by install_<distribution>_with_drush,
|
|
84
|
+
# which build_distribution calls by name. A builder copied from another family can keep that last
|
|
85
|
+
# line while its own family has no such function (the Website family installs in
|
|
86
|
+
# build_website_project): build_distribution exists, so check 3 passes, and the build stops at the install.
|
|
87
|
+
if [ -z "$err" ] && grep -qE '^build_distribution ;$' "$f"; then
|
|
88
|
+
dist=$(grep -oE '^distribution_name="[a-z0-9_]+"' "$f" | head -1 | cut -d'"' -f2)
|
|
89
|
+
if [ -n "$dist" ] && ! grep -qE "^(function )?install_${dist}_with_drush ?\(\)" \
|
|
90
|
+
"${WORKSPACE_SCRIPTS}/functions/fun-distribution-${dist}.sh" 2>/dev/null; then
|
|
91
|
+
err="ends with build_distribution, but install_${dist}_with_drush() is not in fun-distribution-${dist}.sh"
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
|
|
81
95
|
# 4. Builder scripts must have the human-readable name header.
|
|
82
96
|
# A builder is cmd-<distribution><version>-project.sh. cmd-tool-*/cmd-tools-* are the
|
|
83
97
|
# workspace's own tools — projects/cmd-tool-backup-project.sh ends in -project.sh too,
|
|
@@ -247,6 +247,7 @@ Some releases change the container image; the notes say so, and then
|
|
|
247
247
|
| the hard rules and the operational traps | `docs/workspace-tips-and-rules.md` |
|
|
248
248
|
| what changed, dated | `worklogs/` |
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
The pictures in `screenshots/` are used by these guides; `assistant/` and `readme/` hold spare ones of
|
|
251
|
+
the assistant's avatar and of the pages in light and dark. All of them were taken on 13 November 2026
|
|
252
|
+
on a Webship Workspace with demo content, at 1920×1080 (the home page is taller, to show every card),
|
|
253
|
+
in light mode unless the name says dark. A pull request carries its own pictures on its branch.
|
|
@@ -399,7 +399,7 @@ Claude ends the way it would if you typed it, and the conversation is kept. When
|
|
|
399
399
|
project or folder that has one, the same menu offers **Resume Claude Code**, which resumes the last conversation
|
|
400
400
|
with `claude --resume` in that folder.
|
|
401
401
|
|
|
402
|
-

|
|
403
403
|
|
|
404
404
|
### Watch something long
|
|
405
405
|
|
|
@@ -459,7 +459,7 @@ Pick the **Provider** first: OpenAI, Google Gemini, xAI, Stability AI or Claude
|
|
|
459
459
|
quality fields change with it. A provider with no key on this machine is listed but greyed out, with the key
|
|
460
460
|
to set (`ai_providers.<provider>.api_key` in `core/config/settings.yml`).
|
|
461
461
|
|
|
462
|
-

|
|
463
463
|
|
|
464
464
|
**Claude Code** needs no key: it uses your own Claude Code login, so there is no API charge. It cannot paint
|
|
465
465
|
a photo, so it draws a flat illustration as an SVG, and the dashboard turns it into a PNG as well; you keep
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workspace-dashboard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "The Webship Workspace dashboard: a lean core (workspaces, builders, settings, the assistant shell, the plugin system) that every feature joins as a @webship.co/workspace-<name> plugin — required with npm, then activated, installed and configured.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"main": "server.js",
|
|
Binary file
|
|
@@ -7875,7 +7875,7 @@ Summarise what you changed and how to verify it`,
|
|
|
7875
7875
|
// An SVG is a document that can carry script; shown on its own it runs none.
|
|
7876
7876
|
...(type === 'image/svg+xml' ? { 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; sandbox" } : {}),
|
|
7877
7877
|
});
|
|
7878
|
-
return fs.createReadStream(
|
|
7878
|
+
return fs.createReadStream(real).pipe(res);
|
|
7879
7879
|
}
|
|
7880
7880
|
const argsMatch = pathname.match(/^\/fragments\/([a-z0-9_-]+)\/builder-args$/);
|
|
7881
7881
|
if (argsMatch && isValidWorkspace(argsMatch[1])) {
|
|
@@ -59,6 +59,50 @@ test('every ddev config of a build pins Node.js 24, and the first one still gets
|
|
|
59
59
|
assert.ok(!configs[0].includes('ddev_telemetry_off'), configs[0]);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
test('the PHP version of the build is set again after create-project, where the template brings its own DDEV config', () => {
|
|
63
|
+
// The template's .ddev/config.yaml replaces the builder's: composer resolved on 8.4, the project ran the
|
|
64
|
+
// template's 8.3, and drush site:install stopped on "Your Composer dependencies require a PHP version".
|
|
65
|
+
for (const declare of ['PHP_VERSION=8.4', 'site_php_version=8.4', 'PHP_VERSION=8.4; site_php_version=8.5']) {
|
|
66
|
+
const { calls, code } = build({ declare });
|
|
67
|
+
assert.strictEqual(code, 0);
|
|
68
|
+
const configs = calls.filter((c) => c.startsWith('config '));
|
|
69
|
+
assert.strictEqual(configs.length, 2, configs.join('\n'));
|
|
70
|
+
for (const c of configs) assert.ok(c.includes('--php-version=8.4'), `${declare}: ${c}`);
|
|
71
|
+
assert.ok(index(calls, 'composer create-project') < calls.indexOf(configs[1]), 'the second one follows create-project');
|
|
72
|
+
assert.ok(configs[1].includes('--database=mariadb:11.8') && configs[1].includes('--nodejs-version=24'), configs[1]);
|
|
73
|
+
}
|
|
74
|
+
// Nobody asked for a version: DDEV's default before, the template's own after, as it was.
|
|
75
|
+
const plain = build();
|
|
76
|
+
for (const c of plain.calls.filter((x) => x.startsWith('config '))) assert.ok(!c.includes('--php-version'), c);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('the Webships builder sets its PHP version again after create-project too', () => {
|
|
80
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-webships-'));
|
|
81
|
+
const log = path.join(dir, 'ddev.log');
|
|
82
|
+
fs.mkdirSync(path.join(dir, 'ws'));
|
|
83
|
+
const WEBSHIPS = path.join(path.dirname(FUNCTION), 'fun-build-webships.sh');
|
|
84
|
+
const run = (declare) => {
|
|
85
|
+
fs.rmSync(log, { force: true });
|
|
86
|
+
const script = `
|
|
87
|
+
WORKSPACE_ROOT=${dir}; doc_name=ws; PROJECT_NAME=p1; distribution_webroot=web; ${declare}
|
|
88
|
+
ddev() { printf '%s\\n' "$*" >> ${log}; return 0; }
|
|
89
|
+
drop_database() { :; }
|
|
90
|
+
source ${NODEJS}; source ${WEBSHIPS}
|
|
91
|
+
webships_create_project drupal/stub:1.0.x-dev; echo "exit=$?"
|
|
92
|
+
`;
|
|
93
|
+
const out = spawnSync('bash', ['-c', script], { cwd: dir, encoding: 'utf8' }).stdout;
|
|
94
|
+
assert.match(out, /exit=0/);
|
|
95
|
+
return fs.readFileSync(log, 'utf8').trim().split('\n');
|
|
96
|
+
};
|
|
97
|
+
for (const [declare, version] of [['PHP_VERSION=8.5', '8.5'], ['', '8.4']]) {
|
|
98
|
+
const calls = run(declare);
|
|
99
|
+
const configs = calls.filter((c) => c.startsWith('config '));
|
|
100
|
+
assert.strictEqual(configs.length, 2, configs.join('\n'));
|
|
101
|
+
for (const c of configs) assert.ok(c.includes(`--php-version=${version}`) && c.includes('--nodejs-version=24'), c);
|
|
102
|
+
assert.ok(index(calls, 'composer create-project') < calls.indexOf(configs[1]));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
62
106
|
test('a builder that declares site_nodejs_version gets that version instead', () => {
|
|
63
107
|
const { calls } = build({ declare: 'site_nodejs_version=22' });
|
|
64
108
|
const configs = calls.filter((c) => c.startsWith('config '));
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the Website builders run, in what order.
|
|
3
|
+
*
|
|
4
|
+
* ddev and the helpers around it are stubbed, so the test reads the calls rather than building a site.
|
|
5
|
+
* build_website_project (drupal/website, installed by the Webship installer) had no way to add a root
|
|
6
|
+
* requirement or to ignore a patch before the first install, the way build_distribution has.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const assert = require('node:assert');
|
|
10
|
+
const { test } = require('node:test');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { spawnSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const FUNCTIONS = path.join(__dirname, '..', '..', '..', 'scripts', 'functions');
|
|
17
|
+
|
|
18
|
+
// Runs a Website build function with a fake ddev that records its arguments, one call per line.
|
|
19
|
+
function build({ declare = '', run = 'build_website_project' } = {}) {
|
|
20
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-website-'));
|
|
21
|
+
const log = path.join(dir, 'ddev.log');
|
|
22
|
+
fs.mkdirSync(path.join(dir, 'ws'));
|
|
23
|
+
const script = `
|
|
24
|
+
WORKSPACE_ROOT=${dir}; doc_name=ws; PROJECT_NAME=p1
|
|
25
|
+
distribution_name=website; distribution_title=Website; distribution_webroot=web; distribution_project_template=drupal/website; site_version=12.0.x-dev
|
|
26
|
+
account_name=admin; account_pass=pass; account_mail=admin@example.com
|
|
27
|
+
${declare}
|
|
28
|
+
ddev() { printf '%s\\n' "$*" >> ${log}; return 0; }
|
|
29
|
+
drop_database() { :; }; set_default_settings() { :; }; drush_set_debug_on() { :; }; drush_cr() { :; }; add_users() { :; }
|
|
30
|
+
source ${FUNCTIONS}/fun-build-distribution.sh; source ${FUNCTIONS}/fun-ddev-nodejs.sh; source ${FUNCTIONS}/fun-build-website.sh
|
|
31
|
+
${run}; echo "exit=$?"
|
|
32
|
+
`;
|
|
33
|
+
const res = spawnSync('bash', ['-c', script], { cwd: dir, encoding: 'utf8' });
|
|
34
|
+
const calls = fs.existsSync(log) ? fs.readFileSync(log, 'utf8').trim().split('\n') : [];
|
|
35
|
+
return { calls, out: res.stdout, code: Number((res.stdout.match(/exit=(\d+)/) || [])[1]) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const index = (calls, needle) => calls.findIndex((c) => c.includes(needle));
|
|
39
|
+
|
|
40
|
+
test('a Website builder that declares nothing creates the project in one step and installs with the chosen template', () => {
|
|
41
|
+
const { calls, code } = build({ declare: 'TEMPLATE=webship_portal' });
|
|
42
|
+
assert.strictEqual(code, 0);
|
|
43
|
+
const create = calls.find((c) => c.startsWith('composer create-project'));
|
|
44
|
+
assert.strictEqual(create, 'composer create-project drupal/website:12.0.x-dev . --no-interaction');
|
|
45
|
+
assert.ok(index(calls, 'patches-ignore') < 0 && index(calls, 'composer install') < 0);
|
|
46
|
+
const install = calls.find((c) => c.startsWith('drush site:install'));
|
|
47
|
+
assert.ok(install.includes('site:install webship') && install.includes('installer_site_template_form.add_ons=webship_portal'), install);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('declared requirements and patches to ignore are applied between create-project and the first install', () => {
|
|
51
|
+
const { calls, code } = build({
|
|
52
|
+
declare: `site_composer_requires=("drupal/stub:1.2.3 as 1.2.x-dev"); site_patches_ignore=("drupal/pp https://x.test/a.patch")`,
|
|
53
|
+
});
|
|
54
|
+
assert.strictEqual(code, 0);
|
|
55
|
+
const create = index(calls, 'composer create-project');
|
|
56
|
+
assert.ok(calls[create].includes('--no-install') && calls[create].includes('--no-scripts'), calls[create]);
|
|
57
|
+
const order = ['composer create-project', 'composer require drupal/stub', 'composer config --json extra.patches-ignore', 'composer install', 'composer run-script post-create-project-cmd', 'drush site:install']
|
|
58
|
+
.map((n) => index(calls, n));
|
|
59
|
+
assert.ok(order.every((i) => i >= 0), calls.join(' | '));
|
|
60
|
+
assert.deepStrictEqual(order, [...order].sort((a, b) => a - b), `in order: ${calls.join(' | ')}`);
|
|
61
|
+
assert.strictEqual(calls[order[2]], 'composer config --json extra.patches-ignore {"webship/patches": {"drupal/pp": ["https://x.test/a.patch"]}}');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('a site template build on the recommended project takes the same declarations', () => {
|
|
65
|
+
const { calls, code } = build({
|
|
66
|
+
declare: `site_template_name=website_starter; site_template_title="Website Starter"; site_template_package=drupal/website_starter; site_version=1.0.x-dev; site_patches_ignore=("drupal/pp https://x.test/a.patch")`,
|
|
67
|
+
run: 'build_website_site_template',
|
|
68
|
+
});
|
|
69
|
+
assert.strictEqual(code, 0);
|
|
70
|
+
const create = calls.find((c) => c.startsWith('composer create-project'));
|
|
71
|
+
assert.ok(create.startsWith('composer create-project drupal/recommended-project:') && create.includes('--no-install'), create);
|
|
72
|
+
assert.ok(index(calls, 'extra.patches-ignore') > 0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('the Node.js and PHP of the build are set again after create-project, where the template brings its own DDEV config', () => {
|
|
76
|
+
const { calls, code } = build({ declare: 'PHP_VERSION=8.5' });
|
|
77
|
+
assert.strictEqual(code, 0);
|
|
78
|
+
const create = index(calls, 'composer create-project');
|
|
79
|
+
const configs = calls.map((c, i) => [c, i]).filter(([c]) => c.startsWith('config '));
|
|
80
|
+
assert.strictEqual(configs.length, 2, configs.map(([c]) => c).join('\n'));
|
|
81
|
+
assert.ok(configs[0][1] < create && configs[1][1] > create, 'one before create-project, one after');
|
|
82
|
+
for (const [c] of configs) assert.ok(c.includes('--nodejs-version=24') && c.includes('--php-version=8.5'), c);
|
|
83
|
+
assert.ok(index(calls, 'restart') > configs[1][1], 'and the project restarts on them');
|
|
84
|
+
assert.ok(index(calls, 'restart') < index(calls, 'drush site:install'));
|
|
85
|
+
|
|
86
|
+
const declared = build({ declare: 'site_nodejs_version=22' });
|
|
87
|
+
for (const c of declared.calls.filter((x) => x.startsWith('config '))) assert.ok(c.includes('--nodejs-version=22') && c.includes('--php-version=8.4'), c);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('the Website project keeps the recipe-unpack ignore list its template ships, and a site template build sets one before the lock file', () => {
|
|
91
|
+
const project = build();
|
|
92
|
+
assert.strictEqual(project.code, 0);
|
|
93
|
+
assert.ok(index(project.calls, 'drupal-recipe-unpack') < 0, 'composer.json is not changed after the lock file was written');
|
|
94
|
+
|
|
95
|
+
const { calls, code } = build({
|
|
96
|
+
declare: 'site_template_name=website_starter; site_template_title="Website Starter"; site_template_package=drupal/website_starter; site_version=1.0.x-dev',
|
|
97
|
+
run: 'build_website_site_template',
|
|
98
|
+
});
|
|
99
|
+
assert.strictEqual(code, 0);
|
|
100
|
+
const ignore = index(calls, 'drupal-recipe-unpack');
|
|
101
|
+
assert.ok(ignore > 0 && calls[ignore].includes('"drupal/website_starter"'), calls[ignore]);
|
|
102
|
+
assert.ok(ignore < index(calls, 'composer require -W'), 'before the require that writes the lock file');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('--template offers the five site templates the Website project ships, and each one reaches the installer', () => {
|
|
106
|
+
const args = fs.readFileSync(path.join(FUNCTIONS, '..', 'args', 'arg-website.sh'), 'utf8');
|
|
107
|
+
const choices = args.match(/'--template',[\s\S]*?choices=\[([^\]]*)\]/)[1].split(',').map((c) => c.trim().replace(/'/g, ''));
|
|
108
|
+
assert.deepStrictEqual(choices, ['website_starter', 'webship_starter', 'webship_portal', 'webapi_starter', 'webships_starter']);
|
|
109
|
+
for (const template of choices) {
|
|
110
|
+
const { calls, code } = build({ declare: `TEMPLATE=${template}` });
|
|
111
|
+
assert.strictEqual(code, 0);
|
|
112
|
+
assert.ok(calls.find((c) => c.startsWith('drush site:install')).includes(`installer_site_template_form.add_ons=${template}`), template);
|
|
113
|
+
// No composer.json in the stub project: the builder adds what the template does not require, all five.
|
|
114
|
+
assert.ok(calls.find((c) => c.startsWith('composer require')).includes(`drupal/${template}:`), template);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default accounts of every distribution (`--add-users`, cmd-tools-add-users.sh) can be created.
|
|
3
|
+
*
|
|
4
|
+
* Each list is read from set_<distribution>_users in fun-distribution-<distribution>.sh by bash itself.
|
|
5
|
+
* The Website and Webship lists had a "Webmaster" account: the installer's own account is `webmaster`,
|
|
6
|
+
* user names are case-insensitive, so `drush user:create` answered "There is already a user account
|
|
7
|
+
* with the name Webmaster" and the account never existed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const assert = require('node:assert');
|
|
11
|
+
const { test } = require('node:test');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { spawnSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const SCRIPTS = path.join(__dirname, '..', '..', '..', 'scripts');
|
|
17
|
+
const FUNCTIONS = path.join(SCRIPTS, 'functions');
|
|
18
|
+
const SETTINGS = path.join(SCRIPTS, '..', 'config', 'settings.template.yml');
|
|
19
|
+
|
|
20
|
+
/** The accounts of one distribution: [{ key, name, mail, password, role }]. */
|
|
21
|
+
function usersOf(distribution) {
|
|
22
|
+
const script = `
|
|
23
|
+
ddev() { :; }
|
|
24
|
+
source ${FUNCTIONS}/fun-distribution-${distribution}.sh
|
|
25
|
+
set_${distribution}_users
|
|
26
|
+
for user in "\${users[@]}"; do
|
|
27
|
+
n="user_\${user}_name"; m="user_\${user}_mail"; p="user_\${user}_password"; r="user_\${user}_role"
|
|
28
|
+
printf '%s|%s|%s|%s|%s\\n' "\${user}" "\${!n}" "\${!m}" "\${!p}" "\${!r}"
|
|
29
|
+
done`;
|
|
30
|
+
const run = spawnSync('bash', ['-c', script], { encoding: 'utf8' });
|
|
31
|
+
return run.stdout.trim().split('\n').filter(Boolean).map((line) => {
|
|
32
|
+
const [key, name, mail, password, role] = line.split('|');
|
|
33
|
+
return { key, name, mail, password, role };
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const distributions = fs.readdirSync(FUNCTIONS)
|
|
38
|
+
.map((f) => (f.match(/^fun-distribution-([a-z0-9_]+)\.sh$/) || [])[1])
|
|
39
|
+
.filter((d) => d && fs.readFileSync(path.join(FUNCTIONS, `fun-distribution-${d}.sh`), 'utf8').includes(`set_${d}_users()`));
|
|
40
|
+
|
|
41
|
+
// The account the installer creates: account.name in settings.yml.
|
|
42
|
+
const installer = (fs.readFileSync(SETTINGS, 'utf8').match(/^account:\n(?:[ \t]+.*\n)*?[ \t]+name:[ \t]*(\S+)/m) || [])[1];
|
|
43
|
+
|
|
44
|
+
test('the installer account and the distributions with a user list are found', () => {
|
|
45
|
+
assert.strictEqual(installer, 'webmaster');
|
|
46
|
+
for (const d of ['drupal', 'drupal_cms', 'webship', 'website', 'webships', 'cucumber']) assert.ok(distributions.includes(d), d);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
for (const distribution of distributions) {
|
|
50
|
+
test(`${distribution}: every default account is complete, and no name is taken`, () => {
|
|
51
|
+
const users = usersOf(distribution);
|
|
52
|
+
assert.ok(users.length > 0, 'the list is not empty');
|
|
53
|
+
for (const u of users) {
|
|
54
|
+
assert.ok(u.name && u.mail && u.password && u.role, `${u.key} misses a value: ${JSON.stringify(u)}`);
|
|
55
|
+
assert.notStrictEqual(u.name.toLowerCase(), installer.toLowerCase(), `"${u.name}" is the installer's account: user names are case-insensitive`);
|
|
56
|
+
}
|
|
57
|
+
const names = users.map((u) => u.name.toLowerCase());
|
|
58
|
+
assert.strictEqual(new Set(names).size, names.length, `two accounts share a name: ${names.join(', ')}`);
|
|
59
|
+
const mails = users.map((u) => u.mail.toLowerCase());
|
|
60
|
+
assert.strictEqual(new Set(mails).size, mails.length, `two accounts share a mail: ${mails.join(', ')}`);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The ddev calls of add_users for one distribution, with ddev stubbed. */
|
|
65
|
+
function addUsers(distribution, { enableFails = false } = {}) {
|
|
66
|
+
const os = require('os');
|
|
67
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-users-'));
|
|
68
|
+
const log = path.join(dir, 'ddev.log');
|
|
69
|
+
fs.mkdirSync(path.join(dir, 'ws', 'p1', 'web'), { recursive: true });
|
|
70
|
+
const script = `
|
|
71
|
+
WORKSPACE_SCRIPTS=${SCRIPTS}; WORKSPACE_ROOT=${dir}; doc_name=ws; PROJECT_NAME=p1; distribution_name=${distribution}; distribution_webroot=web
|
|
72
|
+
ddev() { printf '%s\\n' "$*" >> ${log}; if [ "${enableFails ? 'yes' : 'no'}" = yes ] && [ "$1 $2" = 'drush pm:enable' ]; then return 1; fi; return 0; }
|
|
73
|
+
source ${FUNCTIONS}/fun-users.sh
|
|
74
|
+
add_drush() { :; }
|
|
75
|
+
add_users p1 ${distribution} > /dev/null; echo "exit=$?"
|
|
76
|
+
`;
|
|
77
|
+
const run = spawnSync('bash', ['-c', script], { encoding: 'utf8' });
|
|
78
|
+
const calls = fs.existsSync(log) ? fs.readFileSync(log, 'utf8').trim().split('\n') : [];
|
|
79
|
+
return { calls, code: Number((run.stdout.match(/exit=(\d+)/) || [])[1]) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
test('cucumber: the role modules are enabled before the first account, and every role given exists then', () => {
|
|
83
|
+
const { calls, code } = addUsers('cucumber');
|
|
84
|
+
assert.strictEqual(code, 0);
|
|
85
|
+
const enable = calls.findIndex((c) => c.startsWith('drush pm:enable'));
|
|
86
|
+
const firstUser = calls.findIndex((c) => c.startsWith('drush user:create'));
|
|
87
|
+
assert.ok(enable >= 0 && enable < firstUser, calls.join(' | '));
|
|
88
|
+
// The roles of the profile itself, and the ones the enabled role modules bring.
|
|
89
|
+
const roles = new Set(['admin', 'super_admin', ...[...calls[enable].matchAll(/cucumber_user_role_([a-z_]+)/g)].map((m) => m[1])]);
|
|
90
|
+
const given = calls.filter((c) => c.startsWith('drush user:role:add')).map((c) => c.split(' ')[2]);
|
|
91
|
+
assert.deepStrictEqual(given, ['developer', 'tester', 'analyst', 'designer', 'coordinator', 'product_owner', 'admin']);
|
|
92
|
+
for (const role of given) assert.ok(roles.has(role), `nothing provides the "${role}" role`);
|
|
93
|
+
assert.ok(!given.includes('administrator'), 'Cucumber has no administrator role');
|
|
94
|
+
assert.strictEqual(calls.filter((c) => c.startsWith('drush user:create')).length, 8);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('cucumber: when the role modules cannot be enabled no account is created', () => {
|
|
98
|
+
const { calls, code } = addUsers('cucumber', { enableFails: true });
|
|
99
|
+
assert.strictEqual(code, 1);
|
|
100
|
+
assert.ok(!calls.some((c) => c.startsWith('drush user:create')), calls.join(' | '));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('a distribution with nothing to prepare adds its users as before', () => {
|
|
104
|
+
const { calls, code } = addUsers('website');
|
|
105
|
+
assert.strictEqual(code, 0);
|
|
106
|
+
assert.ok(!calls.some((c) => c.includes('pm:enable')));
|
|
107
|
+
assert.deepStrictEqual(calls.filter((c) => c.startsWith('drush user:create')).map((c) => c.split(' --mail')[0]),
|
|
108
|
+
['drush user:create Authenticated user', 'drush user:create Content editor', 'drush user:create Administrator']);
|
|
109
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The /files/<workspace>/<path> route resolves the file to `real` and must stream that. It streamed
|
|
2
|
+
// `file`, a name the route never defines, so every plain (non-range) request sent headers and no body.
|
|
3
|
+
const assert = require('node:assert');
|
|
4
|
+
const { test } = require('node:test');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
test('the files route streams the file it resolved', () => {
|
|
9
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', 'server.js'), 'utf8');
|
|
10
|
+
const start = src.indexOf('filesBrowser.realFileIn(workspaceDir(fileMatch[1]), name)');
|
|
11
|
+
assert.ok(start > 0, 'the files route was not found');
|
|
12
|
+
const route = src.slice(start, src.indexOf('builder-args', start));
|
|
13
|
+
const streams = [...route.matchAll(/fs\.createReadStream\((\w+)/g)].map((m) => m[1]);
|
|
14
|
+
assert.ok(streams.length >= 2, 'expected the range branch and the whole-file branch');
|
|
15
|
+
for (const name of streams) assert.strictEqual(name, 'real', `the route streams "${name}", which it does not define`);
|
|
16
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webship.co/workspace",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Webship Workspace: a container-based development workspace with a web dashboard. The core ships empty; workspace folders, hosting providers, git forges, container runtimes and features are @webship.co/workspace-<name> plugins you add one at a time.",
|
|
5
5
|
"license": "GPL-2.0-or-later",
|
|
6
6
|
"repository": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|