shipit-cli 0.21.2__tar.gz → 0.21.3__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.
Files changed (55) hide show
  1. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/PKG-INFO +17 -7
  2. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/README.md +16 -6
  3. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/pyproject.toml +1 -1
  4. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/assets/wordpress/install.sh +11 -7
  5. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/builders/docker.py +1 -1
  6. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/cli.py +1 -0
  7. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/node.py +100 -6
  8. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/wordpress.py +33 -5
  9. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/runners/wasmer.py +15 -15
  10. shipit_cli-0.21.3/src/shipit/version.py +12 -0
  11. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/tests/test_e2e.py +77 -1
  12. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/tests/test_node_provider.py +26 -1
  13. shipit_cli-0.21.2/src/shipit/version.py +0 -5
  14. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/.gitignore +0 -0
  15. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/__init__.py +0 -0
  16. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/assets/node/optimize-node-modules.sh +0 -0
  17. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/assets/php/php.ini +0 -0
  18. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/assets/wordpress/.htaccess +0 -0
  19. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/assets/wordpress/start.php +0 -0
  20. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/assets/wordpress/wp-config.php +0 -0
  21. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/builders/__init__.py +0 -0
  22. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/builders/base.py +0 -0
  23. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/builders/local.py +0 -0
  24. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/generator.py +0 -0
  25. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/procfile.py +0 -0
  26. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/base.py +0 -0
  27. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/go.py +0 -0
  28. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/hugo.py +0 -0
  29. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/install_context.py +0 -0
  30. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/jekyll.py +0 -0
  31. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/laravel.py +0 -0
  32. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/mkdocs.py +0 -0
  33. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/node_static.py +0 -0
  34. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/php.py +0 -0
  35. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/python.py +0 -0
  36. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/registry.py +0 -0
  37. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/providers/staticfile.py +0 -0
  38. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/runners/__init__.py +0 -0
  39. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/runners/base.py +0 -0
  40. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/runners/local.py +0 -0
  41. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/shipit_types.py +0 -0
  42. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/ui.py +0 -0
  43. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/utils.py +0 -0
  44. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/src/shipit/volumes.py +0 -0
  45. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/tests/test_cli_after_deploy.py +0 -0
  46. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/tests/test_generate_shipit_examples.py +0 -0
  47. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/tests/test_install_context.py +0 -0
  48. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/tests/test_node_static_provider.py +0 -0
  49. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/tests/test_php_provider.py +0 -0
  50. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/tests/test_staticfile_provider.py +0 -0
  51. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/tests/test_subdir.py +0 -0
  52. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/tests/test_version.py +0 -0
  53. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/tests/test_volumes.py +0 -0
  54. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/tests/test_wasmer_annotations.py +0 -0
  55. {shipit_cli-0.21.2 → shipit_cli-0.21.3}/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.21.2
3
+ Version: 0.21.3
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
@@ -145,16 +145,26 @@ The e2e tests will:
145
145
  * Run the project (locally or with Wasmer)
146
146
  * Test that the project output (via http requests) is the correct one
147
147
 
148
- ### Automatic PyPI Publish
148
+ ### Release Automation
149
149
 
150
- Publishing to PyPI is automated with GitHub Actions on version tags.
150
+ Releases are automated with Release Please and PyPI Trusted Publishing.
151
151
 
152
152
  Requirements:
153
153
 
154
- * Add a repository secret named `PYPI_API_TOKEN` with a PyPI API token.
155
- * Push tags using the `vX.Y.Z` format (for example: `v0.17.5`).
156
-
157
- On each `v*` tag push, the workflow:
154
+ * Enable GitHub Actions workflow permissions for creating pull requests in the
155
+ repository settings. Release Please uses the built-in `GITHUB_TOKEN`.
156
+ * Configure PyPI Trusted Publishing for the `shipit-cli` project:
157
+ * owner: `wasmerio`
158
+ * repository: `shipit`
159
+ * workflow: `pypi-publish.yml`
160
+ * environment: `pypi`
161
+ * Create a GitHub environment named `pypi`. Add reviewers there if releases
162
+ should require manual approval before publishing.
163
+
164
+ On each push to `main`, Release Please opens or updates a release PR based on
165
+ Conventional Commits. When that PR is merged, it creates a GitHub release with
166
+ a `vX.Y.Z` tag and dispatches the PyPI workflow for that tag with the built-in
167
+ `GITHUB_TOKEN`. The PyPI workflow also supports manual dispatch for reruns. It:
158
168
 
