shipit-cli 0.19.3__tar.gz → 0.19.5__tar.gz
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.
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/PKG-INFO +1 -1
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/pyproject.toml +1 -1
- shipit_cli-0.19.5/src/shipit/assets/wordpress/install.sh +129 -0
- shipit_cli-0.19.5/src/shipit/version.py +5 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/volumes.py +1 -1
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/tests/test_e2e.py +558 -52
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/tests/test_volumes.py +21 -0
- shipit_cli-0.19.3/src/shipit/assets/wordpress/install.sh +0 -113
- shipit_cli-0.19.3/src/shipit/version.py +0 -5
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/.gitignore +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/README.md +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/__init__.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/assets/php/php.ini +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/assets/wordpress/.htaccess +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/assets/wordpress/start.php +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/assets/wordpress/wp-config.php +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/builders/__init__.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/builders/base.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/builders/docker.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/builders/local.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/cli.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/generator.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/procfile.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/providers/base.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/providers/go.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/providers/hugo.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/providers/jekyll.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/providers/laravel.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/providers/mkdocs.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/providers/node_static.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/providers/php.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/providers/python.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/providers/registry.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/providers/staticfile.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/providers/wordpress.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/runners/__init__.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/runners/base.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/runners/local.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/runners/wasmer.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/shipit_types.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/ui.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/src/shipit/utils.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/tests/test_cli_after_deploy.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/tests/test_generate_shipit_examples.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/tests/test_php_provider.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/tests/test_staticfile_provider.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/tests/test_version.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/tests/test_wasmer_annotations.py +0 -0
- {shipit_cli-0.19.3 → shipit_cli-0.19.5}/tests/test_wordpress_phpix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shipit-cli
|
|
3
|
-
Version: 0.19.
|
|
3
|
+
Version: 0.19.5
|
|
4
4
|
Summary: Shipit CLI is the best way to build, serve and deploy your projects anywhere.
|
|
5
5
|
Project-URL: homepage, https://wasmer.io
|
|
6
6
|
Project-URL: repository, https://github.com/wasmerio/shipit
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Needed to get the WP-CLI commands to avoid asking for the TTY size
|
|
2
|
+
IFS=$'\n\t'
|
|
3
|
+
|
|
4
|
+
export COLUMNS=80 # Prevent WP-CLI from asking for TTY size
|
|
5
|
+
export PAGER="cat"
|
|
6
|
+
|
|
7
|
+
WP_ADMIN_EMAIL=${WP_ADMIN_EMAIL:-"admin@example.com"}
|
|
8
|
+
WP_ADMIN_USERNAME=${WP_ADMIN_USERNAME:-"admin"}
|
|
9
|
+
WP_ADMIN_PASSWORD=${WP_ADMIN_PASSWORD:-"admin"}
|
|
10
|
+
WP_LOCALE=${WP_LOCALE:-"en_US"}
|
|
11
|
+
WP_SITEURL=${WP_SITEURL:-"http://localhost"}
|
|
12
|
+
WP_SITE_TITLE=${WP_SITE_TITLE:-"WordPress"}
|
|
13
|
+
|
|
14
|
+
if wp core is-installed; then
|
|
15
|
+
echo "🚀 Setting up WordPress from an existing installation"
|
|
16
|
+
if [ "${WP_UPDATE_DB:-true}" = "true" ]; then
|
|
17
|
+
echo "🛠️ Activating maintenance mode..."
|
|
18
|
+
wp maintenance-mode activate || true
|
|
19
|
+
echo "🔄 Updating database..."
|
|
20
|
+
wp core update-db
|
|
21
|
+
echo "🛠️ Deactivating maintenance mode..."
|
|
22
|
+
wp maintenance-mode deactivate || true
|
|
23
|
+
fi
|
|
24
|
+
else
|
|
25
|
+
echo "🚀 Setting up WordPress from a fresh install"
|
|
26
|
+
echo "📁 Initializing wp-content..."
|
|
27
|
+
|
|
28
|
+
mkdir -p wp-content/plugins
|
|
29
|
+
mkdir -p wp-content/themes
|
|
30
|
+
mkdir -p wp-content/upgrade
|
|
31
|
+
|
|
32
|
+
if [ -n "${WPCONTENT_BASE_PATH:-}" ] && [ -d "${WPCONTENT_BASE_PATH}" ]; then
|
|
33
|
+
shopt -s dotglob nullglob
|
|
34
|
+
# Note: change this back to copy all, once using the WP Zip files
|
|
35
|
+
# cp -R "${WPCONTENT_BASE_PATH}"/* /app/wp-content
|
|
36
|
+
cp -R "${WPCONTENT_BASE_PATH}"/plugins/* /app/wp-content/plugins || true
|
|
37
|
+
cp -R "${WPCONTENT_BASE_PATH}"/themes/twentytwenty* /app/wp-content/themes || true
|
|
38
|
+
shopt -u dotglob nullglob
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
echo "⚙️ Installing WordPress core"
|
|
42
|
+
|
|
43
|
+
wp core install \
|
|
44
|
+
--url="$WP_SITEURL" \
|
|
45
|
+
--title="$WP_SITE_TITLE" \
|
|
46
|
+
--admin_user="$WP_ADMIN_USERNAME" \
|
|
47
|
+
--admin_password="$WP_ADMIN_PASSWORD" \
|
|
48
|
+
--admin_email="$WP_ADMIN_EMAIL" \
|
|
49
|
+
--locale="$WP_LOCALE"
|
|
50
|
+
|
|
51
|
+
echo "🔄 Setting permalinks"
|
|
52
|
+
wp rewrite structure '/%year%/%monthnum%/%day%/%postname%/'
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Install plugins from WP_PLUGINS environment variable
|
|
56
|
+
if [ -n "${WP_PLUGINS:-}" ]; then
|
|
57
|
+
echo "🛠️ Installing plugins from WP_PLUGINS: $WP_PLUGINS"
|
|
58
|
+
|
|
59
|
+
IFS=',' # Split by commas
|
|
60
|
+
for PLUGIN_ENTRY in $WP_PLUGINS; do
|
|
61
|
+
if [[ "$PLUGIN_ENTRY" =~ ^https?:// ]]; then
|
|
62
|
+
echo "• Installing plugin from URL: $PLUGIN_ENTRY"
|
|
63
|
+
wp plugin install "$PLUGIN_ENTRY" --activate
|
|
64
|
+
else
|
|
65
|
+
# Extract name and version using parameter expansion
|
|
66
|
+
PLUGIN_NAME="${PLUGIN_ENTRY%%:*}"
|
|
67
|
+
PLUGIN_VERSION="${PLUGIN_ENTRY#*:}"
|
|
68
|
+
|
|
69
|
+
if [[ "$PLUGIN_NAME" == "$PLUGIN_VERSION" ]]; then
|
|
70
|
+
echo "• Installing plugin '${PLUGIN_NAME}' (latest version)..."
|
|
71
|
+
wp plugin install "$PLUGIN_NAME" --activate
|
|
72
|
+
else
|
|
73
|
+
echo "• Installing plugin '${PLUGIN_NAME}' (version: ${PLUGIN_VERSION})..."
|
|
74
|
+
wp plugin install "$PLUGIN_NAME" --version="$PLUGIN_VERSION" --activate
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Install themes from WP_THEMES environment variable
|
|
81
|
+
if [ -n "${WP_THEMES:-}" ]; then
|
|
82
|
+
echo "🎨 Installing themes from WP_THEMES: $WP_THEMES"
|
|
83
|
+
IFS=','
|
|
84
|
+
|
|
85
|
+
for THEME_ENTRY in $WP_THEMES; do
|
|
86
|
+
if [[ "$THEME_ENTRY" =~ ^https?:// ]]; then
|
|
87
|
+
echo "• Installing theme from URL: $THEME_ENTRY"
|
|
88
|
+
wp theme install "$THEME_ENTRY"
|
|
89
|
+
else
|
|
90
|
+
THEME_NAME="${THEME_ENTRY%%:*}"
|
|
91
|
+
THEME_VERSION="${THEME_ENTRY#*:}"
|
|
92
|
+
|
|
93
|
+
if [[ "$THEME_NAME" == "$THEME_VERSION" ]]; then
|
|
94
|
+
echo "• Installing theme '${THEME_NAME}' (latest version)..."
|
|
95
|
+
wp theme install "$THEME_NAME"
|
|
96
|
+
else
|
|
97
|
+
echo "• Installing theme '${THEME_NAME}' (version: ${THEME_VERSION})..."
|
|
98
|
+
wp theme install "$THEME_NAME" --version="$THEME_VERSION"
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
done
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if [ -n "${WP_DEFAULT_THEME:-}" ]; then
|
|
105
|
+
echo "✨ Activating default theme: $WP_DEFAULT_THEME"
|
|
106
|
+
wp theme activate "$WP_DEFAULT_THEME"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if [ -n "${WP_LOCALE:-}" ]; then
|
|
110
|
+
echo "🌐 Setting locale: $WP_LOCALE"
|
|
111
|
+
wp language core install "$WP_LOCALE"
|
|
112
|
+
wp language theme install --all "$WP_LOCALE"
|
|
113
|
+
wp language plugin install --all "$WP_LOCALE"
|
|
114
|
+
wp site switch-language "$WP_LOCALE"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# echo "✍️ Rewriting permalinks structure"
|
|
118
|
+
# wp rewrite flush --hard || true
|
|
119
|
+
|
|
120
|
+
# if [ ! -f "/app/wp-content/wp-config.php" ]; then
|
|
121
|
+
# cat > /app/wp-content/wp-config.php <<EOF
|
|
122
|
+
# <?php
|
|
123
|
+
# // If you need to set custom configuration, you can place it here.
|
|
124
|
+
# // This file will be included by the main wp-config.php after
|
|
125
|
+
# // loading environment variables.
|
|
126
|
+
# EOF
|
|
127
|
+
# fi
|
|
128
|
+
|
|
129
|
+
echo "✅ WordPress Setup complete"
|
|
@@ -72,7 +72,7 @@ def volume_mapdir_args(
|
|
|
72
72
|
for name, guest_path in volume_mappings.items():
|
|
73
73
|
host_path = build_backend.get_volume_path(name).absolute()
|
|
74
74
|
host_path.mkdir(parents=True, exist_ok=True)
|
|
75
|
-
args.append(f"--volume={host_path}:{guest_path}")
|
|
75
|
+
args.append(f"--volume={host_path.resolve()}:{guest_path}")
|
|
76
76
|
return args
|
|
77
77
|
|
|
78
78
|
|
|
@@ -1,21 +1,27 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
1
3
|
import os
|
|
2
4
|
import random
|
|
3
|
-
import
|
|
5
|
+
import re
|
|
6
|
+
import shlex
|
|
7
|
+
import shutil
|
|
4
8
|
import signal
|
|
5
|
-
import
|
|
9
|
+
import socket
|
|
6
10
|
import subprocess
|
|
7
|
-
import
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import uuid
|
|
14
|
+
import zipfile
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import Enum
|
|
8
17
|
from pathlib import Path
|
|
9
|
-
from typing import List
|
|
10
|
-
from
|
|
18
|
+
from typing import List
|
|
19
|
+
from urllib.parse import urlparse
|
|
20
|
+
from urllib.request import urlopen
|
|
11
21
|
|
|
12
|
-
import pytest
|
|
13
|
-
import shlex
|
|
14
|
-
import shutil
|
|
15
|
-
import contextlib
|
|
16
22
|
import aiohttp
|
|
23
|
+
import pytest
|
|
17
24
|
import yaml
|
|
18
|
-
from enum import Enum
|
|
19
25
|
|
|
20
26
|
|
|
21
27
|
class BuildMode(Enum):
|
|
@@ -34,21 +40,50 @@ class HTTPRequest:
|
|
|
34
40
|
follow_redirects: bool = True
|
|
35
41
|
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class RunCommand:
|
|
45
|
+
command: str
|
|
46
|
+
stdout_match: str | None = None
|
|
47
|
+
stderr_match: str | None = None
|
|
48
|
+
expected_returncode: int = 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class CompletedCommand:
|
|
53
|
+
returncode: int | None
|
|
54
|
+
stdout: str
|
|
55
|
+
stderr: str
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def output(self) -> str:
|
|
59
|
+
return f"[stdout]\n{self.stdout}\n[stderr]\n{self.stderr}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class E2ECase:
|
|
39
64
|
serve_pattern: str
|
|
40
65
|
http: List[HTTPRequest]
|
|
66
|
+
path: str | None = None
|
|
67
|
+
download: str | None = None
|
|
41
68
|
use_random_port: bool = True
|
|
69
|
+
env: dict[str, str] | None = None
|
|
42
70
|
extra_env: dict[str, str] | None = None
|
|
71
|
+
create_db: bool = False
|
|
72
|
+
create_wp_content_volume: bool = False
|
|
73
|
+
run_after_deploy: bool = False
|
|
74
|
+
commands: list[RunCommand] = field(default_factory=list)
|
|
43
75
|
expected_memory_limit: str | None = None
|
|
44
76
|
expect_no_memory_limit: bool = False
|
|
45
77
|
build_modes: tuple[BuildMode, ...] | None = None
|
|
46
78
|
|
|
47
79
|
def __str__(self):
|
|
48
|
-
|
|
80
|
+
if self.path:
|
|
81
|
+
return self.path
|
|
82
|
+
assert self.download is not None
|
|
83
|
+
return Path(urlparse(self.download).path).stem
|
|
49
84
|
|
|
50
85
|
def __repr__(self):
|
|
51
|
-
return self
|
|
86
|
+
return str(self)
|
|
52
87
|
|
|
53
88
|
|
|
54
89
|
@pytest.mark.e2e
|
|
@@ -85,7 +120,10 @@ class E2ECase(NamedTuple):
|
|
|
85
120
|
r"PHP 8\.3\.[0-9]+ Development Server \(http://localhost:[\d]+\) started"
|
|
86
121
|
),
|
|
87
122
|
http=[
|
|
88
|
-
HTTPRequest(
|
|
123
|
+
HTTPRequest(
|
|
124
|
+
path="/",
|
|
125
|
+
body_match=r"\"version\"\s*:\s*\"8\.3\.[0-9]+\"",
|
|
126
|
+
),
|
|
89
127
|
HTTPRequest(path="/api/greet/Alice", body_match=r"Hello, Alice!"),
|
|
90
128
|
],
|
|
91
129
|
),
|
|
@@ -97,6 +135,39 @@ class E2ECase(NamedTuple):
|
|
|
97
135
|
),
|
|
98
136
|
http=[HTTPRequest(path="/", body_match=r"WordPress")],
|
|
99
137
|
),
|
|
138
|
+
# Full WordPress release archive, built and run through Wasmer only.
|
|
139
|
+
E2ECase(
|
|
140
|
+
download="https://wordpress.org/wordpress-6.9.4.zip",
|
|
141
|
+
serve_pattern=(
|
|
142
|
+
r"listening addr"
|
|
143
|
+
),
|
|
144
|
+
http=[
|
|
145
|
+
HTTPRequest(
|
|
146
|
+
path="/",
|
|
147
|
+
expected_status=200,
|
|
148
|
+
body_match=r"WordPress",
|
|
149
|
+
)
|
|
150
|
+
],
|
|
151
|
+
use_random_port=False,
|
|
152
|
+
env={
|
|
153
|
+
"DB_NAME": "test",
|
|
154
|
+
"DB_USERNAME": "root",
|
|
155
|
+
"DB_HOST": "127.0.0.1",
|
|
156
|
+
"DB_PORT": "3306",
|
|
157
|
+
"DB_PASSWORD": "",
|
|
158
|
+
"SHIPIT_PHPIX": "true",
|
|
159
|
+
},
|
|
160
|
+
create_db=True,
|
|
161
|
+
create_wp_content_volume=True,
|
|
162
|
+
run_after_deploy=True,
|
|
163
|
+
commands=[
|
|
164
|
+
RunCommand(
|
|
165
|
+
"wp eval 'echo json_encode([\"status\" => \"ok\"]);'",
|
|
166
|
+
stdout_match=r'\{"status":"ok"\}',
|
|
167
|
+
)
|
|
168
|
+
],
|
|
169
|
+
build_modes=(BuildMode.Wasmer,),
|
|
170
|
+
),
|
|
100
171
|
# WordPress skeleton in phpix mode (Wasmer only), validate memory cap.
|
|
101
172
|
E2ECase(
|
|
102
173
|
path="examples/php-wordpress",
|
|
@@ -226,7 +297,11 @@ class E2ECase(NamedTuple):
|
|
|
226
297
|
BuildMode.WasmerAndDocker,
|
|
227
298
|
],
|
|
228
299
|
)
|
|
229
|
-
async def test_end_to_end(
|
|
300
|
+
async def test_end_to_end(
|
|
301
|
+
case: E2ECase,
|
|
302
|
+
build_mode: BuildMode,
|
|
303
|
+
tmp_path: Path,
|
|
304
|
+
):
|
|
230
305
|
# Skip if `uv` is not available in PATH
|
|
231
306
|
if not shutil.which("uv"):
|
|
232
307
|
pytest.skip("`uv` is not available in PATH")
|
|
@@ -238,29 +313,125 @@ async def test_end_to_end(case: E2ECase, build_mode: BuildMode):
|
|
|
238
313
|
pytest.skip("case is not enabled for this build mode")
|
|
239
314
|
|
|
240
315
|
repo_root = Path(__file__).resolve().parents[1]
|
|
316
|
+
project_path = await _materialize_case(case, repo_root, tmp_path)
|
|
241
317
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
318
|
+
if case.use_random_port:
|
|
319
|
+
port = get_free_port()
|
|
320
|
+
else:
|
|
321
|
+
port = 8080 # This is the default port if not specified
|
|
322
|
+
|
|
323
|
+
env = os.environ.copy()
|
|
324
|
+
if case.env:
|
|
325
|
+
env.update(case.env)
|
|
326
|
+
if case.extra_env:
|
|
327
|
+
env.update(case.extra_env)
|
|
328
|
+
|
|
329
|
+
created_db_name = None
|
|
330
|
+
wp_content_volume_dir = None
|
|
331
|
+
try:
|
|
332
|
+
if case.create_db:
|
|
333
|
+
created_db_name = await _create_mysql_database(env)
|
|
334
|
+
env["DB_NAME"] = created_db_name
|
|
335
|
+
volume_specs = None
|
|
336
|
+
if case.create_wp_content_volume:
|
|
337
|
+
wp_content_volume_dir = _create_wp_content_volume(project_path)
|
|
338
|
+
volume_specs = ["wp-content:/app/wp-content"]
|
|
339
|
+
|
|
340
|
+
if case.download or case.commands:
|
|
341
|
+
build_cmd = _shipit_build_command(project_path, build_mode, port)
|
|
342
|
+
build_result = await _run_completed_command(
|
|
343
|
+
build_cmd,
|
|
344
|
+
cwd=repo_root,
|
|
345
|
+
env=env,
|
|
346
|
+
timeout=180,
|
|
347
|
+
)
|
|
348
|
+
build_output = build_result.output
|
|
349
|
+
if build_result.returncode != 0 or "Build complete ✅" not in build_output:
|
|
350
|
+
pytest.fail(
|
|
351
|
+
"End-to-end build command failed.\n"
|
|
352
|
+
f"command={shlex.join(build_cmd)}\n"
|
|
353
|
+
f"returncode={build_result.returncode}\n\n"
|
|
354
|
+
f"--- Captured output start ---\n{build_output}\n"
|
|
355
|
+
"--- Captured output end ---"
|
|
356
|
+
)
|
|
263
357
|
|
|
358
|
+
run_cmd = _shipit_run_command(
|
|
359
|
+
project_path,
|
|
360
|
+
build_mode,
|
|
361
|
+
run_after_deploy=case.run_after_deploy,
|
|
362
|
+
start=True,
|
|
363
|
+
volume_specs=volume_specs,
|
|
364
|
+
)
|
|
365
|
+
await _run_server_and_check(
|
|
366
|
+
case=case,
|
|
367
|
+
cmd=run_cmd,
|
|
368
|
+
cwd=repo_root,
|
|
369
|
+
env=env,
|
|
370
|
+
project_path=project_path,
|
|
371
|
+
port=port,
|
|
372
|
+
expect_build=False,
|
|
373
|
+
)
|
|
374
|
+
for command in case.commands:
|
|
375
|
+
cmd = _shipit_run_command(
|
|
376
|
+
project_path,
|
|
377
|
+
build_mode,
|
|
378
|
+
command=command.command,
|
|
379
|
+
volume_specs=volume_specs,
|
|
380
|
+
)
|
|
381
|
+
result = await _run_completed_command(
|
|
382
|
+
cmd,
|
|
383
|
+
cwd=repo_root,
|
|
384
|
+
env=env,
|
|
385
|
+
timeout=180,
|
|
386
|
+
)
|
|
387
|
+
_assert_run_command(command, cmd, result)
|
|
388
|
+
return
|
|
389
|
+
|
|
390
|
+
cmd = _shipit_auto_command(
|
|
391
|
+
project_path,
|
|
392
|
+
build_mode,
|
|
393
|
+
port,
|
|
394
|
+
run_after_deploy=case.run_after_deploy,
|
|
395
|
+
)
|
|
396
|
+
await _run_server_and_check(
|
|
397
|
+
case=case,
|
|
398
|
+
cmd=cmd,
|
|
399
|
+
cwd=repo_root,
|
|
400
|
+
env=env,
|
|
401
|
+
project_path=project_path,
|
|
402
|
+
port=port,
|
|
403
|
+
expect_build=True,
|
|
404
|
+
)
|
|
405
|
+
finally:
|
|
406
|
+
if created_db_name:
|
|
407
|
+
drop_result = await _drop_mysql_database(env, created_db_name)
|
|
408
|
+
if drop_result.returncode != 0:
|
|
409
|
+
drop_error = (
|
|
410
|
+
"Failed to drop temporary MySQL database.\n"
|
|
411
|
+
f"database={created_db_name}\n\n"
|
|
412
|
+
f"--- Captured output start ---\n{drop_result.output}\n"
|
|
413
|
+
"--- Captured output end ---"
|
|
414
|
+
)
|
|
415
|
+
if sys.exc_info()[0] is None:
|
|
416
|
+
try:
|
|
417
|
+
if wp_content_volume_dir:
|
|
418
|
+
shutil.rmtree(wp_content_volume_dir, ignore_errors=True)
|
|
419
|
+
finally:
|
|
420
|
+
pytest.fail(drop_error)
|
|
421
|
+
print(drop_error)
|
|
422
|
+
if wp_content_volume_dir:
|
|
423
|
+
shutil.rmtree(wp_content_volume_dir, ignore_errors=True)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
async def _run_server_and_check(
|
|
427
|
+
case: E2ECase,
|
|
428
|
+
cmd: list[str],
|
|
429
|
+
cwd: Path,
|
|
430
|
+
env: dict[str, str],
|
|
431
|
+
project_path: Path,
|
|
432
|
+
port: int,
|
|
433
|
+
expect_build: bool,
|
|
434
|
+
) -> None:
|
|
264
435
|
build_phrase = "Build complete ✅"
|
|
265
436
|
serve_re = re.compile(case.serve_pattern)
|
|
266
437
|
|
|
@@ -268,19 +439,9 @@ async def test_end_to_end(case: E2ECase, build_mode: BuildMode):
|
|
|
268
439
|
start_new_session = os.name != "nt"
|
|
269
440
|
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
|
|
270
441
|
|
|
271
|
-
env = os.environ.copy()
|
|
272
|
-
if case.extra_env:
|
|
273
|
-
env.update(case.extra_env)
|
|
274
|
-
if case.use_random_port:
|
|
275
|
-
port = get_free_port()
|
|
276
|
-
else:
|
|
277
|
-
port = 8080 # This is the default port if not specified
|
|
278
|
-
|
|
279
|
-
cmd.append(f"--serve-port={port}")
|
|
280
|
-
|
|
281
442
|
proc = await asyncio.create_subprocess_exec(
|
|
282
443
|
*cmd,
|
|
283
|
-
cwd=str(
|
|
444
|
+
cwd=str(cwd),
|
|
284
445
|
env=env,
|
|
285
446
|
stdout=asyncio.subprocess.PIPE,
|
|
286
447
|
stderr=asyncio.subprocess.PIPE,
|
|
@@ -290,9 +451,14 @@ async def test_end_to_end(case: E2ECase, build_mode: BuildMode):
|
|
|
290
451
|
|
|
291
452
|
output_lines: List[str] = []
|
|
292
453
|
found_build = asyncio.Event()
|
|
454
|
+
if not expect_build:
|
|
455
|
+
found_build.set()
|
|
293
456
|
found_serve = asyncio.Event()
|
|
457
|
+
matched_serve_output = False
|
|
458
|
+
verified_http_ready = False
|
|
294
459
|
|
|
295
460
|
async def reader(label: str, stream: asyncio.StreamReader) -> None:
|
|
461
|
+
nonlocal matched_serve_output
|
|
296
462
|
async for line in stream:
|
|
297
463
|
line = line.decode("utf-8", errors="replace")
|
|
298
464
|
print(f"[{label}] {line}", end="")
|
|
@@ -300,6 +466,7 @@ async def test_end_to_end(case: E2ECase, build_mode: BuildMode):
|
|
|
300
466
|
if (not found_build.is_set()) and (build_phrase in line):
|
|
301
467
|
found_build.set()
|
|
302
468
|
if (not found_serve.is_set()) and serve_re.search(line):
|
|
469
|
+
matched_serve_output = True
|
|
303
470
|
found_serve.set()
|
|
304
471
|
|
|
305
472
|
assert proc.stdout is not None and proc.stderr is not None
|
|
@@ -313,6 +480,21 @@ async def test_end_to_end(case: E2ECase, build_mode: BuildMode):
|
|
|
313
480
|
while loop.time() < end:
|
|
314
481
|
if found_build.is_set() and found_serve.is_set():
|
|
315
482
|
break
|
|
483
|
+
if (
|
|
484
|
+
found_build.is_set()
|
|
485
|
+
and not found_serve.is_set()
|
|
486
|
+
and case.http
|
|
487
|
+
):
|
|
488
|
+
readiness_request = _http_readiness_request(case.http[0])
|
|
489
|
+
verified_http_ready = await _wait_for_http_response(
|
|
490
|
+
host="localhost",
|
|
491
|
+
port=port,
|
|
492
|
+
request=readiness_request,
|
|
493
|
+
timeout=0.5,
|
|
494
|
+
)
|
|
495
|
+
if verified_http_ready:
|
|
496
|
+
found_serve.set()
|
|
497
|
+
break
|
|
316
498
|
if proc.returncode is not None:
|
|
317
499
|
# Process ended early; stop waiting
|
|
318
500
|
break
|
|
@@ -323,7 +505,7 @@ async def test_end_to_end(case: E2ECase, build_mode: BuildMode):
|
|
|
323
505
|
if found_serve.is_set():
|
|
324
506
|
if case.expected_memory_limit or case.expect_no_memory_limit:
|
|
325
507
|
app_yaml_path = (
|
|
326
|
-
|
|
508
|
+
project_path / ".shipit" / "wasmer" / "app.yaml"
|
|
327
509
|
)
|
|
328
510
|
if not app_yaml_path.is_file():
|
|
329
511
|
full_output = "".join(output_lines)
|
|
@@ -416,8 +598,331 @@ async def test_end_to_end(case: E2ECase, build_mode: BuildMode):
|
|
|
416
598
|
f"--- Captured output start ---\n{full_output}\n--- Captured output end ---"
|
|
417
599
|
)
|
|
418
600
|
|
|
419
|
-
|
|
420
|
-
|
|
601
|
+
if expect_build:
|
|
602
|
+
assert build_phrase in full_output
|
|
603
|
+
assert matched_serve_output or verified_http_ready, (
|
|
604
|
+
"Serve banner regex not found in output and HTTP readiness did not pass"
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
async def _materialize_case(
|
|
609
|
+
case: E2ECase,
|
|
610
|
+
repo_root: Path,
|
|
611
|
+
tmp_path: Path,
|
|
612
|
+
) -> Path:
|
|
613
|
+
if case.path and case.download:
|
|
614
|
+
raise ValueError("E2ECase can define either path or download, not both")
|
|
615
|
+
if case.path:
|
|
616
|
+
return repo_root / case.path
|
|
617
|
+
if not case.download:
|
|
618
|
+
raise ValueError("E2ECase requires either path or download")
|
|
619
|
+
return await asyncio.to_thread(
|
|
620
|
+
_download_and_extract_archive,
|
|
621
|
+
case.download,
|
|
622
|
+
tmp_path,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _download_and_extract_archive(url: str, tmp_path: Path) -> Path:
|
|
627
|
+
download_dir = tmp_path / "download"
|
|
628
|
+
download_dir.mkdir(parents=True, exist_ok=True)
|
|
629
|
+
archive_name = Path(urlparse(url).path).name or "download.zip"
|
|
630
|
+
archive_path = download_dir / archive_name
|
|
631
|
+
|
|
632
|
+
with urlopen(url, timeout=120) as response:
|
|
633
|
+
with archive_path.open("wb") as output:
|
|
634
|
+
shutil.copyfileobj(response, output)
|
|
635
|
+
|
|
636
|
+
extract_dir = tmp_path / "src"
|
|
637
|
+
extract_dir.mkdir(parents=True, exist_ok=True)
|
|
638
|
+
if archive_path.suffix == ".zip":
|
|
639
|
+
_extract_zip(archive_path, extract_dir)
|
|
640
|
+
else:
|
|
641
|
+
shutil.unpack_archive(str(archive_path), str(extract_dir))
|
|
642
|
+
|
|
643
|
+
children = [path for path in extract_dir.iterdir() if path.name != "__MACOSX"]
|
|
644
|
+
if len(children) == 1 and children[0].is_dir():
|
|
645
|
+
return children[0]
|
|
646
|
+
return extract_dir
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _extract_zip(archive_path: Path, extract_dir: Path) -> None:
|
|
650
|
+
extract_root = extract_dir.resolve()
|
|
651
|
+
with zipfile.ZipFile(archive_path) as archive:
|
|
652
|
+
for member in archive.infolist():
|
|
653
|
+
target = (extract_dir / member.filename).resolve()
|
|
654
|
+
if not target.is_relative_to(extract_root):
|
|
655
|
+
raise ValueError(
|
|
656
|
+
f"Archive member escapes extract dir: {member.filename}"
|
|
657
|
+
)
|
|
658
|
+
archive.extractall(extract_dir)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
async def _create_mysql_database(env: dict[str, str]) -> str:
|
|
662
|
+
name = f"shipit_e2e_{uuid.uuid4().hex}"
|
|
663
|
+
result = await _run_mysql_sql(
|
|
664
|
+
env,
|
|
665
|
+
(
|
|
666
|
+
f"CREATE DATABASE {_quote_mysql_identifier(name)} "
|
|
667
|
+
"CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"
|
|
668
|
+
),
|
|
669
|
+
)
|
|
670
|
+
if result.returncode != 0:
|
|
671
|
+
pytest.fail(
|
|
672
|
+
"Failed to create temporary MySQL database.\n"
|
|
673
|
+
f"database={name}\n\n"
|
|
674
|
+
f"--- Captured output start ---\n{result.output}\n"
|
|
675
|
+
"--- Captured output end ---"
|
|
676
|
+
)
|
|
677
|
+
return name
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
async def _drop_mysql_database(
|
|
681
|
+
env: dict[str, str],
|
|
682
|
+
name: str,
|
|
683
|
+
) -> CompletedCommand:
|
|
684
|
+
return await _run_mysql_sql(
|
|
685
|
+
env,
|
|
686
|
+
f"DROP DATABASE IF EXISTS {_quote_mysql_identifier(name)}",
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
async def _run_mysql_sql(env: dict[str, str], sql: str) -> CompletedCommand:
|
|
691
|
+
return await _run_completed_command(
|
|
692
|
+
_mysql_command(env, sql),
|
|
693
|
+
cwd=Path(__file__).resolve().parents[1],
|
|
694
|
+
env=env,
|
|
695
|
+
timeout=30,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _mysql_command(env: dict[str, str], sql: str) -> list[str]:
|
|
700
|
+
mysql = shutil.which("mysql")
|
|
701
|
+
if not mysql:
|
|
702
|
+
pytest.fail(
|
|
703
|
+
"`mysql` client is not available; it is required for "
|
|
704
|
+
"E2ECase(create_db=True)."
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
cmd = [
|
|
708
|
+
mysql,
|
|
709
|
+
"--protocol=TCP",
|
|
710
|
+
"--batch",
|
|
711
|
+
"--skip-column-names",
|
|
712
|
+
"--host",
|
|
713
|
+
env.get("DB_HOST", "127.0.0.1"),
|
|
714
|
+
"--port",
|
|
715
|
+
env.get("DB_PORT", "3306"),
|
|
716
|
+
"--user",
|
|
717
|
+
env.get("DB_USERNAME", "root"),
|
|
718
|
+
]
|
|
719
|
+
if "DB_PASSWORD" in env:
|
|
720
|
+
cmd.append(f"--password={env['DB_PASSWORD']}")
|
|
721
|
+
cmd.extend(["--execute", sql])
|
|
722
|
+
return cmd
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _quote_mysql_identifier(name: str) -> str:
|
|
726
|
+
if not re.fullmatch(r"[A-Za-z0-9_]+", name):
|
|
727
|
+
raise ValueError(f"Invalid MySQL identifier: {name!r}")
|
|
728
|
+
return f"`{name}`"
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _create_wp_content_volume(project_path: Path) -> Path:
|
|
732
|
+
host_dir = Path(
|
|
733
|
+
tempfile.mkdtemp(prefix="shipit-e2e-wp-content-", dir="/tmp")
|
|
734
|
+
)
|
|
735
|
+
volume_path = project_path / ".shipit" / "volumes" / "wp-content"
|
|
736
|
+
volume_path.parent.mkdir(parents=True, exist_ok=True)
|
|
737
|
+
if volume_path.is_symlink() or volume_path.is_file():
|
|
738
|
+
volume_path.unlink()
|
|
739
|
+
elif volume_path.is_dir():
|
|
740
|
+
shutil.rmtree(volume_path)
|
|
741
|
+
volume_path.symlink_to(host_dir, target_is_directory=True)
|
|
742
|
+
return host_dir
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _shipit_auto_command(
|
|
746
|
+
project_path: Path,
|
|
747
|
+
build_mode: BuildMode,
|
|
748
|
+
port: int,
|
|
749
|
+
run_after_deploy: bool,
|
|
750
|
+
) -> list[str]:
|
|
751
|
+
cmd = [
|
|
752
|
+
"uv",
|
|
753
|
+
"run",
|
|
754
|
+
"shipit",
|
|
755
|
+
str(project_path),
|
|
756
|
+
"--skip-prepare",
|
|
757
|
+
"--start",
|
|
758
|
+
"--regenerate",
|
|
759
|
+
]
|
|
760
|
+
if run_after_deploy:
|
|
761
|
+
cmd.append("--after-deploy")
|
|
762
|
+
_append_build_mode_flags(cmd, build_mode)
|
|
763
|
+
cmd.append(f"--serve-port={port}")
|
|
764
|
+
return cmd
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _shipit_build_command(
|
|
768
|
+
project_path: Path,
|
|
769
|
+
build_mode: BuildMode,
|
|
770
|
+
port: int,
|
|
771
|
+
) -> list[str]:
|
|
772
|
+
cmd = [
|
|
773
|
+
"uv",
|
|
774
|
+
"run",
|
|
775
|
+
"shipit",
|
|
776
|
+
str(project_path),
|
|
777
|
+
"--skip-prepare",
|
|
778
|
+
"--regenerate",
|
|
779
|
+
]
|
|
780
|
+
_append_build_mode_flags(cmd, build_mode)
|
|
781
|
+
cmd.append(f"--serve-port={port}")
|
|
782
|
+
return cmd
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def _shipit_run_command(
|
|
786
|
+
project_path: Path,
|
|
787
|
+
build_mode: BuildMode,
|
|
788
|
+
*,
|
|
789
|
+
run_after_deploy: bool = False,
|
|
790
|
+
start: bool = False,
|
|
791
|
+
command: str | None = None,
|
|
792
|
+
volume_specs: list[str] | None = None,
|
|
793
|
+
) -> list[str]:
|
|
794
|
+
cmd = [
|
|
795
|
+
"uv",
|
|
796
|
+
"run",
|
|
797
|
+
"shipit",
|
|
798
|
+
"run",
|
|
799
|
+
str(project_path),
|
|
800
|
+
]
|
|
801
|
+
if run_after_deploy:
|
|
802
|
+
cmd.append("--after-deploy")
|
|
803
|
+
if start:
|
|
804
|
+
cmd.append("--start")
|
|
805
|
+
if command:
|
|
806
|
+
cmd.append(f"--command={command}")
|
|
807
|
+
for spec in volume_specs or []:
|
|
808
|
+
cmd.extend(["--volume", spec])
|
|
809
|
+
_append_run_mode_flags(cmd, build_mode)
|
|
810
|
+
return cmd
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _append_build_mode_flags(cmd: list[str], build_mode: BuildMode) -> None:
|
|
814
|
+
if build_mode == BuildMode.Wasmer:
|
|
815
|
+
cmd.append("--wasmer")
|
|
816
|
+
cmd.append("--wasmer-registry=wasmer.wtf")
|
|
817
|
+
elif build_mode == BuildMode.WasmerAndDocker:
|
|
818
|
+
cmd.append("--wasmer")
|
|
819
|
+
cmd.append("--wasmer-registry=wasmer.wtf")
|
|
820
|
+
cmd.append("--docker")
|
|
821
|
+
elif build_mode == BuildMode.Local:
|
|
822
|
+
pass
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _append_run_mode_flags(cmd: list[str], build_mode: BuildMode) -> None:
|
|
826
|
+
if build_mode == BuildMode.Wasmer:
|
|
827
|
+
cmd.append("--wasmer")
|
|
828
|
+
cmd.append("--wasmer-registry=wasmer.wtf")
|
|
829
|
+
elif build_mode == BuildMode.WasmerAndDocker:
|
|
830
|
+
cmd.append("--wasmer")
|
|
831
|
+
cmd.append("--wasmer-registry=wasmer.wtf")
|
|
832
|
+
cmd.append("--docker")
|
|
833
|
+
elif build_mode == BuildMode.Local:
|
|
834
|
+
pass
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
async def _run_completed_command(
|
|
838
|
+
cmd: list[str],
|
|
839
|
+
cwd: Path,
|
|
840
|
+
env: dict[str, str],
|
|
841
|
+
timeout: float,
|
|
842
|
+
) -> CompletedCommand:
|
|
843
|
+
start_new_session = os.name != "nt"
|
|
844
|
+
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
|
|
845
|
+
proc = await asyncio.create_subprocess_exec(
|
|
846
|
+
*cmd,
|
|
847
|
+
cwd=str(cwd),
|
|
848
|
+
env=env,
|
|
849
|
+
stdout=asyncio.subprocess.PIPE,
|
|
850
|
+
stderr=asyncio.subprocess.PIPE,
|
|
851
|
+
start_new_session=start_new_session,
|
|
852
|
+
creationflags=creationflags,
|
|
853
|
+
)
|
|
854
|
+
try:
|
|
855
|
+
stdout, stderr = await asyncio.wait_for(
|
|
856
|
+
proc.communicate(),
|
|
857
|
+
timeout=timeout,
|
|
858
|
+
)
|
|
859
|
+
except asyncio.TimeoutError:
|
|
860
|
+
await _stop_process(proc)
|
|
861
|
+
stdout, stderr = await proc.communicate()
|
|
862
|
+
return CompletedCommand(
|
|
863
|
+
returncode=proc.returncode,
|
|
864
|
+
stdout=stdout.decode("utf-8", errors="replace"),
|
|
865
|
+
stderr=stderr.decode("utf-8", errors="replace"),
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
async def _stop_process(proc: asyncio.subprocess.Process) -> None:
|
|
870
|
+
try:
|
|
871
|
+
if os.name != "nt":
|
|
872
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGINT)
|
|
873
|
+
else:
|
|
874
|
+
proc.send_signal(signal.CTRL_BREAK_EVENT)
|
|
875
|
+
except Exception:
|
|
876
|
+
pass
|
|
877
|
+
try:
|
|
878
|
+
await asyncio.wait_for(proc.wait(), timeout=2)
|
|
879
|
+
except asyncio.TimeoutError:
|
|
880
|
+
if os.name != "nt":
|
|
881
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
882
|
+
else:
|
|
883
|
+
proc.kill()
|
|
884
|
+
await proc.wait()
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _assert_run_command(
|
|
888
|
+
command: RunCommand,
|
|
889
|
+
cmd: list[str],
|
|
890
|
+
result: CompletedCommand,
|
|
891
|
+
) -> None:
|
|
892
|
+
if result.returncode != command.expected_returncode:
|
|
893
|
+
pytest.fail(
|
|
894
|
+
"Run command exited with unexpected status.\n"
|
|
895
|
+
f"command={shlex.join(cmd)}\n"
|
|
896
|
+
f"expected_returncode={command.expected_returncode}\n"
|
|
897
|
+
f"returncode={result.returncode}\n\n"
|
|
898
|
+
f"--- Captured output start ---\n{result.output}\n"
|
|
899
|
+
"--- Captured output end ---"
|
|
900
|
+
)
|
|
901
|
+
if command.stdout_match and not re.search(command.stdout_match, result.stdout):
|
|
902
|
+
pytest.fail(
|
|
903
|
+
"Run command stdout did not match expected regex.\n"
|
|
904
|
+
f"command={shlex.join(cmd)}\n"
|
|
905
|
+
f"stdout_match={command.stdout_match!r}\n\n"
|
|
906
|
+
f"--- Captured output start ---\n{result.output}\n"
|
|
907
|
+
"--- Captured output end ---"
|
|
908
|
+
)
|
|
909
|
+
if command.stderr_match and not re.search(command.stderr_match, result.stderr):
|
|
910
|
+
pytest.fail(
|
|
911
|
+
"Run command stderr did not match expected regex.\n"
|
|
912
|
+
f"command={shlex.join(cmd)}\n"
|
|
913
|
+
f"stderr_match={command.stderr_match!r}\n\n"
|
|
914
|
+
f"--- Captured output start ---\n{result.output}\n"
|
|
915
|
+
"--- Captured output end ---"
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def _http_readiness_request(request: HTTPRequest) -> HTTPRequest:
|
|
920
|
+
return HTTPRequest(
|
|
921
|
+
path=request.path,
|
|
922
|
+
method=request.method,
|
|
923
|
+
expected_status=request.expected_status or 200,
|
|
924
|
+
follow_redirects=request.follow_redirects,
|
|
925
|
+
)
|
|
421
926
|
|
|
422
927
|
|
|
423
928
|
async def _wait_for_http_response(
|
|
@@ -426,8 +931,9 @@ async def _wait_for_http_response(
|
|
|
426
931
|
url = f"http://{host}:{port}{request.path}"
|
|
427
932
|
loop = asyncio.get_running_loop()
|
|
428
933
|
end = loop.time() + timeout
|
|
934
|
+
request_timeout = max(0.2, min(5.0, timeout))
|
|
429
935
|
async with aiohttp.ClientSession(
|
|
430
|
-
timeout=aiohttp.ClientTimeout(total=
|
|
936
|
+
timeout=aiohttp.ClientTimeout(total=request_timeout)
|
|
431
937
|
) as session:
|
|
432
938
|
while loop.time() < end:
|
|
433
939
|
try:
|
|
@@ -12,6 +12,7 @@ from shipit.volumes import (
|
|
|
12
12
|
load_volume_mappings,
|
|
13
13
|
merge_volume_mappings,
|
|
14
14
|
parse_cli_volume_mappings,
|
|
15
|
+
volume_mapdir_args,
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
|
|
@@ -139,6 +140,26 @@ def test_wasmer_runner_passes_volume_paths_into_wasmer_run(
|
|
|
139
140
|
)
|
|
140
141
|
|
|
141
142
|
|
|
143
|
+
def test_volume_mapdir_args_resolves_symlinked_volume_paths(
|
|
144
|
+
tmp_path: Path,
|
|
145
|
+
) -> None:
|
|
146
|
+
src_dir = tmp_path / "src"
|
|
147
|
+
src_dir.mkdir()
|
|
148
|
+
host_volume = tmp_path / "host-wp-content"
|
|
149
|
+
host_volume.mkdir()
|
|
150
|
+
backend = LocalBuildBackend(src_dir, tmp_path / "assets")
|
|
151
|
+
volume_link = backend.get_volume_path("wp-content")
|
|
152
|
+
volume_link.parent.mkdir(parents=True)
|
|
153
|
+
volume_link.symlink_to(host_volume, target_is_directory=True)
|
|
154
|
+
|
|
155
|
+
args = volume_mapdir_args(
|
|
156
|
+
backend,
|
|
157
|
+
{"wp-content": "/app/wp-content"},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
assert args == [f"--volume={host_volume.resolve()}:/app/wp-content"]
|
|
161
|
+
|
|
162
|
+
|
|
142
163
|
def test_parse_cli_volume_mappings() -> None:
|
|
143
164
|
assert parse_cli_volume_mappings(
|
|
144
165
|
["uploads:/app/uploads", "cache:/app/cache"]
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
# Needed to get the WP-CLI commands to avoid asking for the TTY size
|
|
2
|
-
IFS=$'\n\t'
|
|
3
|
-
|
|
4
|
-
export COLUMNS=80 # Prevent WP-CLI from asking for TTY size
|
|
5
|
-
export PAGER="cat"
|
|
6
|
-
|
|
7
|
-
WP_ADMIN_EMAIL=${WP_ADMIN_EMAIL:-"admin@example.com"}
|
|
8
|
-
WP_ADMIN_USERNAME=${WP_ADMIN_USERNAME:-"admin"}
|
|
9
|
-
WP_ADMIN_PASSWORD=${WP_ADMIN_PASSWORD:-"admin"}
|
|
10
|
-
WP_LOCALE=${WP_LOCALE:-"en_US"}
|
|
11
|
-
WP_SITEURL=${WP_SITEURL:-"http://localhost"}
|
|
12
|
-
WP_SITE_TITLE=${WP_SITE_TITLE:-"WordPress"}
|
|
13
|
-
|
|
14
|
-
echo "🚀 Starting WordPress setup..."
|
|
15
|
-
|
|
16
|
-
echo "Creating required directories..."
|
|
17
|
-
|
|
18
|
-
mkdir -p wp-content/plugins
|
|
19
|
-
mkdir -p wp-content/upgrade
|
|
20
|
-
|
|
21
|
-
if [ -n "${WPCONTENT_BASE_PATH:-}" ] && [ -d "${WPCONTENT_BASE_PATH}" ]; then
|
|
22
|
-
shopt -s dotglob nullglob
|
|
23
|
-
cp -R "${WPCONTENT_BASE_PATH}"/* /app/wp-content
|
|
24
|
-
shopt -u dotglob nullglob
|
|
25
|
-
fi
|
|
26
|
-
|
|
27
|
-
echo "Installing WordPress core"
|
|
28
|
-
|
|
29
|
-
wp core install \
|
|
30
|
-
--url="$WP_SITEURL" \
|
|
31
|
-
--title="$WP_SITE_TITLE" \
|
|
32
|
-
--admin_user="$WP_ADMIN_USERNAME" \
|
|
33
|
-
--admin_password="$WP_ADMIN_PASSWORD" \
|
|
34
|
-
--admin_email="$WP_ADMIN_EMAIL" \
|
|
35
|
-
--locale="$WP_LOCALE"
|
|
36
|
-
|
|
37
|
-
if [ "${WP_UPDATE_DB:-false}" = "true" ]; then
|
|
38
|
-
echo "Updating database..."
|
|
39
|
-
wp core update-db
|
|
40
|
-
fi
|
|
41
|
-
|
|
42
|
-
# Install plugins from WP_PLUGINS environment variable
|
|
43
|
-
if [ -n "${WP_PLUGINS:-}" ]; then
|
|
44
|
-
echo "Installing plugins from WP_PLUGINS: $WP_PLUGINS"
|
|
45
|
-
|
|
46
|
-
IFS=',' # Split by commas
|
|
47
|
-
for PLUGIN_ENTRY in $WP_PLUGINS; do
|
|
48
|
-
if [[ "$PLUGIN_ENTRY" =~ ^https?:// ]]; then
|
|
49
|
-
echo "Installing plugin from URL: $PLUGIN_ENTRY"
|
|
50
|
-
wp plugin install "$PLUGIN_ENTRY" --activate
|
|
51
|
-
else
|
|
52
|
-
# Extract name and version using parameter expansion
|
|
53
|
-
PLUGIN_NAME="${PLUGIN_ENTRY%%:*}"
|
|
54
|
-
PLUGIN_VERSION="${PLUGIN_ENTRY#*:}"
|
|
55
|
-
|
|
56
|
-
if [[ "$PLUGIN_NAME" == "$PLUGIN_VERSION" ]]; then
|
|
57
|
-
echo "Installing plugin '${PLUGIN_NAME}' (latest version)..."
|
|
58
|
-
wp plugin install "$PLUGIN_NAME" --activate
|
|
59
|
-
else
|
|
60
|
-
echo "Installing plugin '${PLUGIN_NAME}' (version: ${PLUGIN_VERSION})..."
|
|
61
|
-
wp plugin install "$PLUGIN_NAME" --version="$PLUGIN_VERSION" --activate
|
|
62
|
-
fi
|
|
63
|
-
fi
|
|
64
|
-
done
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
|
-
# Install themes from WP_THEMES environment variable
|
|
68
|
-
if [ -n "${WP_THEMES:-}" ]; then
|
|
69
|
-
echo "🎨 Installing themes from WP_THEMES: $WP_THEMES"
|
|
70
|
-
IFS=','
|
|
71
|
-
|
|
72
|
-
for THEME_ENTRY in $WP_THEMES; do
|
|
73
|
-
if [[ "$THEME_ENTRY" =~ ^https?:// ]]; then
|
|
74
|
-
echo "Installing theme from URL: $THEME_ENTRY"
|
|
75
|
-
wp theme install "$THEME_ENTRY"
|
|
76
|
-
else
|
|
77
|
-
THEME_NAME="${THEME_ENTRY%%:*}"
|
|
78
|
-
THEME_VERSION="${THEME_ENTRY#*:}"
|
|
79
|
-
|
|
80
|
-
if [[ "$THEME_NAME" == "$THEME_VERSION" ]]; then
|
|
81
|
-
echo "Installing theme '${THEME_NAME}' (latest version)..."
|
|
82
|
-
wp theme install "$THEME_NAME"
|
|
83
|
-
else
|
|
84
|
-
echo "Installing theme '${THEME_NAME}' (version: ${THEME_VERSION})..."
|
|
85
|
-
wp theme install "$THEME_NAME" --version="$THEME_VERSION"
|
|
86
|
-
fi
|
|
87
|
-
fi
|
|
88
|
-
done
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
if [ -n "${WP_DEFAULT_THEME:-}" ]; then
|
|
92
|
-
echo "Activating default theme: $WP_DEFAULT_THEME"
|
|
93
|
-
wp theme activate "$WP_DEFAULT_THEME"
|
|
94
|
-
fi
|
|
95
|
-
|
|
96
|
-
if [ -n "${WP_LOCALE:-}" ]; then
|
|
97
|
-
echo "Setting locale: $WP_LOCALE"
|
|
98
|
-
wp language core install "$WP_LOCALE"
|
|
99
|
-
wp language theme install --all "$WP_LOCALE"
|
|
100
|
-
wp language plugin install --all "$WP_LOCALE"
|
|
101
|
-
wp site switch-language "$WP_LOCALE"
|
|
102
|
-
fi
|
|
103
|
-
|
|
104
|
-
if [ ! -f "/app/wp-content/wp-config.php" ]; then
|
|
105
|
-
cat > /app/wp-content/wp-config.php <<EOF
|
|
106
|
-
<?php
|
|
107
|
-
// If you need to set custom configuration, you can place it here.
|
|
108
|
-
// This file will be included by the main wp-config.php after
|
|
109
|
-
// loading environment variables.
|
|
110
|
-
EOF
|
|
111
|
-
fi
|
|
112
|
-
|
|
113
|
-
echo "✅ WordPress Installation complete"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|