159
169
  * Runs regular test and e2e workflows.
160
170
  * Validates that the tag version without the leading `v` matches:
@@ -122,16 +122,26 @@ The e2e tests will:
122
122
  * Run the project (locally or with Wasmer)
123
123
  * Test that the project output (via http requests) is the correct one
124
124
 
125
- ### Automatic PyPI Publish
125
+ ### Release Automation
126
126
 
127
- Publishing to PyPI is automated with GitHub Actions on version tags.
127
+ Releases are automated with Release Please and PyPI Trusted Publishing.
128
128
 
129
129
  Requirements:
130
130
 
131
- * Add a repository secret named `PYPI_API_TOKEN` with a PyPI API token.
132
- * Push tags using the `vX.Y.Z` format (for example: `v0.17.5`).
133
-
134
- On each `v*` tag push, the workflow:
131
+ * Enable GitHub Actions workflow permissions for creating pull requests in the
132
+ repository settings. Release Please uses the built-in `GITHUB_TOKEN`.
133
+ * Configure PyPI Trusted Publishing for the `shipit-cli` project:
134
+ * owner: `wasmerio`
135
+ * repository: `shipit`
136
+ * workflow: `pypi-publish.yml`
137
+ * environment: `pypi`
138
+ * Create a GitHub environment named `pypi`. Add reviewers there if releases
139
+ should require manual approval before publishing.
140
+
141
+ On each push to `main`, Release Please opens or updates a release PR based on
142
+ Conventional Commits. When that PR is merged, it creates a GitHub release with
143
+ a `vX.Y.Z` tag and dispatches the PyPI workflow for that tag with the built-in
144
+ `GITHUB_TOKEN`. The PyPI workflow also supports manual dispatch for reruns. It:
135
145
 
136
146
  * Runs regular test and e2e workflows.
137
147
  * Validates that the tag version without the leading `v` matches:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shipit-cli"
3
- version = "0.21.2"
3
+ version = "0.21.3"
4
4
  description = "Shipit CLI is the best way to build, serve and deploy your projects anywhere."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,5 +1,6 @@
1
1
  # Needed to get the WP-CLI commands to avoid asking for the TTY size
2
2
  IFS=$'\n\t'
3
+ set -e
3
4
 
4
5
  export COLUMNS=80 # Prevent WP-CLI from asking for TTY size
5
6
  export PAGER="cat"
@@ -11,18 +12,21 @@ WP_DEFAULT_LOCALE=${WP_DEFAULT_LOCALE:-"en_US"}
11
12
  WP_LOCALE=${WP_LOCALE:-"$WP_DEFAULT_LOCALE"}
12
13
  WP_SITEURL=${WP_SITEURL:-"http://localhost"}
13
14
  WP_SITE_TITLE=${WP_SITE_TITLE:-"WordPress"}
15
+ WP_CONTENT_DIR=${WP_CONTENT_DIR:-"wp-content"}
14
16
 
15
17
  echo "📁 Initializing wp-content..."
16
- mkdir -p wp-content/plugins
17
- mkdir -p wp-content/themes
18
- mkdir -p wp-content/upgrade
18
+ mkdir -p "${WP_CONTENT_DIR}/plugins"
19
+ mkdir -p "${WP_CONTENT_DIR}/themes"
20
+ mkdir -p "${WP_CONTENT_DIR}/upgrade"
19
21
 
20
22
  if [ -n "${WPCONTENT_BASE_PATH:-}" ] && [ -d "${WPCONTENT_BASE_PATH}" ]; then
21
23
  shopt -s dotglob nullglob
22
- # Note: change this back to copy all, once using the WP Zip files.
23
- # cp -R "${WPCONTENT_BASE_PATH}"/* /app/wp-content || true
24
- cp -R "${WPCONTENT_BASE_PATH}"/plugins/* /app/wp-content/plugins || true
25
- cp -R "${WPCONTENT_BASE_PATH}"/themes/twentytwenty* /app/wp-content/themes || true
24
+ cp -R "${WPCONTENT_BASE_PATH}"/plugins/* "${WP_CONTENT_DIR}/plugins" || true
25
+ if [ -n "${WP_DEFAULT_THEME:-}" ]; then
26
+ cp -R "${WPCONTENT_BASE_PATH}"/themes/* "${WP_CONTENT_DIR}/themes" || true
27
+ else
28
+ cp -R "${WPCONTENT_BASE_PATH}"/themes/twentytwenty* "${WP_CONTENT_DIR}/themes" || true
29
+ fi
26
30
  shopt -u dotglob nullglob
27
31
  fi
28
32
 
@@ -144,7 +144,7 @@ RUN apt-get update \\
144
144
  && apt-get -y --no-install-recommends install \\
145
145
  build-essential gcc make autoconf libtool bison \\
146
146
  dpkg-dev pkg-config re2c locate \\
147
- libmariadb-dev libmariadb-dev-compat libpq-dev \\
147
+ libmariadb-dev libmariadb-dev-compat libpq-dev libsqlite3-dev \\
148
148
  libvips-dev default-libmysqlclient-dev libmagickwand-dev \\
149
149
  libicu-dev libxml2-dev libxslt-dev libyaml-dev \\
150
150
  sudo curl ca-certificates unzip git \\
@@ -1493,6 +1493,7 @@ def main() -> None:
1493
1493
  console.print(f"[bold red]{type(e).__name__}[/bold red]: {e}")
1494
1494
  if os.environ.get("SHIPIT_DEBUG", "false").lower() in ["1", "true", "yes", "y"]:
1495
1495
  raise e
1496
+ sys.exit(1)
1496
1497
 
1497
1498
 
1498
1499
  if __name__ == "__main__":
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import re
2
3
  import shlex
3
4
  from enum import Enum
4
5
  from pathlib import Path
@@ -483,6 +484,46 @@ class NodeProvider:
483
484
  "src/server.js",
484
485
  "src/index.js",
485
486
  )
487
+ NODE_BUILTIN_MODULES: ClassVar[tuple[str, ...]] = (
488
+ "assert",
489
+ "buffer",
490
+ "child_process",
491
+ "cluster",
492
+ "crypto",
493
+ "dgram",
494
+ "dns",
495
+ "events",
496
+ "fs",
497
+ "http",
498
+ "http2",
499
+ "https",
500
+ "module",
501
+ "net",
502
+ "os",
503
+ "path",
504
+ "perf_hooks",
505
+ "process",
506
+ "querystring",
507
+ "readline",
508
+ "stream",
509
+ "timers",
510
+ "tls",
511
+ "tty",
512
+ "url",
513
+ "util",
514
+ "v8",
515
+ "vm",
516
+ "worker_threads",
517
+ "zlib",
518
+ )
519
+ NODE_ENTRY_MARKER_PATTERN: ClassVar[re.Pattern[str]] = re.compile(
520
+ r"\bmodule\.exports\b"
521
+ r"|\bexports\."
522
+ r"|\b__dirname\b"
523
+ r"|\b__filename\b"
524
+ r"|\bprocess\.(?:env|argv|exit|cwd|platform|version)\b"
525
+ r"|\b[A-Za-z_$][\w$]*\.listen\s*\("
526
+ )
486
527
  START_PROGRAMS = {
487
528
  "edge",
488
529
  "node",
@@ -598,12 +639,62 @@ class NodeProvider:
598
639
  if (path / "package.json").is_file():
599
640
  return DetectResult(cls.name(), 30)
600
641
 
601
- for entry_file in cls.COMMON_ENTRY_FILES:
602
- if (path / entry_file).is_file():
603
- return DetectResult(cls.name(), 30)
642
+ if cls._common_entry_file(path, require_node_evidence=True):
643
+ return DetectResult(cls.name(), 30)
644
+
645
+ return None
604
646
 
647
+ @classmethod
648
+ def _common_entry_file(
649
+ cls,
650
+ path: Path,
651
+ *,
652
+ require_node_evidence: bool,
653
+ ) -> Optional[str]:
654
+ for entry_file in cls.COMMON_ENTRY_FILES:
655
+ entry_path = path / entry_file
656
+ if not entry_path.is_file():
657
+ continue
658
+ if not require_node_evidence:
659
+ return entry_file
660
+ if cls._looks_like_node_entry(entry_path):
661
+ return entry_file
605
662
  return None
606
663
 
664
+ @classmethod
665
+ def _looks_like_node_entry(cls, entry_path: Path) -> bool:
666
+ try:
667
+ source = entry_path.read_text(errors="ignore")
668
+ except OSError:
669
+ return False
670
+
671
+ first_line = source.splitlines()[0] if source else ""
672
+ has_node_shebang = (
673
+ first_line.startswith("#!")
674
+ and re.search(r"\bnode(?:js)?\b", first_line)
675
+ )
676
+ if has_node_shebang:
677
+ return True
678
+
679
+ if cls.NODE_ENTRY_MARKER_PATTERN.search(source):
680
+ return True
681
+
682
+ module_ref = cls._node_builtin_module_ref_pattern()
683
+ module_patterns = (
684
+ rf"\brequire\s*\(\s*['\"]{module_ref}['\"]\s*\)",
685
+ rf"\bimport\s*\(\s*['\"]{module_ref}['\"]\s*\)",
686
+ rf"\bfrom\s*['\"]{module_ref}['\"]",
687
+ rf"\bimport\s*['\"]{module_ref}['\"]",
688
+ )
689
+ return any(re.search(pattern, source) for pattern in module_patterns)
690
+
691
+ @classmethod
692
+ def _node_builtin_module_ref_pattern(cls) -> str:
693
+ modules = "|".join(
694
+ re.escape(module) for module in cls.NODE_BUILTIN_MODULES
695
+ )
696
+ return rf"(?:node:)?(?:{modules})(?:/[^'\"\s)]*)?"
697
+
607
698
  @classmethod
608
699
  def detect_package_manager(cls, path: Path) -> PackageManager:
609
700
  package_json = cls.parse_package_json(path) or {}
@@ -921,9 +1012,12 @@ class NodeProvider:
921
1012
  if isinstance(main, str) and main.strip():
922
1013
  return cls._node_entry_command(main.strip())
923
1014
 
924
- for entry_file in cls.COMMON_ENTRY_FILES:
925
- if (path / entry_file).is_file():
926
- return cls._node_entry_command(entry_file)
1015
+ entry_file = cls._common_entry_file(
1016
+ path,
1017
+ require_node_evidence=not (path / "package.json").is_file(),
1018
+ )
1019
+ if entry_file:
1020
+ return cls._node_entry_command(entry_file)
927
1021
 
928
1022
  return None
929
1023
 
@@ -47,6 +47,9 @@ class WordPressProvider(PhpProvider):
47
47
  THEME_HEADER_RE = re.compile(
48
48
  r"^[ \t/*#@]*Theme Name\s*:", re.I | re.M
49
49
  )
50
+ TEXT_DOMAIN_RE = re.compile(
51
+ r"^[ \t/*#@]*Text Domain\s*:\s*([^\s*/#@]+)", re.I | re.M
52
+ )
50
53
 
51
54
  @classmethod
52
55
  def name(cls) -> str:
@@ -93,7 +96,9 @@ class WordPressProvider(PhpProvider):
93
96
  for plugin_file in plugin_files:
94
97
  if not cls._file_has_header(plugin_file, cls.PLUGIN_HEADER_RE):
95
98
  continue
96
- slug = cls._slugify(path.name)
99
+ slug = cls._detect_text_domain_slug(plugin_file) or cls._slugify(
100
+ path.name
101
+ )
97
102
  return WordPressExtension(
98
103
  kind="plugin",
99
104
  slug=slug,
@@ -113,7 +118,9 @@ class WordPressProvider(PhpProvider):
113
118
  or (path / "theme.json").is_file()
114
119
  ):
115
120
  return None
116
- slug = cls._slugify(path.name)
121
+ slug = cls._detect_text_domain_slug(style_css) or cls._slugify(
122
+ path.name
123
+ )
117
124
  return WordPressExtension(
118
125
  kind="theme",
119
126
  slug=slug,
@@ -121,15 +128,36 @@ class WordPressProvider(PhpProvider):
121
128
  source_file="style.css",
122
129
  )
123
130
 
131
+ @classmethod
132
+ def _detect_text_domain_slug(cls, header_file: Path) -> Optional[str]:
133
+ text_domain = cls._file_header_value(
134
+ header_file,
135
+ cls.TEXT_DOMAIN_RE,
136
+ )
137
+ if not text_domain:
138
+ return None
139
+ return cls._slugify(text_domain)
140
+
124
141
  @classmethod
125
142
  def _file_has_header(cls, path: Path, pattern: re.Pattern[str]) -> bool:
143
+ return cls._file_header_value(path, pattern) is not None
144
+
145
+ @classmethod
146
+ def _file_header_value(
147
+ cls, path: Path, pattern: re.Pattern[str]
148
+ ) -> Optional[str]:
126
149
  if not path.is_file():
127
- return False
150
+ return None
128
151
  try:
129
152
  contents = path.read_text(errors="ignore")[: cls.HEADER_SCAN_BYTES]
130
153
  except OSError:
131
- return False
132
- return bool(pattern.search(contents))
154
+ return None
155
+ match = pattern.search(contents)
156
+ if not match:
157
+ return None
158
+ if match.lastindex:
159
+ return match.group(1).strip()
160
+ return match.group(0).strip()
133
161
 
134
162
  @staticmethod
135
163
  def _slugify(value: str) -> str:
@@ -167,28 +167,28 @@ class WasmerRunner:
167
167
  },
168
168
  "phpix": {
169
169
  "dependencies": {
170
- "latest": "phpix/phpix-83-32bit@=0.2.0",
171
- "8.3": "phpix/phpix-83-32bit@=0.2.0",
172
- "8.3.29": "phpix/phpix-83-32bit@=0.2.0",
173
- "8.2": "phpix/phpix-82-32bit@=0.2.0",
174
- "8.1": "phpix/phpix-81-32bit@=0.2.0",
170
+ "latest": "phpix/phpix-83-32bit@=0.2.2",
171
+ "8.3": "phpix/phpix-83-32bit@=0.2.2",
172
+ "8.3.29": "phpix/phpix-83-32bit@=0.2.2",
173
+ "8.2": "phpix/phpix-82-32bit@=0.2.2",
174
+ "8.1": "phpix/phpix-81-32bit@=0.2.2",
175
175
  "7.4": "php/php-32@=7.4.3301", # Note, we don't have PHPix + PHP 7.4 and never will
176
176
  },
177
177
  "architecture_dependencies": {
178
178
  "64-bit": {
179
- "latest": "phpix/phpix-83-64bit@=0.2.0",
180
- "8.3": "phpix/phpix-83-64bit@=0.2.0",
181
- "8.3.29": "phpix/phpix-83-64bit@=0.2.0",
182
- "8.2": "phpix/phpix-82-64bit@=0.2.0",
183
- "8.1": "phpix/phpix-81-64bit@=0.2.0",
179
+ "latest": "phpix/phpix-83-64bit@=0.2.2",
180
+ "8.3": "phpix/phpix-83-64bit@=0.2.2",
181
+ "8.3.29": "phpix/phpix-83-64bit@=0.2.2",
182
+ "8.2": "phpix/phpix-82-64bit@=0.2.2",
183
+ "8.1": "phpix/phpix-81-64bit@=0.2.2",
184
184
  "7.4": "php/php-64@=7.4.3301",
185
185
  },
186
186
  "32-bit": {
187
- "latest": "phpix/phpix-83-32bit@=0.2.0",
188
- "8.3": "phpix/phpix-83-32bit@=0.2.0",
189
- "8.3.29": "phpix/phpix-83-32bit@=0.2.0",
190
- "8.2": "phpix/phpix-82-32bit@=0.2.0",
191
- "8.1": "phpix/phpix-81-32bit@=0.2.0",
187
+ "latest": "phpix/phpix-83-32bit@=0.2.2",
188
+ "8.3": "phpix/phpix-83-32bit@=0.2.2",
189
+ "8.3.29": "phpix/phpix-83-32bit@=0.2.2",
190
+ "8.2": "phpix/phpix-82-32bit@=0.2.2",
191
+ "8.1": "phpix/phpix-81-32bit@=0.2.2",
192
192
  "7.4": "php/php-32@=7.4.3301",
193
193
  },
194
194
  },
@@ -0,0 +1,12 @@
1
+ __all__ = ["version", "version_info"]
2
+
3
+
4
+ version = "0.21.3" # x-release-please-version
5
+ _version_parts = version.split(".")
6
+ version_info = (
7
+ int(_version_parts[0]),
8
+ int(_version_parts[1]),
9
+ int(_version_parts[2]),
10
+ "final",
11
+ 0,
12
+ )
@@ -65,6 +65,7 @@ class E2ECase:
65
65
  http: List[HTTPRequest]
66
66
  path: str | None = None
67
67
  download: str | None = None
68
+ name: str | None = None
68
69
  use_random_port: bool = True
69
70
  env: dict[str, str] | None = None
70
71
  extra_env: dict[str, str] | None = None
@@ -77,6 +78,8 @@ class E2ECase:
77
78
  build_modes: tuple[BuildMode, ...] | None = None
78
79
 
79
80
  def __str__(self):
81
+ if self.name:
82
+ return self.name
80
83
  if self.path:
81
84
  return self.path
82
85
  assert self.download is not None
@@ -86,6 +89,12 @@ class E2ECase:
86
89
  return str(self)
87
90
 
88
91
 
92
+ BRO_BARBERSHOP_ARCHIVE_URL = (
93
+ "https://github.com/motopress/bro-barbershop/archive/"
94
+ "refs/heads/master.zip"
95
+ )
96
+
97
+
89
98
  @pytest.mark.e2e
90
99
  @pytest.mark.asyncio
91
100
  @pytest.mark.parametrize(
@@ -203,6 +212,41 @@ class E2ECase:
203
212
  ],
204
213
  build_modes=(BuildMode.Wasmer,),
205
214
  ),
215
+ # Real WordPress theme from GitHub; validates custom theme activation.
216
+ E2ECase(
217
+ download=BRO_BARBERSHOP_ARCHIVE_URL,
218
+ name="wordpress_bro_barbershop_theme",
219
+ serve_pattern=(
220
+ r"listening addr"
221
+ ),
222
+ http=[
223
+ HTTPRequest(
224
+ path="/",
225
+ expected_status=200,
226
+ )
227
+ ],
228
+ use_random_port=False,
229
+ env={
230
+ "DB_NAME": "test",
231
+ "DB_USERNAME": "root",
232
+ "DB_HOST": "127.0.0.1",
233
+ "DB_PORT": "3306",
234
+ "DB_PASSWORD": "",
235
+ "SHIPIT_PHPIX": "true",
236
+ "SHIPIT_WP_VERSION": "6.9.4",
237
+ },
238
+ create_db=True,
239
+ create_wp_content_volume=True,
240
+ run_after_deploy=True,
241
+ commands=[
242
+ RunCommand("wp theme is-active bro-barbershop"),
243
+ RunCommand(
244
+ "wp option get stylesheet",
245
+ stdout_match=r"^bro-barbershop\s*$",
246
+ ),
247
+ ],
248
+ build_modes=(BuildMode.Wasmer,),
249
+ ),
206
250
  # WordPress skeleton in phpix mode (Wasmer only), validate memory cap.
207
251
  E2ECase(
208
252
  path="examples/php-wordpress",
@@ -230,6 +274,13 @@ class E2ECase:
230
274
  serve_pattern=r"server is listening on",
231
275
  http=[HTTPRequest(path="/", body_match=r"Test")],
232
276
  ),
277
+ # Static HTML app with browser JavaScript should not be treated as Node.
278
+ E2ECase(
279
+ path="examples/static-htmlwithjs",
280
+ serve_pattern=r"server is listening on",
281
+ http=[HTTPRequest(path="/", body_match=r"Static HTML with JS")],
282
+ build_modes=(BuildMode.Wasmer,),
283
+ ),
233
284
  # Staticfile provider serving content under site/
234
285
  E2ECase(
235
286
  path="examples/staticfile",
@@ -641,11 +692,16 @@ class E2ECase:
641
692
  serve_pattern=r"Uvicorn running on .*",
642
693
  http=[HTTPRequest(path="/", body_match=r"Welcome to Flask")],
643
694
  ),
644
- # Python Django via Uvicorn WSGI (check admin login)
695
+ # Python Django via Uvicorn WSGI. Docker covers collectstatic.
645
696
  E2ECase(
646
697
  path="examples/python-django",
647
698
  serve_pattern=r"Uvicorn running on .*",
648
699
  http=[HTTPRequest(path="/", body_match=r"Django")],
700
+ build_modes=(
701
+ BuildMode.Local,
702
+ BuildMode.Wasmer,
703
+ BuildMode.WasmerAndDocker,
704
+ ),
649
705
  ),
650
706
  # Python ffmpeg demo (FastAPI), homepage is static HTML form
651
707
  E2ECase(
@@ -776,6 +832,7 @@ async def test_end_to_end(
776
832
  env=env,
777
833
  timeout=180,
778
834
  )
835
+ _print_run_command_output(command, cmd, result)
779
836
  _assert_run_command(command, cmd, result)
780
837
  return
781
838
 
@@ -1276,6 +1333,25 @@ async def _stop_process(proc: asyncio.subprocess.Process) -> None:
1276
1333
  await proc.wait()
1277
1334
 
1278
1335
 
1336
+ def _print_run_command_output(
1337
+ command: RunCommand,
1338
+ cmd: list[str],
1339
+ result: CompletedCommand,
1340
+ ) -> None:
1341
+ print(f"[run-command] {shlex.join(cmd)}")
1342
+ print(
1343
+ "[run-command] "
1344
+ f"expected_returncode={command.expected_returncode} "
1345
+ f"returncode={result.returncode}"
1346
+ )
1347
+ if result.stdout:
1348
+ print("[run-command stdout]")
1349
+ print(result.stdout, end="" if result.stdout.endswith("\n") else "\n")
1350
+ if result.stderr:
1351
+ print("[run-command stderr]")
1352
+ print(result.stderr, end="" if result.stderr.endswith("\n") else "\n")
1353
+
1354
+
1279
1355
  def _assert_run_command(
1280
1356
  command: RunCommand,
1281
1357
  cmd: list[str],
@@ -123,6 +123,24 @@ def test_node_provider_detects_generic_node_example() -> None:
123
123
  assert load_provider(path, Config()) is NodeProvider
124
124
 
125
125
 
126
+ def test_common_entry_without_package_json_needs_node_evidence(
127
+ tmp_path: Path,
128
+ ) -> None:
129
+ (tmp_path / "app.js").write_text(
130
+ """import http from "node:http";
131
+
132
+ http.createServer((_req, res) => {
133
+ res.end("ok");
134
+ }).listen(process.env.PORT || 8080);
135
+ """
136
+ )
137
+
138
+ provider_config = NodeProvider.load_config(tmp_path, Config())
139
+
140
+ assert load_provider(tmp_path, Config()) is NodeProvider
141
+ assert provider_config.commands.start == "node app.js"
142
+
143
+
126
144
  @pytest.mark.parametrize(
127
145
  ("example", "framework"),
128
146
  [
@@ -332,7 +350,14 @@ def test_node_start_command_uses_package_main(tmp_path: Path) -> None:
332
350
 
333
351
 
334
352
  def test_node_start_command_uses_common_entry_file(tmp_path: Path) -> None:
335
- (tmp_path / "server.js").write_text("console.log('ok')\n")
353
+ (tmp_path / "server.js").write_text(
354
+ """const http = require("http");
355
+
356
+ http.createServer((_req, res) => {
357
+ res.end("ok");
358
+ }).listen(8080);
359
+ """
360
+ )
336
361
 
337
362
  provider_config = NodeProvider.load_config(tmp_path, Config())
338
363
 
@@ -1,5 +0,0 @@
1
- __all__ = ["version", "version_info"]
2
-
3
-
4
- version = "0.21.2"
5
- version_info = (0, 21, 2, "final", 0)
File without changes